@dommaker/harness 1.8.0 → 1.8.1
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 +8 -1
- package/dist/cli/commands/check.d.ts +8 -0
- package/dist/cli/commands/check.d.ts.map +1 -1
- package/dist/cli/commands/check.js +10 -11
- package/dist/cli/commands/check.js.map +1 -1
- package/dist/cli/commands/status.d.ts +8 -0
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +5 -7
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/state-io.d.ts +34 -0
- package/dist/cli/state-io.d.ts.map +1 -0
- package/dist/cli/state-io.js +71 -0
- package/dist/cli/state-io.js.map +1 -0
- package/dist/hooks/pipeline.d.ts +19 -1
- package/dist/hooks/pipeline.d.ts.map +1 -1
- package/dist/hooks/pipeline.js +40 -0
- package/dist/hooks/pipeline.js.map +1 -1
- package/dist/hooks/types.d.ts +2 -0
- package/dist/hooks/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/CONTEXT.md +1 -1
- package/src/cli/__tests__/state-io.test.ts +50 -0
- package/src/cli/commands/CONTEXT.md +1 -0
- package/src/cli/commands/__tests__/check-read-count.test.ts +3 -0
- package/src/cli/commands/__tests__/check.test.ts +20 -8
- package/src/cli/commands/__tests__/status-extra.test.ts +25 -8
- package/src/cli/commands/__tests__/status.test.ts +46 -18
- package/src/cli/commands/check.ts +20 -18
- package/src/cli/commands/status.ts +12 -7
- package/src/cli/state-io.ts +53 -0
- package/src/hooks/CONTEXT.md +1 -1
- package/src/hooks/__tests__/pipeline.test.ts +105 -0
- package/src/hooks/pipeline.ts +47 -0
- package/src/hooks/types.ts +2 -0
|
@@ -88,3 +88,108 @@ describe('HookPipeline errorStrategy 语义', () => {
|
|
|
88
88
|
expect(result.records.map(r => r.hookName)).toEqual(['enabled']);
|
|
89
89
|
});
|
|
90
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
|
+
});
|
package/src/hooks/pipeline.ts
CHANGED
|
@@ -55,6 +55,53 @@ export class HookPipeline<C = unknown> {
|
|
|
55
55
|
return { passed, records, blockedBy, warnings };
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* 按名执行单个 hook(#167)
|
|
60
|
+
*
|
|
61
|
+
* 与 run() 共用 executeOne 的采样与错误隔离路径(判定与错误策略的实现体
|
|
62
|
+
* 只有一份);enabled / errorStrategy 的值来源仍是注册环节填充的
|
|
63
|
+
* EffectiveHook(唯一声明点 HookConfig,#159)。
|
|
64
|
+
*
|
|
65
|
+
* - enabled:false → 实现体零调用,返回 skipped:true / passed:true 记录
|
|
66
|
+
* (镜像 sampled 先例:未执行 = 记录上一面旗,不是异常)
|
|
67
|
+
* - errorStrategy 'block' 且失败 → 抛 Error(message 含 hook 名与原文);
|
|
68
|
+
* 'warn' 且失败 → resolve,返回 passed:false 带 error 的记录
|
|
69
|
+
* - 未知名 → 抛错(与 assertHookRegistryClosed 闭环口径一致,拒绝静默缺失)
|
|
70
|
+
*
|
|
71
|
+
* @param name hook 名称(须已注册)
|
|
72
|
+
* @param context 传递给 hook 的上下文
|
|
73
|
+
* @returns 管线观测记录
|
|
74
|
+
*/
|
|
75
|
+
async runOne(name: string, context: C): Promise<HookExecutionRecord> {
|
|
76
|
+
const hook = this.registry.get(name);
|
|
77
|
+
if (!hook) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`[harness] hook 执行失败:hook "${name}" 未注册。请注册同名 HookDefinition 与 HookConfig 声明。`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!hook.enabled) {
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
return {
|
|
86
|
+
hookName: hook.name,
|
|
87
|
+
phase: hook.phase,
|
|
88
|
+
startedAt: now,
|
|
89
|
+
completedAt: now,
|
|
90
|
+
durationMs: 0,
|
|
91
|
+
passed: true,
|
|
92
|
+
skipped: true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const record = await this.executeOne(hook, context);
|
|
97
|
+
if (!record.passed && hook.errorStrategy === 'block') {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`[harness] hook "${hook.name}" blocked: ${record.error ?? '未知错误'}`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return record;
|
|
103
|
+
}
|
|
104
|
+
|
|
58
105
|
/**
|
|
59
106
|
* 执行 before + after 全套管线
|
|
60
107
|
*
|