Files
proffesor-for-testing__agen…/tests/unit/cli/commands/post-task-agent-fallback.test.ts
Dragan Spiridonov 79c9a99f9c fix(hooks): use bridge.agent as action_key in Q-learning when --agent is unavailable (#460)
The post-task hook command passes `--agent "$TOOL_RESULT_agent_id"`, but
Claude Code does not expose that env var in PostToolUse context — so
`options.agent` arrives empty, the local `agent` falls back to 'unknown',
and every rl_q_values row used `action_key='unknown'`. The Q-learning
router could never learn per-agent differentiation.

The pre-task bridge payload already carries
`agent: routing.recommendedAgent`, which is the right action key. Compute
an `effectiveAgent` after persistTaskOutcome returns: prefer an explicit
--agent value (user override), otherwise fall back to bridge.agent, and
only land on 'unknown' when neither is available. Use it for both
`updateHookRouterQValue` and `updateRoutingOutcomeQuality` (the latter
uses agent as a tiebreaker in the routing-outcome sentinel match, so it
benefits from the same correction).

4 regression tests cover: bridge fallback when --agent absent, bridge
fallback for explicit empty --agent, explicit user override wins, and
falls through to 'unknown' when neither source is available.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 11:16:05 +00:00

145 lines
5.1 KiB
TypeScript

/**
* Regression: issue #460
*
* post-task resolves `agent = options.agent || 'unknown'`. The shipped hook
* command is `--agent "$TOOL_RESULT_agent_id"` and Claude Code does NOT
* expose that env var in PostToolUse context, so options.agent arrives empty
* and `agent` always lands on 'unknown'. Every rl_q_values row used
* action_key='unknown', and the router could never learn per-agent
* differentiation.
*
* The pre-task bridge payload already carries `agent: routing.recommendedAgent`
* which is the correct action key. This test asserts that the post-task
* pipeline uses `bridge.agent` when --agent is absent, AND that an explicit
* --agent value still wins (so a user override is respected).
*/
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';
const baseBridge = {
selectedPatternIds: ['p1'],
agent: 'qe-test-architect',
description: 'd',
taskType: 'test-generation',
priority: 'normal',
domain: 'test-generation',
complexityBucket: 3,
estimatedTokenSavings: 0,
ts: Date.now(),
};
describe('post-task uses bridge.agent when --agent is absent (#460)', () => {
beforeEach(() => {
persistTaskOutcomeMock.mockReset();
updateHookRouterQValueMock.mockReset();
updateRoutingOutcomeQualityMock.mockReset();
recordOutcomeMock.mockReset();
persistTaskOutcomeMock.mockResolvedValue({
experienceId: 'exp-test',
qualityScore: 0.575,
bridge: baseBridge,
stitchedSiblings: 0,
insightsApplied: 0,
});
recordOutcomeMock.mockResolvedValue(undefined);
updateHookRouterQValueMock.mockResolvedValue(undefined);
updateRoutingOutcomeQualityMock.mockResolvedValue(undefined);
});
it('falls back to bridge.agent for Q-learning when --agent is missing', async () => {
const hooks = new Command('hooks');
registerTaskHooks(hooks);
await hooks.parseAsync(['post-task', '--success', 'true', '--json'], { from: 'user' });
expect(updateHookRouterQValueMock).toHaveBeenCalledTimes(1);
expect(updateHookRouterQValueMock.mock.calls[0][0].agent).toBe('qe-test-architect');
expect(updateRoutingOutcomeQualityMock).toHaveBeenCalledTimes(1);
expect(updateRoutingOutcomeQualityMock.mock.calls[0][0].agent).toBe('qe-test-architect');
});
it('falls back to bridge.agent when --agent is the literal empty string', async () => {
const hooks = new Command('hooks');
registerTaskHooks(hooks);
await hooks.parseAsync(
['post-task', '--agent', '', '--success', 'true', '--json'],
{ from: 'user' },
);
expect(updateHookRouterQValueMock.mock.calls[0][0].agent).toBe('qe-test-architect');
expect(updateRoutingOutcomeQualityMock.mock.calls[0][0].agent).toBe('qe-test-architect');
});
it('respects an explicit --agent over bridge.agent (user override)', async () => {
const hooks = new Command('hooks');
registerTaskHooks(hooks);
await hooks.parseAsync(
['post-task', '--agent', 'qe-coverage-specialist', '--success', 'true', '--json'],
{ from: 'user' },
);
expect(updateHookRouterQValueMock.mock.calls[0][0].agent).toBe('qe-coverage-specialist');
expect(updateRoutingOutcomeQualityMock.mock.calls[0][0].agent).toBe('qe-coverage-specialist');
});
it('stays on "unknown" when neither --agent nor bridge.agent is available', async () => {
persistTaskOutcomeMock.mockResolvedValueOnce({
experienceId: 'exp-test',
qualityScore: 0.575,
bridge: { ...baseBridge, agent: '' }, // bridge present but no agent
stitchedSiblings: 0,
insightsApplied: 0,
});
const hooks = new Command('hooks');
registerTaskHooks(hooks);
await hooks.parseAsync(['post-task', '--success', 'true', '--json'], { from: 'user' });
expect(updateHookRouterQValueMock.mock.calls[0][0].agent).toBe('unknown');
expect(updateRoutingOutcomeQualityMock.mock.calls[0][0].agent).toBe('unknown');
});
});