mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
5f711b5b3e
The shipped `aqe init` template wired the UserPromptSubmit hook to
`npx agentic-qe hooks route --task "$PROMPT" --json`, but Claude Code
never exposed `$PROMPT` as an env var — only stdin event JSON. The
shell expanded `"$PROMPT"` to `""`, so every prompt routed as empty
and `routing_outcomes.task_json` was persisted as `{"description":""}`,
silently breaking the routing learning loop.
- routing-hooks.ts: demote --task to optional, read stdin (500ms
timeout) and extract prompt/user_prompt/command/tool_input.{prompt,
description}; explicit error when neither source has content
- 07-hooks.ts and init-wizard-hooks.ts: drop --task "\$PROMPT", let
the CLI read stdin directly. isAqeHookEntry still matches both
shapes, so existing projects upgrade on `aqe init --upgrade`
- tests/unit/cli/commands/hooks-routing.test.ts: 10 unit tests
covering all event shapes, fallback ordering, malformed JSON, and
empty-field skip
Verified end-to-end: piped a Claude Code-shaped event into a fresh
project's configured route command and confirmed task_json captures
the real prompt instead of "". Backward-compat manual --task usage
preserved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
82 lines
2.9 KiB
TypeScript
82 lines
2.9 KiB
TypeScript
/**
|
|
* Routing hook CLI - prompt extraction tests
|
|
*
|
|
* Regression coverage for the UserPromptSubmit stdin path. Claude Code
|
|
* delivers the prompt body on stdin as a JSON event ({prompt: "..."}),
|
|
* not as an env var. Before this fix, the init template passed
|
|
* --task "$PROMPT" which the shell expanded to "" — so every routing
|
|
* decision saw an empty task and routing_outcomes.task_json ended up
|
|
* {"description":""} on every entry, breaking the learning loop.
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import { extractPromptFromEvent } from '../../../../src/cli/commands/hooks-handlers/routing-hooks.js';
|
|
|
|
describe('extractPromptFromEvent', () => {
|
|
it('returns empty string for empty input', () => {
|
|
expect(extractPromptFromEvent('')).toBe('');
|
|
expect(extractPromptFromEvent(' ')).toBe('');
|
|
});
|
|
|
|
it('extracts prompt from UserPromptSubmit event shape', () => {
|
|
const event = JSON.stringify({
|
|
hook_event_name: 'UserPromptSubmit',
|
|
session_id: 'abc',
|
|
prompt: 'generate tests for auth module',
|
|
});
|
|
expect(extractPromptFromEvent(event)).toBe('generate tests for auth module');
|
|
});
|
|
|
|
it('falls back to user_prompt field', () => {
|
|
const event = JSON.stringify({ user_prompt: 'find coverage gaps' });
|
|
expect(extractPromptFromEvent(event)).toBe('find coverage gaps');
|
|
});
|
|
|
|
it('extracts command from PreToolUse Bash events', () => {
|
|
const event = JSON.stringify({
|
|
hook_event_name: 'PreToolUse',
|
|
tool_name: 'Bash',
|
|
command: 'npm test',
|
|
});
|
|
expect(extractPromptFromEvent(event)).toBe('npm test');
|
|
});
|
|
|
|
it('reads tool_input.prompt for Task tool events (snake_case)', () => {
|
|
const event = JSON.stringify({
|
|
hook_event_name: 'PreToolUse',
|
|
tool_name: 'Task',
|
|
tool_input: { prompt: 'review PR for security issues' },
|
|
});
|
|
expect(extractPromptFromEvent(event)).toBe('review PR for security issues');
|
|
});
|
|
|
|
it('reads toolInput.description for Task tool events (camelCase)', () => {
|
|
const event = JSON.stringify({
|
|
toolInput: { description: 'run security scan' },
|
|
});
|
|
expect(extractPromptFromEvent(event)).toBe('run security scan');
|
|
});
|
|
|
|
it('treats non-JSON input as the prompt body verbatim', () => {
|
|
expect(extractPromptFromEvent('plain text prompt\n')).toBe('plain text prompt');
|
|
});
|
|
|
|
it('returns empty string when no recognized field is present', () => {
|
|
const event = JSON.stringify({ unrelated: 'data', count: 5 });
|
|
expect(extractPromptFromEvent(event)).toBe('');
|
|
});
|
|
|
|
it('ignores empty or whitespace-only prompt fields and tries the next candidate', () => {
|
|
const event = JSON.stringify({
|
|
prompt: '',
|
|
command: ' ',
|
|
user_prompt: 'real prompt',
|
|
});
|
|
expect(extractPromptFromEvent(event)).toBe('real prompt');
|
|
});
|
|
|
|
it('does not crash on malformed JSON — falls through to verbatim path', () => {
|
|
expect(extractPromptFromEvent('{not valid json')).toBe('{not valid json');
|
|
});
|
|
});
|