import assert from 'node:assert/strict'; import type { AddressInfo } from 'node:net'; import test from 'node:test'; import { createLocalDemoApp } from '../src/local-app.js'; import type { DomesticDataset, DomesticMetricSummary } from '../src/types/domestic-dataset.js'; const emptyMetrics: DomesticMetricSummary = { gmv: 0, soldUnits: 0, transactionOrders: 0, transactionCustomers: 0, impressions: 0, clicks: 0, views: 0, visitors: 0, cartUnits: 0, orderAmount: 0, orderUnits: 0, orderCount: 0, refundAmount: 0, refundUnits: 0, refundOrders: 0, conversionRate: 0, clickThroughRate: 0, averageUnitPrice: 0, refundToGmvRate: 0, }; const dataset: DomesticDataset = { schemaVersion: 1, generatedAt: '2026-07-23T00:00:00.000Z', caseName: 'Demashi', platform: 'jd', source: { sourceFile: 'demashi-summary.json', sourceHash: 'test', sheets: [], dateRange: { start: '2026-07-01', end: '2026-07-23' }, }, summary: { metricRows: 0, metricProducts: 1, mappingRows: 0, relations: 0, uniqueCompetitorProducts: 0, category2Count: 0, category3Count: 0, reviewCount: 0, }, dailyTotals: [], products: [{ platform: 'jd', productId: '11266507445', productKey: 'jd:11266507445', asin: '11266507445', role: 'own', brand: 'Demashi', title: 'Local demo product', model: '', category1: '', category2: '', category3: '', source: 'excel', relationCount: 0, summary: emptyMetrics, trend: [], }], mappingGroups: [], relations: [], reviews: [], quality: { orphanMappings: [], mappingsWithoutCompetitor: [], brandWithoutProductId: [], }, }; test('local demo serves a snapshot and queryable completed sync jobs without a database', async () => { const app = createLocalDemoApp({ dataset, corsOrigins: ['http://localhost:4200'] }); const server = await new Promise>((resolve) => { const listening = app.listen(0, '127.0.0.1', () => resolve(listening)); }); try { const address = server.address() as AddressInfo; const baseUrl = `http://127.0.0.1:${address.port}`; const health = await fetch(`${baseUrl}/health`); assert.equal(health.status, 200); const healthBody = await health.json() as { mode: string; database: string; dataset: { products: number; reviews: number }; }; assert.equal(healthBody.mode, 'local-demo'); assert.equal(healthBody.database, 'deferred'); assert.deepEqual(healthBody.dataset, { caseName: 'Demashi', platform: 'jd', products: 1, reviews: 0 }); const snapshot = await fetch(`${baseUrl}/api/domestic-voc/snapshot?workspaceId=demashi&platform=jd`); assert.equal(snapshot.status, 200); assert.equal((await snapshot.json() as DomesticDataset).products[0]?.productId, '11266507445'); const knowledge = await fetch(`${baseUrl}/api/knowledge/products?workspaceId=demashi&status=active`); assert.equal(knowledge.status, 200); assert.equal((await knowledge.json() as { items: Array<{ productId: string }> }).items[0]?.productId, '11266507445'); const savedKnowledge = await fetch(`${baseUrl}/api/knowledge/products`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ workspaceId: 'demashi', productKey: 'jd:11266507445', productId: '11266507445', productRole: 'own', featured: true, status: 'active', tags: ['经营TOP', '演示重点'], note: '本地后端知识库写入验证', ownerUserId: 'local-admin', }), }); assert.equal(savedKnowledge.status, 200); const savedItem = (await savedKnowledge.json() as { item: { tags: string[]; note: string } }).item; assert.deepEqual(savedItem.tags, ['经营TOP', '演示重点']); assert.equal(savedItem.note, '本地后端知识库写入验证'); const syncResponse = await fetch(`${baseUrl}/api/domestic-voc/sync`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Idempotency-Key': 'local-demo-sync-11266507445' }, body: JSON.stringify({ productIds: ['11266507445'] }), }); assert.equal(syncResponse.status, 202); const syncBody = await syncResponse.json() as { job: { id: string; status: string; progress: number } }; assert.equal(syncBody.job.status, 'completed'); assert.equal(syncBody.job.progress, 100); const jobResponse = await fetch(`${baseUrl}/api/domestic-voc/jobs/${syncBody.job.id}`); assert.equal(jobResponse.status, 200); assert.equal((await jobResponse.json() as { job: { status: string } }).job.status, 'completed'); const partialResponse = await fetch(`${baseUrl}/api/domestic-voc/sync`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Idempotency-Key': 'local-demo-sync-missing-product' }, body: JSON.stringify({ productIds: ['missing-product'] }), }); assert.equal(partialResponse.status, 202); const partial = await partialResponse.json() as { job: { id: string; status: string } }; assert.equal(partial.job.status, 'partial'); const retryResponse = await fetch(`${baseUrl}/api/domestic-voc/jobs/${partial.job.id}/retry`, { method: 'POST' }); assert.equal(retryResponse.status, 202); assert.equal((await retryResponse.json() as { job: { status: string } }).job.status, 'partial'); const eventsResponse = await fetch(`${baseUrl}/api/domestic-voc/jobs/${partial.job.id}/events`); assert.equal(eventsResponse.status, 200); const eventTypes = (await eventsResponse.json() as { items: Array<{ type: string }> }).items.map((item) => item.type); assert.deepEqual(eventTypes, ['sync_finished', 'sync_manually_retried']); const invalidCancel = await fetch(`${baseUrl}/api/domestic-voc/jobs/${syncBody.job.id}/cancel`, { method: 'POST' }); assert.equal(invalidCancel.status, 409); } finally { await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); test('local demo workspace id is configurable and becomes the request default', async () => { const app = createLocalDemoApp({ dataset, workspaceId: 'case-two', corsOrigins: ['http://localhost:4200'], }); const server = await new Promise>((resolve) => { const listening = app.listen(0, '127.0.0.1', () => resolve(listening)); }); try { const address = server.address() as AddressInfo; const baseUrl = `http://127.0.0.1:${address.port}`; const context = await fetch(`${baseUrl}/api/saas/context`); assert.equal(context.status, 200); assert.equal((await context.json() as { workspaces: Array<{ id: string }> }).workspaces[0]?.id, 'case-two'); const snapshot = await fetch(`${baseUrl}/api/domestic-voc/snapshot?platform=jd`); assert.equal(snapshot.status, 200); assert.equal((await snapshot.json() as DomesticDataset).caseName, 'Demashi'); const sync = await fetch(`${baseUrl}/api/domestic-voc/sync`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Idempotency-Key': 'case-two-default-workspace' }, body: JSON.stringify({ productIds: ['11266507445'] }), }); assert.equal(sync.status, 202); assert.equal((await sync.json() as { job: { workspaceId: string } }).job.workspaceId, 'case-two'); } finally { await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } });