@dommaker/harness 1.8.1 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +1 -1
- package/dist/cli/commands/sync-docs/capabilities-syncer.d.ts +22 -0
- package/dist/cli/commands/sync-docs/capabilities-syncer.d.ts.map +1 -1
- package/dist/cli/commands/sync-docs/capabilities-syncer.js +113 -8
- package/dist/cli/commands/sync-docs/capabilities-syncer.js.map +1 -1
- package/dist/cli/commands/sync-docs/index.d.ts.map +1 -1
- package/dist/cli/commands/sync-docs/index.js +36 -4
- package/dist/cli/commands/sync-docs/index.js.map +1 -1
- package/dist/hooks/bootstrap.d.ts +9 -12
- package/dist/hooks/bootstrap.d.ts.map +1 -1
- package/dist/hooks/bootstrap.js +9 -31
- package/dist/hooks/bootstrap.js.map +1 -1
- package/dist/hooks/index.d.ts +5 -7
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +4 -10
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/CONTEXT.md +1 -1
- package/src/__tests__/public-exports.test.ts +30 -4
- package/src/__tests__/public-type-surface.test.ts +0 -8
- package/src/cli/commands/CONTEXT.md +3 -0
- package/src/cli/commands/__tests__/registry.test.ts +39 -3
- package/src/cli/commands/__tests__/sync-docs-table-layout.test.ts +259 -0
- package/src/cli/commands/sync-docs/capabilities-syncer.ts +139 -9
- package/src/cli/commands/sync-docs/index.ts +41 -4
- package/src/hooks/CONTEXT.md +12 -22
- package/src/hooks/__tests__/bootstrap.test.ts +21 -70
- package/src/hooks/bootstrap.ts +9 -48
- package/src/hooks/index.ts +5 -20
- package/src/index.ts +1 -13
- package/dist/hooks/config.d.ts +0 -30
- package/dist/hooks/config.d.ts.map +0 -1
- package/dist/hooks/config.js +0 -34
- package/dist/hooks/config.js.map +0 -1
- package/dist/hooks/pipeline.d.ts +0 -54
- package/dist/hooks/pipeline.d.ts.map +0 -1
- package/dist/hooks/pipeline.js +0 -173
- package/dist/hooks/pipeline.js.map +0 -1
- package/dist/hooks/registry.d.ts +0 -70
- package/dist/hooks/registry.d.ts.map +0 -1
- package/dist/hooks/registry.js +0 -141
- package/dist/hooks/registry.js.map +0 -1
- package/dist/hooks/types.d.ts +0 -113
- package/dist/hooks/types.d.ts.map +0 -1
- package/dist/hooks/types.js +0 -9
- package/dist/hooks/types.js.map +0 -1
- package/src/__tests__/hooks-pipeline.test.ts +0 -201
- package/src/hooks/__tests__/config.test.ts +0 -19
- package/src/hooks/__tests__/pipeline.test.ts +0 -195
- package/src/hooks/__tests__/registry.test.ts +0 -115
- package/src/hooks/config.ts +0 -33
- package/src/hooks/pipeline.ts +0 -202
- package/src/hooks/registry.ts +0 -166
- package/src/hooks/types.ts +0 -120
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HookRegistry + HookPipeline 测试(Phase 1)
|
|
3
|
-
*
|
|
4
|
-
* #159 起 `enabled` / `errorStrategy` 由 HookConfig 唯一声明,
|
|
5
|
-
* 注册时以配置填充有效值;HookDefinition 不再携带这两个字段。
|
|
6
|
-
*/
|
|
7
|
-
import { describe, it, expect, beforeEach } from '@jest/globals';
|
|
8
|
-
import { HookRegistry } from '../hooks/registry';
|
|
9
|
-
import { HookPipeline } from '../hooks/pipeline';
|
|
10
|
-
import type { HookConfig, HookDefinition } from '../hooks/types';
|
|
11
|
-
|
|
12
|
-
interface TestContext {
|
|
13
|
-
value: string;
|
|
14
|
-
calls: string[];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function makeHook(
|
|
18
|
-
name: string,
|
|
19
|
-
opts: Partial<HookDefinition<TestContext, unknown>> = {}
|
|
20
|
-
): HookDefinition<TestContext, unknown> {
|
|
21
|
-
return {
|
|
22
|
-
name,
|
|
23
|
-
phase: 'before',
|
|
24
|
-
priority: 100,
|
|
25
|
-
execute: async (ctx) => {
|
|
26
|
-
ctx.calls.push(name);
|
|
27
|
-
return { passed: true };
|
|
28
|
-
},
|
|
29
|
-
...opts,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function makeConfig(name: string, overrides: Partial<HookConfig> = {}): HookConfig {
|
|
34
|
-
return { name, enabled: true, errorStrategy: 'block', ...overrides };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
describe('HookRegistry', () => {
|
|
38
|
-
let registry: HookRegistry<TestContext, unknown>;
|
|
39
|
-
|
|
40
|
-
beforeEach(() => {
|
|
41
|
-
registry = new HookRegistry();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('应注册 hook', () => {
|
|
45
|
-
registry.register(makeHook('test'), makeConfig('test'));
|
|
46
|
-
expect(registry.get('test')).toBeDefined();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('同名注册应覆盖', () => {
|
|
50
|
-
registry.register(makeHook('test', { priority: 50 }), makeConfig('test'));
|
|
51
|
-
registry.register(makeHook('test', { priority: 90 }), makeConfig('test'));
|
|
52
|
-
expect(registry.get('test')?.priority).toBe(90);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('应获取已启用的 hook 按优先级排序', () => {
|
|
56
|
-
registry.register(makeHook('low', { priority: 200 }), makeConfig('low'));
|
|
57
|
-
registry.register(makeHook('high', { priority: 10 }), makeConfig('high'));
|
|
58
|
-
registry.register(makeHook('mid', { priority: 100 }), makeConfig('mid'));
|
|
59
|
-
registry.register(makeHook('disabled'), makeConfig('disabled', { enabled: false }));
|
|
60
|
-
|
|
61
|
-
const enabled = registry.getEnabled('before');
|
|
62
|
-
expect(enabled.map(h => h.name)).toEqual(['high', 'mid', 'low']);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('应注销 hook', () => {
|
|
66
|
-
registry.register(makeHook('test'), makeConfig('test'));
|
|
67
|
-
expect(registry.unregister('test')).toBe(true);
|
|
68
|
-
expect(registry.get('test')).toBeUndefined();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('应设置启用/禁用', () => {
|
|
72
|
-
registry.register(makeHook('test'), makeConfig('test'));
|
|
73
|
-
expect(registry.setEnabled('test', false)).toBe(true);
|
|
74
|
-
expect(registry.getEnabled('before').length).toBe(0);
|
|
75
|
-
expect(registry.setEnabled('test', true)).toBe(true);
|
|
76
|
-
expect(registry.getEnabled('before').length).toBe(1);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('应正确统计数量', () => {
|
|
80
|
-
expect(registry.size).toBe(0);
|
|
81
|
-
registry.register(makeHook('a'), makeConfig('a'));
|
|
82
|
-
registry.register(makeHook('b'), makeConfig('b'));
|
|
83
|
-
expect(registry.size).toBe(2);
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
describe('HookPipeline', () => {
|
|
88
|
-
let registry: HookRegistry<TestContext, unknown>;
|
|
89
|
-
let pipeline: HookPipeline<TestContext>;
|
|
90
|
-
|
|
91
|
-
beforeEach(() => {
|
|
92
|
-
registry = new HookRegistry();
|
|
93
|
-
pipeline = new HookPipeline(registry);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('应执行所有 before hook', async () => {
|
|
97
|
-
const ctx: TestContext = { value: '', calls: [] };
|
|
98
|
-
registry.register(makeHook('h1'), makeConfig('h1'));
|
|
99
|
-
registry.register(makeHook('h2'), makeConfig('h2'));
|
|
100
|
-
|
|
101
|
-
const result = await pipeline.run('before', ctx);
|
|
102
|
-
|
|
103
|
-
expect(result.passed).toBe(true);
|
|
104
|
-
expect(ctx.calls).toEqual(['h1', 'h2']);
|
|
105
|
-
expect(result.records.length).toBe(2);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('blocking hook 失败应停止后续', async () => {
|
|
109
|
-
const ctx: TestContext = { value: '', calls: [] };
|
|
110
|
-
registry.register(makeHook('pass1'), makeConfig('pass1'));
|
|
111
|
-
registry.register(makeHook('block', {
|
|
112
|
-
execute: async () => ({ passed: false, error: 'blocked' }),
|
|
113
|
-
}), makeConfig('block', { errorStrategy: 'block' }));
|
|
114
|
-
registry.register(makeHook('never_runs'), makeConfig('never_runs'));
|
|
115
|
-
|
|
116
|
-
const result = await pipeline.run('before', ctx);
|
|
117
|
-
|
|
118
|
-
expect(result.passed).toBe(false);
|
|
119
|
-
expect(result.blockedBy).toEqual(['block']);
|
|
120
|
-
// block hook 的 execute 被覆盖,不 push calls
|
|
121
|
-
expect(ctx.calls).toEqual(['pass1']);
|
|
122
|
-
expect(ctx.calls).not.toContain('never_runs');
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('warn hook 失败应继续但记录警告', async () => {
|
|
126
|
-
const ctx: TestContext = { value: '', calls: [] };
|
|
127
|
-
registry.register(makeHook('warn_hook', {
|
|
128
|
-
execute: async () => ({ passed: false, error: 'warning' }),
|
|
129
|
-
}), makeConfig('warn_hook', { errorStrategy: 'warn' }));
|
|
130
|
-
registry.register(makeHook('should_still_run'), makeConfig('should_still_run'));
|
|
131
|
-
|
|
132
|
-
const result = await pipeline.run('before', ctx);
|
|
133
|
-
|
|
134
|
-
expect(result.passed).toBe(true);
|
|
135
|
-
expect(result.warnings).toEqual(['warn_hook']);
|
|
136
|
-
expect(ctx.calls).toContain('should_still_run');
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('hook 抛异常应被捕获', async () => {
|
|
140
|
-
const ctx: TestContext = { value: '', calls: [] };
|
|
141
|
-
registry.register(makeHook('throws', {
|
|
142
|
-
execute: async () => { throw new Error('crash'); },
|
|
143
|
-
}), makeConfig('throws', { errorStrategy: 'warn' }));
|
|
144
|
-
registry.register(makeHook('after_crash', { priority: 200 }), makeConfig('after_crash'));
|
|
145
|
-
|
|
146
|
-
const result = await pipeline.run('before', ctx);
|
|
147
|
-
|
|
148
|
-
expect(result.records[0].error).toContain('crash');
|
|
149
|
-
expect(ctx.calls).toContain('after_crash');
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('runFull 应在 before/after 之间执行操作', async () => {
|
|
153
|
-
const ctx: TestContext = { value: '', calls: [] };
|
|
154
|
-
registry.register(makeHook('before_hook'), makeConfig('before_hook'));
|
|
155
|
-
|
|
156
|
-
const afterHook = makeHook('after_hook', { phase: 'after' });
|
|
157
|
-
registry.register(afterHook, makeConfig('after_hook'));
|
|
158
|
-
|
|
159
|
-
const { pipelineResult, operationResult } = await pipeline.runFull(ctx, async () => {
|
|
160
|
-
ctx.calls.push('operation');
|
|
161
|
-
return 'done';
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
expect(pipelineResult.passed).toBe(true);
|
|
165
|
-
expect(operationResult).toBe('done');
|
|
166
|
-
expect(ctx.calls).toEqual(['before_hook', 'operation', 'after_hook']);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('before hook 失败不应执行操作', async () => {
|
|
170
|
-
const ctx: TestContext = { value: '', calls: [] };
|
|
171
|
-
registry.register(makeHook('block', {
|
|
172
|
-
execute: async () => ({ passed: false }),
|
|
173
|
-
}), makeConfig('block', { errorStrategy: 'block' }));
|
|
174
|
-
|
|
175
|
-
const { pipelineResult, operationResult } = await pipeline.runFull(ctx, async () => {
|
|
176
|
-
ctx.calls.push('should_not_run');
|
|
177
|
-
return 'nope';
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
expect(pipelineResult.passed).toBe(false);
|
|
181
|
-
expect(operationResult).toBeUndefined();
|
|
182
|
-
expect(ctx.calls).not.toContain('should_not_run');
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('空注册表应返回通过', async () => {
|
|
186
|
-
const result = await pipeline.run('before', { value: '', calls: [] });
|
|
187
|
-
expect(result.passed).toBe(true);
|
|
188
|
-
expect(result.records).toEqual([]);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it('应忽略配置禁用的 hook', async () => {
|
|
192
|
-
const ctx: TestContext = { value: '', calls: [] };
|
|
193
|
-
registry.register(makeHook('enabled'), makeConfig('enabled'));
|
|
194
|
-
registry.register(makeHook('disabled'), makeConfig('disabled', { enabled: false }));
|
|
195
|
-
|
|
196
|
-
await pipeline.run('before', ctx);
|
|
197
|
-
|
|
198
|
-
expect(ctx.calls).toEqual(['enabled']);
|
|
199
|
-
expect(ctx.calls).not.toContain('disabled');
|
|
200
|
-
});
|
|
201
|
-
});
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hook 配置归一映射语义测试(G7)
|
|
3
|
-
*
|
|
4
|
-
* blocking → errorStrategy 无损映射:
|
|
5
|
-
* - blocking=true → 'block'(失败阻断管线)
|
|
6
|
-
* - blocking=false → 'warn'(记录警告继续)
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { toErrorStrategy } from '../config';
|
|
10
|
-
|
|
11
|
-
describe('toErrorStrategy(blocking → errorStrategy 无损映射,G7)', () => {
|
|
12
|
-
it('blocking=true → block(失败阻断管线)', () => {
|
|
13
|
-
expect(toErrorStrategy(true)).toBe('block');
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('blocking=false → warn(记录警告继续)', () => {
|
|
17
|
-
expect(toErrorStrategy(false)).toBe('warn');
|
|
18
|
-
});
|
|
19
|
-
});
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HookPipeline errorStrategy 语义测试(G7 映射语义确认)
|
|
3
|
-
*
|
|
4
|
-
* block/warn 行为是 studio safeCallHook 语义在管线侧的无损承载面:
|
|
5
|
-
* - 'block':hook 失败 → passed=false、blockedBy 记名、停止执行后续 hook
|
|
6
|
-
* - 'warn':hook 失败 → 记录警告、继续执行后续 hook、passed 不受影响
|
|
7
|
-
*
|
|
8
|
-
* #159 起 `enabled` / `errorStrategy` 的唯一声明点是 HookConfig:
|
|
9
|
-
* HookDefinition 不再携带这两个字段,有效值在注册环节由配置表填充。
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { HookPipeline } from '../pipeline';
|
|
13
|
-
import { HookRegistry } from '../registry';
|
|
14
|
-
import type { HookConfig, HookDefinition } from '../types';
|
|
15
|
-
|
|
16
|
-
function makeConfig(name: string, overrides: Partial<HookConfig> = {}): HookConfig {
|
|
17
|
-
return { name, enabled: true, errorStrategy: 'warn', ...overrides };
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function makeRegistry(
|
|
21
|
-
entries: Array<{ hook: HookDefinition; config?: Partial<HookConfig> }>
|
|
22
|
-
): { registry: HookRegistry; pipeline: HookPipeline } {
|
|
23
|
-
const registry = new HookRegistry();
|
|
24
|
-
for (const { hook, config } of entries) {
|
|
25
|
-
registry.register(hook, makeConfig(hook.name, config));
|
|
26
|
-
}
|
|
27
|
-
return { registry, pipeline: new HookPipeline(registry) };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
describe('HookPipeline errorStrategy 语义', () => {
|
|
31
|
-
it('block:失败阻断管线(passed=false、blockedBy 记名、停止后续 hook)', async () => {
|
|
32
|
-
const second = jest.fn().mockResolvedValue({ passed: true });
|
|
33
|
-
const { pipeline } = makeRegistry([
|
|
34
|
-
{ hook: { name: 'first', phase: 'before', execute: async () => ({ passed: false, error: 'boom' }) }, config: { errorStrategy: 'block' } },
|
|
35
|
-
{ hook: { name: 'second', phase: 'before', execute: second } },
|
|
36
|
-
]);
|
|
37
|
-
|
|
38
|
-
const result = await pipeline.run('before', {});
|
|
39
|
-
|
|
40
|
-
expect(result.passed).toBe(false);
|
|
41
|
-
expect(result.blockedBy).toEqual(['first']);
|
|
42
|
-
expect(second).not.toHaveBeenCalled();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('warn:失败记录警告并继续(passed 不受影响、后续 hook 照常执行)', async () => {
|
|
46
|
-
const second = jest.fn().mockResolvedValue({ passed: true });
|
|
47
|
-
const { pipeline } = makeRegistry([
|
|
48
|
-
{ hook: { name: 'first', phase: 'before', execute: async () => ({ passed: false, error: 'soft' }) }, config: { errorStrategy: 'warn' } },
|
|
49
|
-
{ hook: { name: 'second', phase: 'before', execute: second } },
|
|
50
|
-
]);
|
|
51
|
-
|
|
52
|
-
const result = await pipeline.run('before', {});
|
|
53
|
-
|
|
54
|
-
expect(result.passed).toBe(true);
|
|
55
|
-
expect(result.warnings).toEqual(['first']);
|
|
56
|
-
expect(result.blockedBy).toEqual([]);
|
|
57
|
-
expect(second).toHaveBeenCalled();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('block 失败停在当前 phase,不改变其它 phase 的 hook', async () => {
|
|
61
|
-
const afterHook = jest.fn().mockResolvedValue({ passed: true });
|
|
62
|
-
const { pipeline } = makeRegistry([
|
|
63
|
-
{ hook: { name: 'blocker', phase: 'before', execute: async () => ({ passed: false }) }, config: { errorStrategy: 'block' } },
|
|
64
|
-
{ hook: { name: 'afterHook', phase: 'after', execute: afterHook } },
|
|
65
|
-
]);
|
|
66
|
-
|
|
67
|
-
const before = await pipeline.run('before', {});
|
|
68
|
-
expect(before.passed).toBe(false);
|
|
69
|
-
|
|
70
|
-
const after = await pipeline.run('after', {});
|
|
71
|
-
expect(after.passed).toBe(true);
|
|
72
|
-
expect(afterHook).toHaveBeenCalled();
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('配置 enabled:false 的 hook 不进入管线执行(配置表是唯一声明点)', async () => {
|
|
76
|
-
const disabledHook = jest.fn().mockResolvedValue({ passed: true });
|
|
77
|
-
const enabledHook = jest.fn().mockResolvedValue({ passed: true });
|
|
78
|
-
const { pipeline } = makeRegistry([
|
|
79
|
-
{ hook: { name: 'disabled', phase: 'before', execute: disabledHook }, config: { enabled: false } },
|
|
80
|
-
{ hook: { name: 'enabled', phase: 'before', execute: enabledHook } },
|
|
81
|
-
]);
|
|
82
|
-
|
|
83
|
-
const result = await pipeline.run('before', {});
|
|
84
|
-
|
|
85
|
-
expect(result.passed).toBe(true);
|
|
86
|
-
expect(disabledHook).not.toHaveBeenCalled();
|
|
87
|
-
expect(enabledHook).toHaveBeenCalled();
|
|
88
|
-
expect(result.records.map(r => r.hookName)).toEqual(['enabled']);
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe('HookPipeline.runOne 按名执行单个 hook(#167)', () => {
|
|
93
|
-
it('按名执行并返回观测记录,不连带其它 hook', async () => {
|
|
94
|
-
const other = jest.fn().mockResolvedValue({ passed: true });
|
|
95
|
-
const target = jest.fn().mockResolvedValue({ passed: true });
|
|
96
|
-
const { pipeline } = makeRegistry([
|
|
97
|
-
{ hook: { name: 'target', phase: 'before', execute: target } },
|
|
98
|
-
{ hook: { name: 'other', phase: 'before', execute: other } },
|
|
99
|
-
]);
|
|
100
|
-
|
|
101
|
-
const record = await pipeline.runOne('target', {});
|
|
102
|
-
|
|
103
|
-
expect(record.hookName).toBe('target');
|
|
104
|
-
expect(record.phase).toBe('before');
|
|
105
|
-
expect(record.passed).toBe(true);
|
|
106
|
-
expect(record.skipped).toBeUndefined();
|
|
107
|
-
expect(typeof record.durationMs).toBe('number');
|
|
108
|
-
expect(target).toHaveBeenCalledTimes(1);
|
|
109
|
-
expect(other).not.toHaveBeenCalled();
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('enabled:false → 实现体零调用,返回 skipped:true / passed:true 记录', async () => {
|
|
113
|
-
const disabled = jest.fn().mockResolvedValue({ passed: true });
|
|
114
|
-
const { pipeline } = makeRegistry([
|
|
115
|
-
{ hook: { name: 'disabled', phase: 'before', execute: disabled }, config: { enabled: false } },
|
|
116
|
-
]);
|
|
117
|
-
|
|
118
|
-
const record = await pipeline.runOne('disabled', {});
|
|
119
|
-
|
|
120
|
-
expect(disabled).not.toHaveBeenCalled();
|
|
121
|
-
expect(record.skipped).toBe(true);
|
|
122
|
-
expect(record.passed).toBe(true);
|
|
123
|
-
expect(record.hookName).toBe('disabled');
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('block + 实现体抛错 → 抛错,message 含 hook 名与原文', async () => {
|
|
127
|
-
const { pipeline } = makeRegistry([
|
|
128
|
-
{
|
|
129
|
-
hook: { name: 'guard', phase: 'before', execute: async () => { throw new Error('iron-law violated'); } },
|
|
130
|
-
config: { errorStrategy: 'block' },
|
|
131
|
-
},
|
|
132
|
-
]);
|
|
133
|
-
|
|
134
|
-
await expect(pipeline.runOne('guard', {})).rejects.toThrow('iron-law violated');
|
|
135
|
-
await expect(pipeline.runOne('guard', {})).rejects.toThrow('[harness] hook "guard" blocked:');
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('block + 实现体返回 passed:false → 抛错,message 含 result.error 原文', async () => {
|
|
139
|
-
const { pipeline } = makeRegistry([
|
|
140
|
-
{ hook: { name: 'guard', phase: 'before', execute: async () => ({ passed: false, error: 'boom' }) }, config: { errorStrategy: 'block' } },
|
|
141
|
-
]);
|
|
142
|
-
|
|
143
|
-
await expect(pipeline.runOne('guard', {})).rejects.toThrow('[harness] hook "guard" blocked: boom');
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('warn + 失败 → resolve,返回 passed:false 带 error 的记录', async () => {
|
|
147
|
-
const { pipeline } = makeRegistry([
|
|
148
|
-
{
|
|
149
|
-
hook: { name: 'soft', phase: 'before', execute: async () => { throw new Error('soft-fail'); } },
|
|
150
|
-
config: { errorStrategy: 'warn' },
|
|
151
|
-
},
|
|
152
|
-
]);
|
|
153
|
-
|
|
154
|
-
const record = await pipeline.runOne('soft', {});
|
|
155
|
-
|
|
156
|
-
expect(record.passed).toBe(false);
|
|
157
|
-
expect(record.error).toBe('soft-fail');
|
|
158
|
-
expect(record.skipped).toBeUndefined();
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('未知 hook 名 → 抛错(与注册表闭环口径一致,不静默跳过)', async () => {
|
|
162
|
-
const { pipeline } = makeRegistry([
|
|
163
|
-
{ hook: { name: 'known', phase: 'before', execute: async () => ({ passed: true }) } },
|
|
164
|
-
]);
|
|
165
|
-
|
|
166
|
-
await expect(pipeline.runOne('no_such_hook', {})).rejects.toThrow('no_such_hook');
|
|
167
|
-
await expect(pipeline.runOne('no_such_hook', {})).rejects.toThrow('未注册');
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('sampleRate < 1 时采样语义与 run 路径一致(未抽中 → 零调用 + sampled 记录)', async () => {
|
|
171
|
-
const execute = jest.fn().mockResolvedValue({ passed: true });
|
|
172
|
-
const { pipeline } = makeRegistry([
|
|
173
|
-
{ hook: { name: 'sampled', phase: 'before', sampleRate: 0.5, execute } },
|
|
174
|
-
]);
|
|
175
|
-
|
|
176
|
-
const randomSpy = jest.spyOn(Math, 'random').mockReturnValue(0.9);
|
|
177
|
-
try {
|
|
178
|
-
const record = await pipeline.runOne('sampled', {});
|
|
179
|
-
expect(execute).not.toHaveBeenCalled();
|
|
180
|
-
expect(record.sampled).toBe(true);
|
|
181
|
-
expect(record.passed).toBe(true);
|
|
182
|
-
} finally {
|
|
183
|
-
randomSpy.mockRestore();
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const hitSpy = jest.spyOn(Math, 'random').mockReturnValue(0.1);
|
|
187
|
-
try {
|
|
188
|
-
const record = await pipeline.runOne('sampled', {});
|
|
189
|
-
expect(execute).toHaveBeenCalledTimes(1);
|
|
190
|
-
expect(record.sampled).toBeUndefined();
|
|
191
|
-
} finally {
|
|
192
|
-
hitSpy.mockRestore();
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
});
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hook 注册表闭环双向校验测试(H5,复制 checker 闭环模式)
|
|
3
|
-
*
|
|
4
|
-
* 两个失败方向 + 重复声明/注册都必须抛错;闭环通过时不抛错。
|
|
5
|
-
*
|
|
6
|
-
* #159 起注册即配对:`register(hook, config)` 以 HookConfig 填充有效
|
|
7
|
-
* `enabled` / `errorStrategy`——配置表是这两个语义的唯一声明点,
|
|
8
|
-
* HookDefinition 类型上不再存在同名字段(矛盾在构造上不可能出现)。
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { assertHookRegistryClosed, HookRegistry } from '../registry';
|
|
12
|
-
import type { HookConfig, HookDefinition } from '../types';
|
|
13
|
-
|
|
14
|
-
function makeHook(name: string): HookDefinition {
|
|
15
|
-
return {
|
|
16
|
-
name,
|
|
17
|
-
phase: 'before',
|
|
18
|
-
execute: async () => ({ passed: true }),
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function makeConfig(name: string, overrides: Partial<HookConfig> = {}): HookConfig {
|
|
23
|
-
return { name, enabled: true, errorStrategy: 'block', ...overrides };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
describe('assertHookRegistryClosed', () => {
|
|
27
|
-
it('声明与注册一一对应时通过', () => {
|
|
28
|
-
const configs = [makeConfig('beforeAgentExecute'), makeConfig('afterReview')];
|
|
29
|
-
const hooks = [makeHook('beforeAgentExecute'), makeHook('afterReview')];
|
|
30
|
-
expect(() => assertHookRegistryClosed(configs, hooks)).not.toThrow();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('空声明 + 空注册通过(无 hook 的 consumer 合法)', () => {
|
|
34
|
-
expect(() => assertHookRegistryClosed([], [])).not.toThrow();
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('引用未注册 → 抛错:声明了配置但没有注册实现', () => {
|
|
38
|
-
const configs = [makeConfig('beforeAgentExecute'), makeConfig('ghostHook')];
|
|
39
|
-
const hooks = [makeHook('beforeAgentExecute')];
|
|
40
|
-
expect(() => assertHookRegistryClosed(configs, hooks)).toThrow(
|
|
41
|
-
/hook 配置 "ghostHook" 引用的实现未注册/
|
|
42
|
-
);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('注册无定义 → 抛错:注册了实现但没有配置声明', () => {
|
|
46
|
-
const configs = [makeConfig('beforeAgentExecute')];
|
|
47
|
-
const hooks = [makeHook('beforeAgentExecute'), makeHook('orphanHook')];
|
|
48
|
-
expect(() => assertHookRegistryClosed(configs, hooks)).toThrow(
|
|
49
|
-
/hook "orphanHook" 注册了实现但没有对应配置声明/
|
|
50
|
-
);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('重复声明 → 抛错', () => {
|
|
54
|
-
const configs = [makeConfig('dup'), makeConfig('dup')];
|
|
55
|
-
const hooks = [makeHook('dup')];
|
|
56
|
-
expect(() => assertHookRegistryClosed(configs, hooks)).toThrow(/重复声明/);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('重复注册 → 抛错', () => {
|
|
60
|
-
const configs = [makeConfig('dup')];
|
|
61
|
-
const hooks = [makeHook('dup'), makeHook('dup')];
|
|
62
|
-
expect(() => assertHookRegistryClosed(configs, hooks)).toThrow(/重复注册/);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('与 HookRegistry.listAll 组合可用(consumer 注册点用法)', () => {
|
|
66
|
-
const registry = new HookRegistry();
|
|
67
|
-
registry.registerAll([makeHook('beforeAgentExecute')], [makeConfig('beforeAgentExecute')]);
|
|
68
|
-
expect(() => assertHookRegistryClosed([makeConfig('beforeAgentExecute')], registry.listAll()))
|
|
69
|
-
.not.toThrow();
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
describe('HookRegistry 注册即配对(#159 配置表唯一化)', () => {
|
|
74
|
-
it('register 以 HookConfig 填充有效 enabled / errorStrategy', () => {
|
|
75
|
-
const registry = new HookRegistry();
|
|
76
|
-
registry.register(makeHook('a'), makeConfig('a', { enabled: false, errorStrategy: 'block' }));
|
|
77
|
-
|
|
78
|
-
const effective = registry.get('a');
|
|
79
|
-
expect(effective?.enabled).toBe(false);
|
|
80
|
-
expect(effective?.errorStrategy).toBe('block');
|
|
81
|
-
// 配置 enabled:false → 不进 enabled 列表
|
|
82
|
-
expect(registry.getEnabled('before')).toEqual([]);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('register 定义与配置名称不一致 → 抛错', () => {
|
|
86
|
-
const registry = new HookRegistry();
|
|
87
|
-
expect(() => registry.register(makeHook('a'), makeConfig('b'))).toThrow(/名称不一致/);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('registerAll 有实现缺配置声明 → 抛错', () => {
|
|
91
|
-
const registry = new HookRegistry();
|
|
92
|
-
expect(() =>
|
|
93
|
-
registry.registerAll([makeHook('a'), makeHook('orphan')], [makeConfig('a')])
|
|
94
|
-
).toThrow(/hook "orphan" 没有对应的 HookConfig 声明/);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('HookDefinition 类型上不再存在 enabled / errorStrategy(编译期钉)', () => {
|
|
98
|
-
const withStrategy: HookDefinition = {
|
|
99
|
-
name: 'x',
|
|
100
|
-
phase: 'before',
|
|
101
|
-
// @ts-expect-error HookDefinition 不再存在 errorStrategy 字段(#159)
|
|
102
|
-
errorStrategy: 'block',
|
|
103
|
-
execute: async () => ({ passed: true }),
|
|
104
|
-
};
|
|
105
|
-
const withEnabled: HookDefinition = {
|
|
106
|
-
name: 'y',
|
|
107
|
-
phase: 'before',
|
|
108
|
-
// @ts-expect-error HookDefinition 不再存在 enabled 字段(#159)
|
|
109
|
-
enabled: false,
|
|
110
|
-
execute: async () => ({ passed: true }),
|
|
111
|
-
};
|
|
112
|
-
expect(withStrategy.name).toBe('x');
|
|
113
|
-
expect(withEnabled.name).toBe('y');
|
|
114
|
-
});
|
|
115
|
-
});
|
package/src/hooks/config.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hook 配置归一(G7)
|
|
3
|
-
*
|
|
4
|
-
* studio 侧 per-hook 运行时配置(`blocking: boolean`)与 harness 侧
|
|
5
|
-
* `HookConfig.errorStrategy` 双轨归一:以 errorStrategy 为唯一真相,
|
|
6
|
-
* `blocking` → errorStrategy 为无损映射。
|
|
7
|
-
*
|
|
8
|
-
* 映射语义(确认于 H5):
|
|
9
|
-
* - blocking=true → 'block':hook 失败阻断管线(停止执行后续 hook,
|
|
10
|
-
* 管线结果 passed=false、blockedBy 记名)——对应 studio safeCallHook 抛错
|
|
11
|
-
* - blocking=false → 'warn':hook 失败记录警告并继续执行后续 hook
|
|
12
|
-
* (管线结果 warnings 记名,passed 不受影响)——对应 studio safeCallHook
|
|
13
|
-
* console.warn 继续
|
|
14
|
-
*
|
|
15
|
-
* #159 起 `HookConfig` 是 `enabled` / `errorStrategy` 的唯一声明点:
|
|
16
|
-
* HookDefinition 不再携带这两个字段,注册环节以配置表填充有效值,
|
|
17
|
-
* 有效策略集合收敛为 'block' | 'warn'(原 'ignore' 退出有效面)。
|
|
18
|
-
*
|
|
19
|
-
* studio 侧随动(studio #150 A4):hooks/config.ts 删 DEFAULTS/safeCallHook,
|
|
20
|
-
* blocking→errorStrategy 无损映射进 toHookDef;HARNESS_HOOK_DISABLE 保留 studio。
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import type { HookErrorStrategy } from './types';
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* blocking 布尔 → errorStrategy 无损映射(block/warn)
|
|
27
|
-
*
|
|
28
|
-
* @param blocking studio 侧「失败是否阻断」语义
|
|
29
|
-
* @returns 'block'(阻断)| 'warn'(警告继续)
|
|
30
|
-
*/
|
|
31
|
-
export function toErrorStrategy(blocking: boolean): HookErrorStrategy {
|
|
32
|
-
return blocking ? 'block' : 'warn';
|
|
33
|
-
}
|