product-knowledge.store.test.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. import assert from 'node:assert/strict';
  2. import test from 'node:test';
  3. import type { ParseObject, ParseQueryResult } from '../src/db/parse-rest.client.js';
  4. import { VOC_PARSE_CLASSES } from '../src/db/parse-rest.schema.js';
  5. import { ParseRestProductKnowledgeStore, type ProductKnowledgeRecord } from '../src/modules/product-knowledge/product-knowledge.store.js';
  6. type StoredKnowledge = Omit<ProductKnowledgeRecord, 'id'> & { naturalKey: string } & ParseObject;
  7. class FakeKnowledgeClient {
  8. readonly records: StoredKnowledge[] = [];
  9. private nextId = 1;
  10. async find<T>(_className: string, options: { where?: Record<string, unknown>; limit?: number }): Promise<ParseQueryResult<T>> {
  11. return {
  12. results: this.records.filter((record) => matches(record, options.where ?? {}))
  13. .sort((left, right) => left.objectId.localeCompare(right.objectId))
  14. .slice(0, options.limit) as unknown as Array<T & ParseObject>,
  15. };
  16. }
  17. async findOne<T>(_className: string, where: Record<string, unknown>): Promise<(T & ParseObject) | null> {
  18. return (this.records.find((record) => matches(record, where)) ?? null) as (T & ParseObject) | null;
  19. }
  20. async create<T extends Record<string, unknown>>(className: string, object: T): Promise<ParseObject> {
  21. assert.equal(className, VOC_PARSE_CLASSES.productKnowledge);
  22. const now = '2026-07-27T00:00:00.000Z';
  23. const stored = { ...object, objectId: `knowledge-${this.nextId++}`, createdAt: now, updatedAt: now } as unknown as StoredKnowledge;
  24. this.records.push(stored);
  25. return stored;
  26. }
  27. async update<T extends Record<string, unknown>>(_className: string, objectId: string, patch: T): Promise<{ updatedAt: string }> {
  28. const record = this.records.find((item) => item.objectId === objectId);
  29. assert.ok(record);
  30. const updatedAt = '2026-07-27T01:00:00.000Z';
  31. Object.assign(record, patch, { updatedAt });
  32. return { updatedAt };
  33. }
  34. }
  35. function matches(record: StoredKnowledge, where: Record<string, unknown>): boolean {
  36. return Object.entries(where).every(([key, value]) => {
  37. if (key === 'objectId' && value && typeof value === 'object' && '$gt' in value) {
  38. return record.objectId > String((value as { $gt: unknown }).$gt);
  39. }
  40. return record[key as keyof StoredKnowledge] === value;
  41. });
  42. }
  43. test('product knowledge stays workspace scoped and updates without duplicates', async () => {
  44. const client = new FakeKnowledgeClient();
  45. const store = new ParseRestProductKnowledgeStore(client);
  46. const common = {
  47. productKey: 'jd:1001', productId: '1001', productRole: 'own' as const,
  48. featured: true, status: 'active' as const, tags: ['经营TOP', '经营TOP', ' 蒸烤箱 '],
  49. note: '重点跟踪', ownerUserId: 'operator', actorUserId: 'admin',
  50. };
  51. const created = await store.upsert({ workspaceId: 'demashi', ...common });
  52. await store.upsert({ workspaceId: 'other', ...common });
  53. const updated = await store.upsert({ workspaceId: 'demashi', ...common, note: '已复核', featured: false });
  54. assert.equal(client.records.length, 2);
  55. assert.equal(created.createdBy, 'admin');
  56. assert.deepEqual(created.tags, ['经营TOP', '蒸烤箱']);
  57. assert.equal(updated.note, '已复核');
  58. assert.equal(updated.featured, false);
  59. assert.equal((await store.list({ workspaceId: 'demashi', limit: 10, cursor: null })).items.length, 1);
  60. assert.equal((await store.list({ workspaceId: 'other', limit: 10, cursor: null })).items.length, 1);
  61. });
  62. test('product knowledge supports cursor paging and soft archive', async () => {
  63. const client = new FakeKnowledgeClient();
  64. const store = new ParseRestProductKnowledgeStore(client);
  65. for (const productId of ['1001', '1002', '1003']) {
  66. await store.upsert({
  67. workspaceId: 'demashi', productKey: `jd:${productId}`, productId, productRole: 'own',
  68. featured: true, status: 'active', tags: [], note: '', ownerUserId: '', actorUserId: 'admin',
  69. });
  70. }
  71. const first = await store.list({ workspaceId: 'demashi', limit: 2, cursor: null, featured: true });
  72. const second = await store.list({ workspaceId: 'demashi', limit: 2, cursor: first.nextCursor, featured: true });
  73. const archived = await store.archive('demashi', 'jd:1002', 'admin');
  74. assert.equal(first.items.length, 2);
  75. assert.ok(first.nextCursor);
  76. assert.deepEqual(second.items.map((item) => item.productId), ['1003']);
  77. assert.equal(archived?.status, 'archived');
  78. assert.equal(archived?.featured, false);
  79. });