| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197 |
- 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<ReturnType<typeof app.listen>>((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<void>((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<ReturnType<typeof app.listen>>((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<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
- }
- });
|