Archived
真跑 e2e 逼出来的地基缺陷。活动清单存在 sys.activity_package.manifest 这个 **jsonb 列**里,而 PG 按「先短后长、同长字典序」重排所有嵌套对象的键。 auto-runner 原先逐字段**按声明顺序**求值、后面的字段靠 ctx.computed 看前面的值 —— 顺序一乱,依赖就断,而且**一声不响**。 W05 实证:judgement_class(15B)被排到它依赖的 difference_status(17B)前面, 求值时后者恒 undefined、when 三级全落空,摔进 constraint_type 兜底档 —— 7 行差异结论全判错,与实际状态无关;而 difference_status 走 lookup:'result:items' 不依赖字段序,所以它是对的。「一个对一个全错」由此而来。 隔离测试复现不出来,因为它直接读 dist/manifest.json,从不经 DB 往返。 修法:字段求值改为**依赖拓扑排序**(Kahn),不用迭代到不动点 —— 后者的数值收敛判据会漏掉「A/B 互相 when 为假、恰好在错误状态下稳定」这种 结构有环却数值收敛的配方;拓扑是结构性判定,不会假阴性。 通用图算法 topoOrder 提进 @utils(契约新增,非破坏); AutoExpr→依赖名提取因 L4 不能 import L5,在 loop-engine 与 activity-registry 各留一份并互相指路。真正的环由装包静态审当场拒装 —— 不兜底,兜底正是这次出事的形态。 变异验证:git stash 只撤 auto-runner.ts 那一处,两条新测试当场变红, 数值与专查报告的 SQL 转录逐字吻合(使能阈值/match 被误判 hard_mismatch/medium); stash pop 后 37/37 绿。E04 92/92、I01 55/55 全绿;全仓 2447 测通过。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
89 lines
3.5 KiB
TypeScript
89 lines
3.5 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { assertNever, groupBy, isDefined, sortedEntries, topoOrder } from '../src/index.ts';
|
|
|
|
describe('@utils', () => {
|
|
it('isDefined 滤掉 null 与 undefined', () => {
|
|
expect([1, null, 2, undefined].filter(isDefined)).toEqual([1, 2]);
|
|
});
|
|
|
|
it('groupBy 按键分组', () => {
|
|
const rows = [
|
|
{ pn: 'A', type: 'A' as const },
|
|
{ pn: 'B', type: 'B' as const },
|
|
{ pn: 'C', type: 'A' as const },
|
|
];
|
|
expect(groupBy(rows, (r) => r.type)).toEqual({
|
|
A: [rows[0], rows[2]],
|
|
B: [rows[1]],
|
|
});
|
|
});
|
|
|
|
it('sortedEntries 稳定排序', () => {
|
|
expect(sortedEntries({ b: 2, a: 1 })).toEqual([
|
|
['a', 1],
|
|
['b', 2],
|
|
]);
|
|
});
|
|
|
|
it('assertNever 抛错', () => {
|
|
expect(() => assertNever('x' as never)).toThrow('未覆盖的分支');
|
|
});
|
|
});
|
|
|
|
describe('topoOrder', () => {
|
|
it('无依赖:随便什么序都放行,谁都不欠谁', () => {
|
|
const result = topoOrder(['b', 'a', 'c'], () => []);
|
|
expect(result.ok).toBe(true);
|
|
expect(result.ok && [...result.order].sort()).toEqual(['a', 'b', 'c']);
|
|
});
|
|
|
|
/* 入参故意按「被依赖的排在后面」给 —— 证明排序看的是依赖关系,不是输入数组的次序 */
|
|
it('链式依赖:按「被依赖的排前面」重排,不管输入次序', () => {
|
|
const deps: Record<string, string[]> = { c: ['b'], b: ['a'], a: [] };
|
|
const result = topoOrder(['c', 'b', 'a'], (n) => deps[n] ?? []);
|
|
expect(result).toEqual({ ok: true, order: ['a', 'b', 'c'] });
|
|
});
|
|
|
|
it('菱形依赖:d 依赖 b 和 c,b 与 c 都依赖 a —— a 最先,d 最后,b/c 顺序不拘', () => {
|
|
const deps: Record<string, string[]> = { a: [], b: ['a'], c: ['a'], d: ['b', 'c'] };
|
|
const result = topoOrder(['a', 'b', 'c', 'd'], (n) => deps[n] ?? []);
|
|
expect(result.ok).toBe(true);
|
|
const order = result.ok ? result.order : [];
|
|
expect(order.indexOf('a')).toBeLessThan(order.indexOf('b'));
|
|
expect(order.indexOf('a')).toBeLessThan(order.indexOf('c'));
|
|
expect(order.indexOf('b')).toBeLessThan(order.indexOf('d'));
|
|
expect(order.indexOf('c')).toBeLessThan(order.indexOf('d'));
|
|
});
|
|
|
|
it('真正的环(a 依赖 b、b 依赖 a):当场报出来,不默默兜底', () => {
|
|
const deps: Record<string, string[]> = { a: ['b'], b: ['a'] };
|
|
const result = topoOrder(['a', 'b'], (n) => deps[n] ?? []);
|
|
expect(result.ok).toBe(false);
|
|
expect(!result.ok && [...result.cycle].sort()).toEqual(['a', 'b']);
|
|
});
|
|
|
|
it('自依赖(a 依赖自己)同样算环', () => {
|
|
const result = topoOrder(['a'], () => ['a']);
|
|
expect(result).toEqual({ ok: false, cycle: ['a'] });
|
|
});
|
|
|
|
it('三元环:a 依赖 c、c 依赖 b、b 依赖 a,三个一起报出来', () => {
|
|
const deps: Record<string, string[]> = { a: ['c'], b: ['a'], c: ['b'] };
|
|
const result = topoOrder(['a', 'b', 'c'], (n) => deps[n] ?? []);
|
|
expect(result.ok).toBe(false);
|
|
expect(!result.ok && [...result.cycle].sort()).toEqual(['a', 'b', 'c']);
|
|
});
|
|
|
|
it('环之外互不相干的节点不受连累 —— 只有环上那两个排不出来', () => {
|
|
const deps: Record<string, string[]> = { y: [], a: ['b'], b: ['a'] };
|
|
const result = topoOrder(['y', 'a', 'b'], (n) => deps[n] ?? []);
|
|
expect(result.ok).toBe(false);
|
|
expect(!result.ok && [...result.cycle].sort()).toEqual(['a', 'b']);
|
|
});
|
|
|
|
it('依赖了一个不在 nodes 里的名字:当没有这条依赖,不误报成环', () => {
|
|
const result = topoOrder(['a'], () => ['not-a-node']);
|
|
expect(result).toEqual({ ok: true, order: ['a'] });
|
|
});
|
|
});
|