mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
ca714ac1a7
* fix(hooks): post-task runs Stream B/D/F when --task-id is missing (#449) The shipped PostToolUse hook command for Task/Agent matchers is `npx agentic-qe hooks post-task --task-id "$TOOL_RESULT_agent_id" --success --json` Claude Code does not populate $TOOL_RESULT_agent_id, so the flag arrives empty and options.taskId is falsy on every real hook invocation. The `if (options.taskId)` gate inside post-task then short-circuited the entire Stream B/D/F learning chain: - reasoningBank.recordOutcome — never called - persistTaskOutcome — never called (no captured_experiences, experience_applications, qe_trajectories, dream_insights.applied) - updateRoutingOutcomeQuality — never called (routing_outcomes sentinels accumulated unfilled at success=0/quality_score=-1) - updateHookRouterQValue — never called (rl_q_values stayed empty forever) Same regression was fixed in v3.9.21 (patch 030) and silently came back in v3.9.23. Fix: - Remove the `if (options.taskId)` gate in post-task. Use `hook-${Date.now()}` fallback for the patternId / experience id when the caller didn't supply one. The bridge lookup in persistTaskOutcome keys by `ORDER BY created_at DESC LIMIT 1` (not by taskId), so Stream F's Bellman Q-update still finds the bridge payload it needs. - Remove the parallel `&& options.taskId` clause from the pre-task routing_outcomes sentinel write at line 195 — same root cause. - Add `DO NOT REINTRODUCE` comment blocks at both sites pointing at #449 and patch 030. Test: tests/unit/cli/commands/task-hooks-no-taskid.test.ts mocks hooks-shared and asserts persistTaskOutcome / updateHookRouterQValue fire when --task-id is absent. Bug-introduce verification: re-introducing the gate makes the no-taskId test fail with the expected "called 1 times, but got 0 times". Co-authored-by: Jordi Izquierdo <noreply@github.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(release): bump version to v3.9.24 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
132 lines
4.6 KiB
TypeScript
132 lines
4.6 KiB
TypeScript
/**
|
|
* Regression: issue #449
|
|
*
|
|
* post-task must run the full Stream B/D/F learning chain even when
|
|
* --task-id is missing.
|
|
*
|
|
* The shipped PostToolUse hook command is
|
|
* `npx agentic-qe hooks post-task --task-id "$TOOL_RESULT_agent_id" --success --json`
|
|
* Claude Code does not populate $TOOL_RESULT_agent_id for Task/Agent tools, so
|
|
* options.taskId is empty on every real invocation. The pre-fix code wrapped
|
|
* persistTaskOutcome / updateRoutingOutcomeQuality / updateHookRouterQValue in
|
|
* `if (options.taskId)`, silently killing all three. rl_q_values stayed empty
|
|
* forever.
|
|
*
|
|
* This test mocks hooks-shared and asserts the pipeline still fires with no
|
|
* --task-id. If the gate is reintroduced, persistTaskOutcome / Q-update spies
|
|
* will never be called and the test will fail.
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { Command } from 'commander';
|
|
|
|
const {
|
|
persistTaskOutcomeMock,
|
|
updateHookRouterQValueMock,
|
|
updateRoutingOutcomeQualityMock,
|
|
recordOutcomeMock,
|
|
} = vi.hoisted(() => ({
|
|
persistTaskOutcomeMock: vi.fn(),
|
|
updateHookRouterQValueMock: vi.fn(),
|
|
updateRoutingOutcomeQualityMock: vi.fn(),
|
|
recordOutcomeMock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../../../../src/cli/commands/hooks-handlers/hooks-shared.js', () => ({
|
|
getHooksSystem: vi.fn().mockResolvedValue({
|
|
hookRegistry: { emit: vi.fn().mockResolvedValue([]) },
|
|
reasoningBank: {
|
|
recordOutcome: recordOutcomeMock,
|
|
routeTask: vi.fn().mockResolvedValue({ success: false }),
|
|
},
|
|
}),
|
|
applyHookBusyTimeout: vi.fn(),
|
|
createHybridBackendWithTimeout: vi.fn().mockResolvedValue({
|
|
store: vi.fn(),
|
|
retrieve: vi.fn().mockResolvedValue(null),
|
|
delete: vi.fn(),
|
|
close: vi.fn(),
|
|
}),
|
|
incrementDreamExperience: vi.fn().mockResolvedValue(0),
|
|
checkAndTriggerDream: vi.fn().mockResolvedValue({ triggered: false }),
|
|
persistTaskOutcome: persistTaskOutcomeMock,
|
|
updateHookRouterQValue: updateHookRouterQValueMock,
|
|
updateRoutingOutcomeQuality: updateRoutingOutcomeQualityMock,
|
|
printJson: vi.fn(),
|
|
printSuccess: vi.fn(),
|
|
}));
|
|
|
|
import { registerTaskHooks } from '../../../../src/cli/commands/hooks-handlers/task-hooks.js';
|
|
|
|
describe('post-task without --task-id (issue #449)', () => {
|
|
beforeEach(() => {
|
|
persistTaskOutcomeMock.mockReset();
|
|
updateHookRouterQValueMock.mockReset();
|
|
updateRoutingOutcomeQualityMock.mockReset();
|
|
recordOutcomeMock.mockReset();
|
|
|
|
persistTaskOutcomeMock.mockResolvedValue({
|
|
experienceId: 'exp-test',
|
|
qualityScore: 0.575,
|
|
bridge: {
|
|
selectedPatternIds: ['p1'],
|
|
agent: 'qe-test-architect',
|
|
description: 'd',
|
|
taskType: 'test-generation',
|
|
priority: 'normal',
|
|
domain: 'general',
|
|
complexityBucket: 3,
|
|
estimatedTokenSavings: 0,
|
|
ts: Date.now(),
|
|
},
|
|
stitchedSiblings: 0,
|
|
insightsApplied: 0,
|
|
});
|
|
recordOutcomeMock.mockResolvedValue(undefined);
|
|
updateHookRouterQValueMock.mockResolvedValue(undefined);
|
|
updateRoutingOutcomeQualityMock.mockResolvedValue(undefined);
|
|
});
|
|
|
|
it('runs persistTaskOutcome and updateHookRouterQValue when --task-id is absent', async () => {
|
|
const hooks = new Command('hooks');
|
|
registerTaskHooks(hooks);
|
|
|
|
await hooks.parseAsync(
|
|
['post-task', '--success', 'true', '--agent', 'qe-test-architect', '--json'],
|
|
{ from: 'user' },
|
|
);
|
|
|
|
expect(persistTaskOutcomeMock).toHaveBeenCalledTimes(1);
|
|
const call = persistTaskOutcomeMock.mock.calls[0][0];
|
|
expect(call.agent).toBe('qe-test-architect');
|
|
expect(call.success).toBe(true);
|
|
expect(call.taskId).toMatch(/^hook-\d+$/);
|
|
|
|
expect(updateRoutingOutcomeQualityMock).toHaveBeenCalledTimes(1);
|
|
expect(updateHookRouterQValueMock).toHaveBeenCalledTimes(1);
|
|
expect(updateHookRouterQValueMock.mock.calls[0][0]).toMatchObject({
|
|
taskType: 'test-generation',
|
|
domain: 'general',
|
|
agent: 'qe-test-architect',
|
|
success: true,
|
|
});
|
|
|
|
expect(recordOutcomeMock).toHaveBeenCalledTimes(1);
|
|
expect(recordOutcomeMock.mock.calls[0][0].patternId).toMatch(/^task:qe-test-architect:hook-\d+$/);
|
|
});
|
|
|
|
it('still uses the real --task-id when one is provided', async () => {
|
|
const hooks = new Command('hooks');
|
|
registerTaskHooks(hooks);
|
|
|
|
await hooks.parseAsync(
|
|
['post-task', '--task-id', 'real-task-42', '--success', 'true', '--agent', 'coder', '--json'],
|
|
{ from: 'user' },
|
|
);
|
|
|
|
expect(persistTaskOutcomeMock).toHaveBeenCalledTimes(1);
|
|
expect(persistTaskOutcomeMock.mock.calls[0][0].taskId).toBe('real-task-42');
|
|
expect(recordOutcomeMock.mock.calls[0][0].patternId).toBe('task:coder:real-task-42');
|
|
});
|
|
});
|