import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import crypto from 'node:crypto'; import { createApplication } from '../src/core.mjs'; import { attachBoardActionHandlers } from '../src/board-actions.mjs'; const data = JSON.parse(fs.readFileSync(new URL('../references/runtime-data.json', import.meta.url))); function fixture() { let rows = {}, seq = 0; const clone = (x) => structuredClone(x); const store = { async transaction(fn) { const before = clone(rows); try { return await fn(store); } catch (error) { rows = before; throw error; } }, async lock() {}, async get(name, id) { return clone(rows[name]?.find((row) => row.objectId === id) || null); }, async list(name, { where = {} } = {}) { return clone((rows[name] || []).filter((row) => Object.entries(where).every(([key, value]) => row[key] === value))); }, async create(name, value) { const row = { ...clone(value), objectId: 'test' + ++seq, updatedAt: new Date(1800000000000 + seq).toISOString() }; (rows[name] ||= []).push(row); return clone(row); }, async update(name, id, changes) { const row = rows[name].find((row) => row.objectId === id); Object.assign(row, clone(changes), { updatedAt: new Date(1800000000000 + ++seq).toISOString() }); return clone(row); }, }; const app = createApplication({ store, crypto, Parse: {}, data, settings: { signingSecret: 'test-only' }, files: {} }); attachBoardActionHandlers(app, { store }); return { store, app }; } async function card(f, extra = {}) { return f.store.create('KfOpportunity', { title: 'test', status: 'candidate', externalKey: 'workshop:opportunity:test', schemaVersion: 'ksf.v0.7', linkedProject: 'kept-project', linkedArtifacts: ['kept-report'], ...extra }); } const params = (row, extra = {}) => ({ requestId: 'request-' + crypto.randomUUID(), opportunityId: row.objectId, expectedUpdatedAt: row.updatedAt, by: '自报姓名', ...extra }); test('匿名三状态持久化,保留互链与旧决策,不产生项目或企业批准', async () => { const f = fixture(); let row = await card(f); for (const status of ['approved', 'rejected', 'candidate']) { const result = (await f.app.dispatch('ksfSetPublicOpportunityStatus', params(row, { status }), null)).data; assert.equal(result.opportunity.status, status); assert.equal(result.identityVerified, false); assert.equal(result.projectCreated, false); assert.equal(result.enterpriseApproval, false); assert.equal(result.opportunity.decision.via, 'public_board'); assert.equal(result.opportunity.decision.identityVerified, false); row = await f.store.get('KfOpportunity', row.objectId); assert.equal(row.status, status); assert.equal(row.linkedProject, 'kept-project'); assert.deepEqual(row.linkedArtifacts, ['kept-report']); } assert.equal(row.decisionLog.length, 3); assert.equal((await f.store.list('KfProject')).length, 0); assert.equal((await f.store.list('KsfOperation')).length, 3); }); test('重复请求复用回执,改内容复用请求键、过期版本均不写入', async () => { const f = fixture(), row = await card(f), p = params(row, { status: 'approved' }); const first = await f.app.dispatch('ksfSetPublicOpportunityStatus', p, null); assert.deepEqual((await f.app.dispatch('ksfSetPublicOpportunityStatus', p, null)).data, first.data); await assert.rejects(() => f.app.dispatch('ksfSetPublicOpportunityStatus', { ...p, status: 'rejected' }, null), { code: 'IDEMPOTENCY_CONFLICT' }); await assert.rejects(() => f.app.dispatch('ksfSetPublicOpportunityStatus', params(row, { status: 'rejected' }), null), { code: 'VERSION_CONFLICT' }); assert.equal((await f.store.list('KsfOperation')).length, 1); assert.equal((await f.store.get('KfOpportunity', row.objectId)).decisionLog.length, 1); }); test('私有/归档/未知记录、越权字段与坏状态被拒绝,回执不泄露私有字段', async () => { const f = fixture(); for (const extra of [{ externalKey: 'private:test' }, { recordState: 'archived' }]) { const row = await card(f, extra); await assert.rejects(() => f.app.dispatch('ksfSetPublicOpportunityStatus', params(row, { status: 'approved' }), null), { code: 'REFERENCE_NOT_FOUND' }); } const row = await card(f, { privateInfo: 'must-not-leak' }); for (const extra of [{ status: 'completed' }, { status: 'approved', linkedProject: 'hack' }, { status: 'approved', expectedUpdatedAt: null }]) { await assert.rejects(() => f.app.dispatch('ksfSetPublicOpportunityStatus', params(row, extra), null), { code: 'VALIDATION_FAILED' }); } const result = await f.app.dispatch('ksfSetPublicOpportunityStatus', params(row, { status: 'approved', by: '' }), null); assert.equal(result.data.opportunity.privateInfo, undefined); assert.equal(result.data.opportunity.decision.by, '匿名访客'); }); test('匿名评分校验与同署名覆盖,保留其他评分', async () => { const f = fixture(); let row = await card(f, { scores: [{ user: '其他人', score: 60 }] }); for (const score of [70, 80]) { await f.app.dispatch('ksfRatePublicOpportunity', params(row, { score, comment: 'test' }), null); row = await f.store.get('KfOpportunity', row.objectId); } assert.equal(row.scores.length, 2); assert.equal(row.scores.find((s) => s.user === '自报姓名').score, 80); for (const extra of [{ score: 101 }, { score: '80' }, { score: 50, by: '' }]) await assert.rejects(() => f.app.dispatch('ksfRatePublicOpportunity', params(row, extra), null), { code: 'VALIDATION_FAILED' }); });