import assert from 'node:assert/strict'; import test from 'node:test'; import type { ParseObject, ParseQueryResult } from '../src/db/parse-rest.client.js'; import { VOC_PARSE_CLASSES } from '../src/db/parse-rest.schema.js'; import { ParseRestProductKnowledgeStore, type ProductKnowledgeRecord } from '../src/modules/product-knowledge/product-knowledge.store.js'; type StoredKnowledge = Omit & { naturalKey: string } & ParseObject; class FakeKnowledgeClient { readonly records: StoredKnowledge[] = []; private nextId = 1; async find(_className: string, options: { where?: Record; limit?: number }): Promise> { return { results: this.records.filter((record) => matches(record, options.where ?? {})) .sort((left, right) => left.objectId.localeCompare(right.objectId)) .slice(0, options.limit) as unknown as Array, }; } async findOne(_className: string, where: Record): Promise<(T & ParseObject) | null> { return (this.records.find((record) => matches(record, where)) ?? null) as (T & ParseObject) | null; } async create>(className: string, object: T): Promise { assert.equal(className, VOC_PARSE_CLASSES.productKnowledge); const now = '2026-07-27T00:00:00.000Z'; const stored = { ...object, objectId: `knowledge-${this.nextId++}`, createdAt: now, updatedAt: now } as unknown as StoredKnowledge; this.records.push(stored); return stored; } async update>(_className: string, objectId: string, patch: T): Promise<{ updatedAt: string }> { const record = this.records.find((item) => item.objectId === objectId); assert.ok(record); const updatedAt = '2026-07-27T01:00:00.000Z'; Object.assign(record, patch, { updatedAt }); return { updatedAt }; } } function matches(record: StoredKnowledge, where: Record): boolean { return Object.entries(where).every(([key, value]) => { if (key === 'objectId' && value && typeof value === 'object' && '$gt' in value) { return record.objectId > String((value as { $gt: unknown }).$gt); } return record[key as keyof StoredKnowledge] === value; }); } test('product knowledge stays workspace scoped and updates without duplicates', async () => { const client = new FakeKnowledgeClient(); const store = new ParseRestProductKnowledgeStore(client); const common = { productKey: 'jd:1001', productId: '1001', productRole: 'own' as const, featured: true, status: 'active' as const, tags: ['经营TOP', '经营TOP', ' 蒸烤箱 '], note: '重点跟踪', ownerUserId: 'operator', actorUserId: 'admin', }; const created = await store.upsert({ workspaceId: 'demashi', ...common }); await store.upsert({ workspaceId: 'other', ...common }); const updated = await store.upsert({ workspaceId: 'demashi', ...common, note: '已复核', featured: false }); assert.equal(client.records.length, 2); assert.equal(created.createdBy, 'admin'); assert.deepEqual(created.tags, ['经营TOP', '蒸烤箱']); assert.equal(updated.note, '已复核'); assert.equal(updated.featured, false); assert.equal((await store.list({ workspaceId: 'demashi', limit: 10, cursor: null })).items.length, 1); assert.equal((await store.list({ workspaceId: 'other', limit: 10, cursor: null })).items.length, 1); }); test('product knowledge supports cursor paging and soft archive', async () => { const client = new FakeKnowledgeClient(); const store = new ParseRestProductKnowledgeStore(client); for (const productId of ['1001', '1002', '1003']) { await store.upsert({ workspaceId: 'demashi', productKey: `jd:${productId}`, productId, productRole: 'own', featured: true, status: 'active', tags: [], note: '', ownerUserId: '', actorUserId: 'admin', }); } const first = await store.list({ workspaceId: 'demashi', limit: 2, cursor: null, featured: true }); const second = await store.list({ workspaceId: 'demashi', limit: 2, cursor: first.nextCursor, featured: true }); const archived = await store.archive('demashi', 'jd:1002', 'admin'); assert.equal(first.items.length, 2); assert.ok(first.nextCursor); assert.deepEqual(second.items.map((item) => item.productId), ['1003']); assert.equal(archived?.status, 'archived'); assert.equal(archived?.featured, false); });