| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342 |
- import test from 'node:test';
- import assert from 'node:assert/strict';
- import fs from 'node:fs';
- import os from 'node:os';
- import path from 'node:path';
- import crypto from 'node:crypto';
- import { createApplication } from '../server/src/core.mjs';
- import { attachWorkshopHandlers } from '../server/src/workshop.mjs';
- import { runWorkshop } from '../skills/ksf-workshop-runtime/scripts/ksf-workshop.mjs';
- import { buildModel } from '../src/core/kfs/build.js';
- import { entriesForType, reportCenter, TYPE_FOR_ARTIFACT } from '../src/core/library/reports.js';
- import { reportOverview } from '../src/core/report.js';
- import { seedMemberGroups } from '../src/core/member-groups.js';
- import { workshopReview, workshopOutputRefs } from '../src/core/workshop-outputs.js';
- import { createFileAdapter } from '../server/src/files.mjs';
- import { CORE_DELIVERABLES, DELIVERY_KINDS, DELIVERY_SCHEMA, reportCategoryOf } from '../skills/ksf-workshop-runtime/scripts/report-contract.mjs';
- import { saveAndVerifyReport, prepareHtmlReport } from '../skills/ksf-workshop-runtime/scripts/save-report.mjs';
- import { reportAttachments } from '../src/core/report-attachments.js';
- import { STATISTICS_COLUMNS, statisticsRows } from '../skills/ksf-workshop-runtime/scripts/report-statistics.mjs';
- import { COURSE_ASSESSMENT_COLUMNS, courseAssessmentRows } from '../skills/ksf-workshop-runtime/scripts/course-assessment.mjs';
- // Isolated storage: these tests never connect to the production service.
- const data = JSON.parse(fs.readFileSync(new URL('../server/references/runtime-data.json', import.meta.url)));
- const course = fs.readFileSync(new URL('../skills/ksf-workshop-runtime/references/workshop-course-flow.md', import.meta.url), 'utf8');
- const curriculum = [...course.matchAll(/^\| (D[12]-(?:\d\d|N)) \| \d+ \| [^|]+ \| (S[1-5]) \| ([^\n]+)\|/gm)]
- .flatMap(([, module, stage, types]) => [...types.matchAll(/`(\w+)`/g)].map(([, type]) => ({ module, stage, type })));
- const actor = { id: 'parity-service', kind: 'service', scopes: ['data:read', 'ingest:write'], projectIds: ['*'] };
- const agentProfile = { key: 'parity-agent', name: '测试共用智能体', role: 'workshop', runtimePlatform: 'test', channel: 'test' };
- function fixture() {
- let rows = {}, seq = 0;
- const store = {
- async transaction(fn) { const prior = structuredClone(rows); try { return await fn(store); } catch (error) { rows = prior; throw error; } },
- async lock() {},
- async get(name, id) { return structuredClone(rows[name]?.find(row => row.objectId === id) || null); },
- async list(name, { where = {}, afterId, limit = 200 } = {}) {
- return structuredClone((rows[name] || []).filter(row => Object.entries(where).every(([key, value]) => JSON.stringify(row[key] ?? null) === JSON.stringify(value)))
- .filter(row => !afterId || row.objectId > afterId).sort((a, b) => a.objectId.localeCompare(b.objectId)).slice(0, limit));
- },
- async create(name, values) {
- for (const key of Object.keys(values)) assert.ok(data.tables[name][key], `${name}.${key} must exist in the schema`);
- const row = { ...structuredClone(values), objectId: 'parity' + String(++seq).padStart(8, '0'), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() };
- (rows[name] ||= []).push(row); return structuredClone(row);
- },
- async update(name, id, values) { const row = rows[name]?.find(row => row.objectId === id); assert.ok(row); Object.assign(row, structuredClone(values)); return structuredClone(row); },
- };
- const settings = { signingSecret: 'parity-test-only', functionsUrl: 'https://kangshifu.fmode.cn/api/functions' };
- const files = createFileAdapter({ store, crypto, Parse: {}, data, settings });
- const app = createApplication({ store, crypto, Parse: {}, data, settings, files });
- attachWorkshopHandlers(app, { store, data, files });
- const call = async (name, input, identity = actor) => (await app.dispatch(name, structuredClone(input), identity)).data;
- return { store, call, files,
- async start(index, workshopKey = 'parity-topic') { return call('ksfStartWorkshopSession', {
- requestId: `start-${index}-${workshopKey}`, workshopKey, dataMode: 'sample', groupId: `G${index % 5 + 1}`,
- participant: { key: `participant-${index}`, name: index < 2 ? '同名组员' : `测试组员${index}`, identityBasis: 'participant_registration' },
- agent: { key: agentProfile.key, name: agentProfile.name, role: 'workshop', identityBasis: 'operator_registration', runtimePlatform: 'test', channel: 'test' },
- conversation: { key: `conversation-${index}`, channel: 'test' },
- }); },
- async save(sessionId, item, index, version = 1, chapterId = 'analysis') {
- const body = `隔离测试正文 ${index},版本 ${version}。不是实际调研成果。`;
- return runWorkshop('workshop-save', {
- sessionId, taskKey: `parity-topic:${item.module}:report-${index}`, version, stageCode: item.stage,
- title: `隔离测试报告${index}`, artifactType: item.type, rawText: body, sourceIds: [], pendingQuestions: ['需要实际研究验证'],
- report: { executive: [body], chapters: [{ id: chapterId, title: '测试正文', sections: [
- { kind: 'para', text: body }, { kind: 'table', data: { cols: ['范围', '结论'], rows: [['隔离测试', body]] } },
- ] }] },
- }, { requestId: `save-${sessionId}-${index}-v${version}`, agentProfile, call });
- },
- async publicData() {
- const raw = Object.fromEntries(['project', 'artifact', 'insight', 'source', 'session', 'opportunity'].map(name => [name, []]));
- let cursor;
- do { const page = await call('ksfGetPublicBoard', { limit: 7, ...(cursor ? { cursor } : {}) }, null);
- for (const name of Object.keys(raw)) raw[name].push(...page[name]); cursor = page.nextCursor;
- } while (cursor);
- return raw;
- },
- };
- }
- test('Skill课表13环节的每种报告均经保存入口、精确回读、公开分页、正式分类和全文模型保留正文', async () => {
- assert.equal(new Set(curriculum.map(item => item.module)).size, 13);
- assert.equal(new Set(curriculum.map(item => item.type)).size, 7);
- const f = fixture(), { sessionId } = await f.start(0), saved = [];
- for (const [index, item] of curriculum.entries()) {
- const receipt = await f.save(sessionId, item, index);
- const read = await f.call('ksfGetRecord', { className: 'KfArtifact', objectId: receipt.artifactId });
- assert.equal(read.record.artifactType, item.type);
- assert.equal(read.record.stageCode, item.stage);
- assert.match(read.record.rawText, new RegExp(`隔离测试正文 ${index}`));
- saved.push({ item, receipt, read });
- }
- const db = buildModel(await f.publicData()), view = workshopReview(db, seedMemberGroups(db.members), { mode: 'sample' });
- assert.equal(db.artifacts.length, curriculum.length);
- assert.equal(view.rows.length, curriculum.length);
- assert.equal(view.hierarchy.flatMap(group => group.members.flatMap(member => member.reports)).length, curriculum.length);
- for (const { item, receipt, read } of saved) {
- const record = db.artifactById[receipt.artifactId];
- assert.deepEqual(record.report, read.record.report);
- assert.equal(record.rawText, read.record.rawText);
- assert.ok(entriesForType(db, TYPE_FOR_ARTIFACT[item.type]).some(entry => entry.kind === 'artifact' && entry.id === receipt.artifactId));
- assert.equal(reportOverview(db, receipt.artifactId).chapters[0].sections[1].data.rows.length, 1);
- }
- assert.equal(reportCenter(db).entries.filter(entry => entry.kind === 'artifact').length, curriculum.length);
- assert.equal(workshopReview(db, null).rows.length, 0, '样例按默认真实资料筛选不会显示,不能误报漏存');
- });
- test('35位组员共用同一智能体,5组各人的同系列报告和版本不混合,报告分类与组员归属对应', async () => {
- const f = fixture(), expected = new Map();
- for (let index = 0; index < 35; index++) {
- const { sessionId } = await f.start(index), item = curriculum[index % curriculum.length];
- await f.save(sessionId, item, 0, 1);
- const latest = await f.save(sessionId, item, 0, 2);
- expected.set(sessionId, latest.artifactId);
- }
- const db = buildModel(await f.publicData()), groups = seedMemberGroups(db.members);
- const latest = workshopReview(db, groups, { mode: 'sample' });
- assert.equal(db.members.length, 35);
- assert.equal(groups.groups.length, 5);
- assert.equal(latest.rows.length, 35);
- assert.equal(workshopReview(db, groups, { mode: 'sample', history: '1' }).rows.length, 70);
- for (const group of latest.hierarchy.filter(group => group.memberCount)) {
- assert.equal(group.memberCount, 7);
- for (const member of group.members) {
- assert.equal(member.reports.length, 1);
- const row = member.reports[0];
- assert.equal(row.artifact.memberId, member.id);
- assert.equal(row.id, expected.get(row.artifact.sessionId));
- assert.equal(row.artifact.version, 2);
- assert.equal(row.reportType, TYPE_FOR_ARTIFACT[row.artifact.type]);
- }
- }
- });
- test('七类核心交付物可承载在正式报告类型中,但章节导航与报告分类是两个维度', async () => {
- const f = fixture(), { sessionId } = await f.start(0);
- const outputs = [
- ['market', 'market-analysis', 'category_report'], ['social', 'social-needs', 'voc_report'],
- ['evidence', 'evidence-pool', 'voc_report'], ['opportunities', 'opportunity-cards', 'opportunity_card'],
- ['validation', 'opportunity-validation', 'assessment_report'], ['value', 'buying-points', 'concept'],
- ['proposal', 'product-proposal', 'concept'],
- ];
- for (const [index, [, anchor, type]] of outputs.entries()) await f.save(sessionId, { module: 'D2-08', stage: 'S5', type }, index, 1, anchor);
- const db = buildModel(await f.publicData());
- for (const [key] of outputs) assert.equal(db.artifacts.filter(artifact => workshopOutputRefs(artifact).refs.some(ref => ref.key === key)).length, 1);
- assert.equal(new Set(db.artifacts.map(reportCategoryOf)).size, 7, '七类核心交付物现在可分别分类');
- });
- function deliveryTask(sessionId, key = 'market', overrides = {}) {
- const output = CORE_DELIVERABLES.find(o => o.key === key), type = DELIVERY_KINDS[key].types[0];
- return { sessionId, taskKey: `parity-topic:D2-08:${key}`, version: 1, stageCode: 'S5',
- title: '隔离测试:' + output.name, artifactType: type, sourceIds: [], pendingQuestions: ['测试内容待实际研究验证'],
- rawText: '隔离测试的完整正文,不代表实际消费者研究。',
- report: { executive: ['隔离测试'], delivery: { schema: DELIVERY_SCHEMA, moduleId: 'D2-08', kind: key, contentStatus: 'ready', outputs: [key] },
- chapters: [{ id: output.anchor, title: output.name, sections: [{ kind: 'para', text: '该环节的完整测试正文。' }] }] }, ...overrides };
- }
- function verifier(f, override = {}) {
- return { requestId: 'new-save', invoke: (command, task, requestId) => runWorkshop(command, task, { requestId, agentProfile, call: f.call }),
- publicRead: p => f.call('ksfGetPublicBoard', p, null),
- readAttachment: async url => (await f.files.readFile({ query: Object.fromEntries(new URL(url).searchParams) }, null)).bytes,
- ...override };
- }
- const inlineFile = (name, contentType, bytes) => ({ name, contentType, sha256: crypto.createHash('sha256').update(bytes).digest('hex'), sizeBytes: bytes.length, dataBase64: bytes.toString('base64') });
- test('七类新报告经实际Skill保存、精确回读、公开读取校验,个人七项提交状态与分类一致', async () => {
- const f = fixture(), { sessionId } = await f.start(0);
- for (const { key } of CORE_DELIVERABLES) {
- const result = await saveAndVerifyReport(deliveryTask(sessionId, key), verifier(f, { requestId: 'verified-' + key }));
- assert.equal(result.recordVerified, true);assert.equal(result.publicDataVerified, true);assert.equal(result.browserVerified, false);
- }
- const db = buildModel(await f.publicData()), state = seedMemberGroups(db.members), view = workshopReview(db, state, { mode: 'sample' });
- const member = view.hierarchy.flatMap(g => g.members).find(m => m.reports.length);
- assert.equal(member.topics.length, 1);assert.deepEqual(member.topics[0].outputs.map(o => o.state), Array(7).fill('saved'));
- assert.equal(new Set(member.reports.map(r => r.reportType)).size, 7);
- for (const row of member.reports) assert.ok(entriesForType(db, row.reportType).some(e => e.id === row.id && e.href));
- });
- test('默认HTML生成后走实际保存、编号回读、公开投影和文件字节回读;员工确认保持待处理', async t => {
- const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ksf-html-parity-'));
- t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
- const f = fixture(), { sessionId } = await f.start(0);
- for (const { key } of CORE_DELIVERABLES) {
- const { task, htmlPath } = prepareHtmlReport(deliveryTask(sessionId, key), { directory });
- const receipt = await saveAndVerifyReport(task, verifier(f, { requestId: 'html-' + key }));
- assert.equal(receipt.filesVerified, 1);assert.equal(receipt.employeeConfirmation, 'pending');
- assert.equal(receipt.htmlFiles.length, 1);assert.equal(receipt.publicDataVerified, true);
- const file = receipt.htmlFiles[0];
- assert.equal(new URL(file.previewUrl).searchParams.get('artifactId'), receipt.artifactId);
- assert.equal(new URL(file.previewUrl).searchParams.get('preview'), '1');
- const actual = await f.files.readFile({ query: Object.fromEntries(new URL(file.url).searchParams) }, null);
- assert.ok(actual.bytes.equals(fs.readFileSync(htmlPath)));
- const row = await f.store.get('KfArtifact', receipt.artifactId);
- assert.deepEqual(row.report.chapters, task.report.chapters);
- assert.equal(row.meta.workshop.formalConfirmation, false);
- }
- assert.equal((await f.store.list('KfArtifact')).length, CORE_DELIVERABLES.length);
- });
- test('全部相关资料去向、正反证统计与代表原话随HTML及正文保存、回读和公开展示,分子不由引文卡数代替', async t => {
- const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ksf-statistics-parity-'));
- t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
- const f = fixture(), { sessionId } = await f.start(0), task = deliveryTask(sessionId, 'social');
- const corpus = [...Array(10)].map((_, i) => ({ id: 'test-' + i, disposition: 'included', stance: i < 4 ? 'support' : i < 6 ? 'oppose' : 'unknown', text: '合成原话' + i }));
- corpus.push({ id: 'test-10', disposition: 'excluded-irrelevant' }, { id: 'test-11', disposition: 'excluded-duplicate' }, { id: 'test-12', disposition: 'pending-body' });
- const source = await f.store.create('KfSource', { title: '隔离合成测试资料', rawText: JSON.stringify(corpus), dataMode: 'sample', meta: { ownerId: actor.id } });
- task.sourceIds = [source.objectId];task.report.evidence = { e: { sourceId: source.objectId, claim: '合成测试,原话不等于全量分子', quotes: [{ text: '合成原话0', ref: 'https://example.org/test' }] } };
- const included = corpus.filter(r => r.disposition === 'included');
- const summary = { id: 'statistics-summary', kind: 'table', data: { cols: [...STATISTICS_COLUMNS], rows: ['support', 'oppose', 'unknown'].map(stance => [stance,
- included.filter(r => r.stance === stance).length, included.length, '合成测试|13条候选:10纳入、2排除、1待核;非真实消费者资料', '全体已判定记录统计;相反材料也保留', '仅测试数据流,不形成实际研发建议', 'e']) } };
- task.report.chapters[0].sections.push({ kind: 'table', title: '全部相关资料处理去向', data: { cols: ['记录', '去向', '判定'], rows: corpus.map(r => [r.id, r.disposition, r.stance || '未判定']) } }, summary);
- task.rawText = JSON.stringify({ corpus, summary, explanation: '隔离测试;精选一条引文不代表只分析了一条资料' });
- const { task: prepared, htmlPath } = prepareHtmlReport(task, { directory, dataMode: 'sample' });
- const saved = await saveAndVerifyReport(prepared, verifier(f, { requestId: 'statistics-saved' }));
- const db = buildModel(await f.publicData()), actual = db.artifactById[saved.artifactId];
- assert.deepEqual(actual.report.chapters, task.report.chapters);
- assert.deepEqual(statisticsRows(actual.report.chapters[0].sections.at(-1)).map(row => row.percent), ['40.0%', '20.0%', '40.0%']);
- const file = await f.files.readFile({ query: Object.fromEntries(new URL(saved.htmlFiles[0].url).searchParams) }, null);
- assert.ok(file.bytes.equals(fs.readFileSync(htmlPath)));assert.match(file.bytes.toString(), /2排除、1待核/);
- assert.equal(actual.report.evidence.e.quotes.length, 1);assert.equal(saved.filesVerified, 1);
- assert.equal(statisticsRows(summary)[0].numerator, 4);
- });
- test('课程三维与未知项以现有报告表保存,公开数据及HTML字节回读一致,无新增数据库字段', async t => {
- const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ksf-course-parity-'));
- t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
- const f = fixture(), { sessionId } = await f.start(0), task = deliveryTask(sessionId, 'validation');
- task.taskKey = 'parity-topic:D2-05:priority-list';
- task.report.chapters[0].sections.push({ kind: 'table', id: 'course-priority', data: {
- cols: [...COURSE_ASSESSMENT_COLUMNS], rows: [['测试A', 2, 3, 2, '隔离测试依据,商业价值需本组复核', '第1'],
- ['测试B', 2, null, 2, '测试工艺未知,另表列出补证方式', '先补证']],
- } });
- const prepared = prepareHtmlReport(task, { directory, dataMode: 'sample' });
- const saved = await saveAndVerifyReport(prepared.task, verifier(f, { requestId: 'course-score-saved' }));
- const actual = buildModel(await f.publicData()).artifactById[saved.artifactId];
- assert.deepEqual(courseAssessmentRows(actual.report.chapters[0].sections.at(-1)).map(row => row.scores), [[2, 3, 2], [2, null, 2]]);
- const file = await f.files.readFile({ query: Object.fromEntries(new URL(saved.htmlFiles[0].url).searchParams) }, null);
- assert.ok(file.bytes.equals(fs.readFileSync(prepared.htmlPath)));assert.match(file.bytes.toString(), /data-course-assessment/);
- assert.equal(saved.publicDataVerified, true);assert.equal(saved.employeeConfirmation, 'pending');
- });
- test('HTML与图片随报告原子保存、文件字节回读、公开预览关联;直接匿名取私有文件被拒', async () => {
- const f = fixture(), { sessionId } = await f.start(0), task = deliveryTask(sessionId, 'proposal');
- task.report.attachments = [inlineFile('最终提案.html', 'text/html', Buffer.from('<!doctype html><meta charset="utf-8"><h1>隔离测试提案</h1>')),
- inlineFile('示意图.png', 'image/png', Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a5YkAAAAASUVORK5CYII=', 'base64'))];
- const result = await saveAndVerifyReport(task, verifier(f));assert.equal(result.filesVerified, 2);
- const replay = await saveAndVerifyReport(task, verifier(f));assert.equal(replay.artifactId, result.artifactId);
- assert.equal((await f.store.list('KsfFileObject')).length, 2, '同请求重放不产生重复文件');
- const row = await f.store.get('KfArtifact', result.artifactId);
- assert.equal(JSON.stringify(row.report).includes('dataBase64'), false);
- const files = row.report.attachments;assert.equal(files.length, 2);
- assert.equal(reportAttachments({ id: row.objectId, report: row.report }).length, 2);
- await assert.rejects(f.files.readFile({ query: { fileId: files[0].id } }, null), { code: 'AUTH_REQUIRED' });
- const publicFile = await f.files.readFile({ query: { artifactId: row.objectId, fileId: files[0].id } }, null);
- assert.equal(publicFile.name, '最终提案.html');assert.match(publicFile.bytes.toString(), /隔离测试提案/);
- await assert.rejects(f.files.readFile({ query: { artifactId: 'another-report', fileId: files[0].id } }, null), { code: 'REFERENCE_NOT_FOUND' });
- await f.store.update('KfArtifact', row.objectId, { recordState: 'archived' });
- await assert.rejects(f.files.readFile({ query: { artifactId: row.objectId, fileId: files[0].id } }, null), { code: 'REFERENCE_NOT_FOUND' });
- });
- test('附件内容损坏、伪造回执、跨身份取文件、空交付物均拒绝,失败不遗留报告或文件', async () => {
- const f = fixture(), { sessionId } = await f.start(0);
- const bad = deliveryTask(sessionId);bad.report.chapters[0].sections = [{ kind: 'table', data: { cols: ['标题'], rows: [[' ']] } }];
- await assert.rejects(f.call('ksfSaveWorkshopTask', { ...bad, requestId: 'empty-report' }), { code: 'VALIDATION_FAILED' });
- const attachment = inlineFile('bad.png', 'image/png', Buffer.from('not an image'));
- bad.report = deliveryTask(sessionId).report;bad.report.attachments = [attachment];
- await assert.rejects(f.call('ksfSaveWorkshopTask', { ...bad, requestId: 'bad-image' }), { code: 'VALIDATION_FAILED' });
- bad.report.attachments = [inlineFile('ok.txt', 'text/plain', Buffer.from('保存后也须回滚')), { ...inlineFile('bad.txt', 'text/plain', Buffer.from('正文')), sha256: 'a'.repeat(64) }];
- await assert.rejects(f.call('ksfSaveWorkshopTask', { ...bad, requestId: 'bad-hash' }), { code: 'UPLOAD_INCOMPLETE' });
- bad.report.attachments = [{ name: 'fake.txt', contentType: 'text/plain', sizeBytes: 1, sha256: 'a'.repeat(64), receipt: { fileId: 'fake' } }];
- await assert.rejects(f.call('ksfSaveWorkshopTask', { ...bad, requestId: 'fake-file' }), { code: 'UPLOAD_INCOMPLETE' });
- assert.equal((await f.store.list('KfArtifact')).length, 0);assert.equal((await f.store.list('KsfFileObject')).length, 0);
- assert.equal((await f.store.list('KfSource')).length, 0);
- const good = deliveryTask(sessionId);good.report.attachments = [inlineFile('private.txt', 'text/plain', Buffer.from('私有原件'))];
- const saved = await saveAndVerifyReport(good, verifier(f));
- const [file] = await f.store.list('KsfFileObject');
- await assert.rejects(f.files.readFile({ query: { fileId: file.objectId } }, { id: 'other', kind: 'service', scopes: ['data:read'] }), { code: 'FORBIDDEN' });
- await f.store.update('KfArtifact', saved.artifactId, { meta: {}, dashboardState: 'draft' });
- await assert.rejects(f.files.readFile({ query: { artifactId: saved.artifactId, fileId: file.objectId } }, null), { code: 'REFERENCE_NOT_FOUND' });
- });
- test('仅有boardVisible回执不算验证完成;回读不符保留编号且不重试保存,旧服务在写前停止', async () => {
- const f = fixture(), { sessionId } = await f.start(0), task = deliveryTask(sessionId);
- let saves = 0;
- const base = verifier(f);
- await assert.rejects(saveAndVerifyReport(task, { ...base, invoke: async (command, input, id) => {
- if (command === 'workshop-save') saves++;
- return base.invoke(command, input, id);
- }, publicRead: async () => ({ artifact: [], nextCursor: null }) }), error => {
- assert.equal(error.code, 'REPORT_PUBLIC_READBACK_MISMATCH');assert.ok(error.partial.receipt.artifactId);assert.equal(error.partial.automaticRetry, false);return true;
- });
- assert.equal(saves, 1);assert.equal((await f.store.list('KfArtifact')).length, 1);
- await assert.rejects(saveAndVerifyReport(task, { ...base, invoke: async command => {
- assert.equal(command, 'workshop-contract');return { workshop: { version: 'ksf.workshop.v1' } };
- } }), { code: 'REPORT_DELIVERY_CAPABILITY_UNAVAILABLE' });
- });
- test('个人交付核对按课题、资料模式、最新版本隔离;草稿不计已交,缺正文、旧成果待定位均明示', async () => {
- const f = fixture(), first = await f.start(0), other = await f.start(0, 'another-topic');
- await saveAndVerifyReport(deliveryTask(first.sessionId), verifier(f));
- const draft = deliveryTask(first.sessionId);draft.version = 2;draft.report.delivery.contentStatus = 'draft';
- await saveAndVerifyReport(draft, verifier(f, { requestId: 'save-draft' }));
- await saveAndVerifyReport(deliveryTask(other.sessionId, 'proposal', { taskKey: 'another-topic:D2-08:proposal' }), verifier(f, { requestId: 'other-topic' }));
- const raw = await f.publicData();
- const db = buildModel(raw), view = workshopReview(db, seedMemberGroups(db.members), { mode: 'all', history: '1', reportType: 'value' });
- const topics = view.hierarchy.flatMap(g => g.members.flatMap(m => m.topics));
- const a = topics.find(t => t.workshopKey === 'parity-topic'), b = topics.find(t => t.workshopKey === 'another-topic');
- assert.equal(a.outputs.find(o => o.key === 'market').state, 'draft');
- assert.equal(a.outputs.find(o => o.key === 'proposal').state, 'missing');
- assert.equal(b.outputs.find(o => o.key === 'proposal').state, 'saved');
- assert.equal(b.outputs.find(o => o.key === 'market').state, 'missing');
- const legacy = structuredClone(raw.artifact[0]);legacy.objectId = 'legacy-empty';legacy.reportSeriesKey = 'legacy';legacy.version = 1;
- legacy.report = { chapters: [{ id: 'evidence-pool', sections: [] }] };legacy.rawText = '';
- raw.artifact.push(legacy);
- const db2 = buildModel(raw), v2 = workshopReview(db2, seedMemberGroups(db2.members), { mode: 'sample' });
- assert.equal(v2.hierarchy.flatMap(g => g.members.flatMap(m => m.topics)).find(t => t.workshopKey === 'parity-topic').outputs.find(o => o.key === 'evidence').state, 'missing-body');
- });
- test('报告中心仅计成果报告;旧原文可读、无正文不伪造,独立机会及资料分开统计', () => {
- const db = buildModel({ artifact: [{ objectId: 'text', title: '旧报告', artifactType: 'voc_report', rawText: '已保存的真实原文。' },
- { objectId: 'empty', title: '缺正文', artifactType: 'concept' }], opportunity: [{ objectId: 'opp', title: '独立机会' }],
- source: [{ objectId: 'category-source', sourceType: 'category' }] });
- const center = reportCenter(db);assert.equal(center.total, 2);assert.equal(center.separateObjects.opportunities, 1);
- assert.ok(center.entries.every(e => e.kind === 'artifact'));
- assert.equal(center.entries.find(e => e.id === 'text').href, '/report/text');
- const read = reportOverview(db, 'text');assert.equal(read.bodyStatus, 'text');assert.match(read.report.subtitle, /尚非结构化/);
- assert.equal(read.chapters[0].sections[0].text, '已保存的真实原文。');assert.equal(db.artifactById.text.report, null);
- assert.equal(reportOverview(db, 'empty'), null);
- });
- test('已有上传回执可关联报告;跨身份回执及超限附件在写入前拒绝', async () => {
- const f = fixture(), { sessionId } = await f.start(0), bytes = Buffer.from('已通过文件通道的正文。');
- const input = inlineFile('来源文件.txt', 'text/plain', bytes);
- const prepared = await f.call('ksfPrepareFileUpload', { requestId: 'prepare-report-file', fileName: input.name,
- contentType: input.contentType, sizeBytes: input.sizeBytes, sha256: input.sha256 }, { ...actor, scopes: [...actor.scopes,'source:write'] });
- const receipt = await f.files.upload({ query: Object.fromEntries(new URL(prepared.upload.url).searchParams),
- headers: { 'x-ksf-upload-token': prepared.upload.headers['X-Ksf-Upload-Token'] }, body: bytes });
- const attachment = { ...input, receipt };delete attachment.dataBase64;
- await assert.rejects(f.store.transaction(s => f.files.saveReportAttachments(s, [attachment], { ...actor, id: 'other-service' })), { code: 'UPLOAD_INCOMPLETE' });
- const task = deliveryTask(sessionId);task.report.attachments = [attachment];
- const saved = await saveAndVerifyReport(task, verifier(f));assert.equal(saved.filesVerified, 1);
- const large = { ...attachment, sizeBytes: 1024 * 1024 + 1 };
- await assert.rejects(f.store.transaction(s => f.files.saveReportAttachments(s, [large], actor)), { code: 'VALIDATION_FAILED' });
- });
|