import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { snapshot, projectStory, sourceUnion, validateSample } from '../src/core/showcase.js'; const seed = () => JSON.parse(readFileSync(new URL('../data/preview-seed.json', import.meta.url), 'utf8')); test('sample metrics use actual records, even when declared totals are stale', () => { const data = seed(); data.actualSeedCounts.projects = 999; const model = snapshot(data); assert.equal(model.total, 334); assert.equal(model.counts.projects, 12); assert.deepEqual(model.stages, { S5: 4, S4: 6, S3: 2 }); assert.deepEqual(model.states, { completed: 2, in_progress: 7, blocked: 3 }); assert.equal(model.opportunityStates.approved, 12); }); test('a live object or a second demo namespace is rejected', () => { const data = seed(); data.projects[0].dataMode = 'live'; data.members[0].demoId = 'OTHER'; assert.ok(validateSample(data).some((x) => x.includes('不是模拟对象'))); assert.throws(() => snapshot(data), /同一演示空间/); }); test('dangling references, duplicate records and mismatched quotes are rejected', () => { const data = seed(); data.projects[0].sourceIds.push('MISSING'); data.members.push({ ...data.members[0] }); data.evidence[0].quote = '这不是原文'; const errors = validateSample(data); assert.ok(errors.some((x) => x.includes('MISSING'))); assert.ok(errors.some((x) => x.includes('重复'))); assert.ok(errors.some((x) => x.includes('引文不在同源原文中'))); }); test('a malformed collection returns validation errors', () => { const data = seed(); data.evidence = {}; assert.ok(validateSample(data).some((x) => x.includes('缺少集合:evidence'))); }); test('source union counts indirect references and deduplicates direct ones', () => { const model = snapshot(seed()); const source = model.seed.sources[0]; const evidence = model.seed.evidence[0]; assert.deepEqual(sourceUnion([ { sourceIds: [source.id] }, { evidenceIds: [evidence.id] }, { sourceIds: [source.id], evidenceIds: [evidence.id] }, ], model.indexes).map((x) => x.id), [source.id]); }); test('stories preserve project scope and empty later stages', () => { const model = snapshot(seed()); const complete = projectStory(model, 'DEMO-P01'); assert.deepEqual(complete.reports.map((r) => r.stageCode), ['S3', 'S4', 'S5']); assert.deepEqual(complete.concepts.map((c) => c.version), [1, 2]); assert.equal(complete.questions.length, 6); for (const id of ['DEMO-P01', 'DEMO-P03', 'DEMO-P10']) { const story = projectStory(model, id); for (const row of [...story.reports, ...story.questions, ...story.tasks, ...story.concepts]) assert.equal(row.projectId, id); for (const row of story.ideas) assert.equal(row.intendedProjectId, id); } const insufficient = projectStory(model, 'DEMO-P10'); assert.equal(insufficient.project.stageCode, 'S3'); assert.equal(insufficient.concepts.length, 0); assert.equal(insufficient.tasks.length, 0); assert.equal(projectStory(model, 'MISSING'), null); }); test('static service retries failures, shares inflight reads and never falls back', async () => { const { loadShowcase } = await import('../src/services/showcase-data.js?retry-test'); const calls = []; const failed = (url) => { calls.push(url.href); return Promise.resolve({ ok: false, status: 503 }); }; await assert.rejects(loadShowcase({ transport: failed }), /503/); let resolve; const transport = (url) => { calls.push(url.href); return new Promise((done) => { resolve = done; }); }; const first = loadShowcase({ transport }); const second = loadShowcase({ transport }); resolve({ ok: true, json: async () => seed() }); assert.strictEqual(await first, await second); assert.equal(calls.length, 2); assert.ok(calls.every((url) => url.endsWith('/src/assets/showcase/preview-seed.json'))); });