| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687 |
- 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);
- });
|