feat(w05): 五张报告模版 —— 两张数据工单件、评估输入清单、差异审核件、落账确认件(目标件平移)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
This commit is contained in:
xcs
2026-08-26 23:45:17 +08:00
co-authored by Claude Fable 5
parent f7fe8e3836
commit f9dbdc8c9c
2 changed files with 776 additions and 8 deletions
@@ -2,12 +2,11 @@
* ACT-WORK-05 差异评估的包内自测 —— **只准 import @sdk / @contracts / @sfeel**。
* 案例活动没有 `run`,自测不跑回合;测的是清单本身与授权面对齐,结构照 E04 / I01。
*
* 本文件目前只写三组(清单本身合规 / 台账声明 / 活动入口);
* 「案例模型与清单的对齐」「报告模版:编辑面与只读面各就各位」两组留给 Task 4/5 追加 ——
* `src/case-model.ts` 与 `src/views/report.ts` 眼下还是占位,写了那两组只会红。
* 五组:清单本身合规 / 台账声明 / 案例模型与清单的对齐 / 报告模版:编辑面与只读面各就各位 / 活动入口。
*/
import { describe, expect, it } from 'vitest';
import { activityManifestSchema } from '@contracts';
import { createMockHost } from '@sdk';
import { manifest, D, RULE_VERSION } from '../src/manifest.ts';
import activity from '../src/index.ts';
@@ -15,6 +14,7 @@ const ITEMS = ['S0_bind', 'S0d_master_data', 'S1_input_confirm', 'S2_normalize',
const NO_DONE_WHEN = new Set(['S0d_master_data', 'S1_input_confirm', 'S4_review', 'T_release']);
const VIEWS = ['p08-master-create', 'p10-document-create', 'eval-input-list', 'difference-review', 'landing-confirm'];
const byAddress = (domain: string, object: string) => manifest.declarations.ledger.find((l) => l.domain === domain && l.object === object);
const viewHtml = (name: string) => manifest.assets.viewTemplates.find((t) => t.name === name)?.html ?? '';
describe('清单本身合规', () => {
it('过 @contracts 的 schema', () => {
@@ -177,8 +177,69 @@ describe('案例模型与清单的对齐', () => {
});
});
describe('报告模版:编辑面与只读面各就各位', () => {
it('两张数据工单件可加行可编辑:data-edit / data-edit-add 与主键 data-col 都在', () => {
const p08 = viewHtml('p08-master-create');
expect(p08).toContain('data-edit="parts.profile"');
expect(p08).toContain('data-edit-add="parts.profile"');
expect(p08).toContain('data-col="internal_pn"');
expect(p08).toContain('data-col="normalized_parameter_set"');
const p10 = viewHtml('p10-document-create');
expect(p10).toContain('data-edit="parts.document"');
expect(p10).toContain('data-col="doc_seq"');
});
it('评估输入清单:缺口表可编辑(supplement),绑定区只读', () => {
const html = viewHtml('eval-input-list');
expect(html).toContain(`data-edit="${D}.input_gap"`);
expect(html).toContain('data-col="supplement"');
expect(html).toContain(`data-rows="mine:${D}.pn_check"`);
});
it('差异审核与落账确认只读:没有任何 data-edit / data-col', () => {
for (const name of ['difference-review', 'landing-confirm']) {
expect(viewHtml(name), name).not.toContain('data-edit=');
expect(viewHtml(name), name).not.toContain('data-col=');
}
});
it('每个可编辑格子的 data-col 都在对应台账对象的声明里', () => {
const cases: Array<[string, string, string]> = [
['p08-master-create', 'parts', 'profile'],
['p10-document-create', 'parts', 'document'],
['eval-input-list', D, 'input_gap'],
];
for (const [view, domain, object] of cases) {
const cols = new Set([...viewHtml(view).matchAll(/data-col="([^"]+)"/g)].map((m) => m[1] ?? ''));
const declared = new Set(byAddress(domain, object)?.fields.map((f) => f.name));
for (const c of cols) expect(declared.has(c), `${view}: ${c}`).toBe(true);
}
});
it('枚举列只用 data-class-of 投影,不摆 ASCII 原值', () => {
expect(viewHtml('difference-review')).toContain('data-class-of="difference_status"');
expect(viewHtml('difference-review')).toContain('data-class-of="recommendation_level"');
expect(viewHtml('difference-review')).not.toContain('data-slot="difference_status"');
});
it('宿主拒推含脚本的模版', async () => {
const host = createMockHost({ contract: CONTRACT });
await expect(host.base.interaction.pushHtml('<div><script>x()</script></div>')).rejects.toThrow(/无脚本执行权/);
});
});
describe('活动入口', () => {
it('没有 run —— 行为全在清单里', () => {
expect(activity.run).toBeUndefined();
});
});
const CONTRACT = {
activityId: 'part-substitution-assessment',
version: '1.0.0',
data: {},
message: undefined,
outputPorts: { mandatory: ['assessment-released'], selective: [] },
prohibitions: [],
discretion: [],
} as const;