import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { buildSampleModel, filterSample, sampleDetail, validateExtension } from '../src/core/sample.js'; import { snapshot } from '../src/core/showcase.js'; import { createSampleService, SAMPLE_STORAGE_KEY } from '../src/services/sample-data.js'; const seed = JSON.parse(readFileSync(new URL('../data/preview-seed.json', import.meta.url), 'utf8')); const extension = JSON.parse(readFileSync(new URL('../src/assets/showcase/demo-extension.json', import.meta.url), 'utf8')); const model = buildSampleModel(seed, {}, extension); const storage = () => { const values = new Map(); return { getItem: (k) => values.get(k) ?? null, setItem: (k, v) => values.set(k, v), removeItem: (k) => values.delete(k), values }; }; const transport = async (url) => ({ ok: true, json: async () => String(url).includes('preview-seed') ? structuredClone(seed) : structuredClone(extension) }); test('334 baseline records remain separate from 16446 rich synthetic extension records', () => { assert.equal(model.baselineEntityCount, 334); assert.equal(Object.values(model.extensionCounts).reduce((n, x) => n + x, 0), 16446); assert.equal(new Set(extension.voices.map((v) => v.rawText)).size, 12000); assert.equal(model.evidence.length + extension.citations.length, 360); assert.equal(extension.prices.length, extension.observations.length * 4); assert.ok(extension.prices.some((p) => p.unitPricePer100G == null)); assert.ok(extension.researchTasks.some((v) => v.state === 'failed' && v.failureReason)); for (const p of extension.prices.filter((p) => p.unitPricePer100G != null)) assert.ok(Math.abs(p.unitPricePer100G - p.pricePaid / p.packCount / p.netWeightG * 100) <= .0051); }); test('extension validation rejects live data and unrelated references', () => { const ext = structuredClone(extension); ext.voices[0].dataMode = 'live'; assert.throws(() => validateExtension(ext, snapshot(seed)), /非模拟/); ext.voices[0].dataMode = 'sample'; ext.citations[0].quote = '不在原文里'; assert.throws(() => validateExtension(ext, snapshot(seed)), /逐字/); }); test('member, group and project filters agree across projects, reports, sources and cards', () => { const member = 'DEMO-M31'; const f = filterSample(model, { member }); assert.ok(f.projects.some((p) => p.id === 'DEMO-P10'), 'cross-group collaborator participates'); const ids = new Set(f.projects.map((p) => p.id)); assert.ok(f.reports.every((r) => ids.has(r.projectId))); assert.ok(f.opportunities.every((o) => ids.has(o.linkedProjectId))); const g = filterSample(model, { group: 'DEMO-G2' }); assert.equal(g.projects.length, 2); assert.equal(g.opportunities.length, 4); assert.equal(g.voices.length, 2000); assert.ok(g.reports.every((r) => r.groupId === 'DEMO-G2')); assert.equal(filterSample(model, { group: 'DEMO-G1', project: 'DEMO-P03' }).projects.length, 0); assert.equal(filterSample(model, { search: '完全无此关键词XYZ' }).voices.length, 0); }); test('details preserve report, question and concept fields, with no live fallback', () => { const p = sampleDetail(model, 'project', 'DEMO-P01'); assert.equal(p.questions.length, 6); assert.equal(p.concepts.length, 2); assert.equal(p.reports.length, 3); assert.ok(p.questions.every((q) => q.question && q.requiredMaterial && q.ownerMemberId)); assert.equal(sampleDetail(model, 'project', 'DEMO-P10').concepts.length, 0); assert.equal(sampleDetail(model, 'source', 'UNKNOWN'), null); assert.ok(sampleDetail(model, 'source', extension.voices[0].id).row.rawText); }); test('state, score replacement, comments and refresh remain in a separate namespace', async () => { const store = storage(); store.setItem('kf-board:me', 'unchanged'); const service = createSampleService({ storage: store, transport, now: () => '2026-11-03T08:00:00Z' }); await service.load(); service.mutate('status', { id: 'DEMO-O01', status: 'candidate', user: '模拟甲' }); service.mutate('score', { id: 'DEMO-O01', user: '甲', score: 80 }); service.mutate('score', { id: 'DEMO-O01', user: '乙', score: 60 }); service.mutate('score', { id: 'DEMO-O01', user: '甲', score: 100 }); assert.equal(service.model.opportunitiesById['DEMO-O01'].stats.avg, 80); assert.equal(service.model.opportunitiesById['DEMO-O01'].stats.n, 2); service.mutate('comment', { id: 'DEMO-P01', user: '甲', body: '先核复水条件' }); const refreshed = createSampleService({ storage: store, transport }); await refreshed.load(); assert.equal(refreshed.model.opportunitiesById['DEMO-O01'].status, 'candidate'); assert.equal(refreshed.model.comments['DEMO-P01'][0].body, '先核复水条件'); service.reset(); assert.equal(service.model.opportunitiesById['DEMO-O01'].status, 'approved'); assert.equal(store.getItem('kf-board:me'), 'unchanged'); assert.equal(store.getItem(SAMPLE_STORAGE_KEY), null); }); test('stale revisions, invalid scores and failed storage writes never report success', async () => { const store = storage(); const a = createSampleService({ storage: store, transport }), b = createSampleService({ storage: store, transport }); await Promise.all([a.load(), b.load()]); a.mutate('score', { id: 'DEMO-O01', user: '甲', score: 80 }); assert.throws(() => b.mutate('score', { id: 'DEMO-O01', user: '乙', score: 20 }, 0), /其他演示窗口/); assert.equal(b.model.revision, 1); assert.throws(() => a.mutate('score', { id: 'DEMO-O01', user: '甲', score: '' }), /0—100/); assert.throws(() => a.mutate('status', { id: 'QE9hjtCj8E', status: 'approved', user: '甲' }), /模拟机会卡不存在/); const failed = createSampleService({ storage: { ...store, setItem() { throw new Error('quota full'); } }, transport }); await failed.load(); assert.throws(() => failed.mutate('score', { id: 'DEMO-O01', user: '甲', score: 30 }), /quota full/); assert.equal(failed.model.opportunitiesById['DEMO-O01'].stats.avg, 80); });