Implements cassette recording/replay to test the full agent execution pipeline (ProcessManager → FileTailer → OutputHandler → SignalManager) without real AI API calls. Key components: - `CassetteProcessManager`: extends ProcessManager, intercepts spawnDetached to replay cassettes or record real runs on completion - `replay-worker.mjs`: standalone node script that replays JSONL + signal.json as a subprocess, exercising the complete file-based output pipeline - `CassetteStore`: reads/writes cassette JSON files keyed by SHA256 hash - `normalizer.ts`: strips dynamic content (UUIDs, temp paths, timestamps, session numbers) from prompts for stable cassette keys - `key.ts`: hashes normalized prompt + provider args + worktree file content (worktree hash detects content drift for execute-mode agents) - `createCassetteHarness()`: wraps RealProviderHarness with cassette support, same interface so existing real-provider tests work unchanged Mode control via env vars: (default) → replay: cassette must exist (safe for CI) CW_CASSETTE_RECORD=1 → auto: replay if exists, record if missing CW_CASSETTE_FORCE_RECORD=1 → record: always run real agent, overwrite cassette MultiProviderAgentManager gains an optional `processManagerOverride` constructor parameter for clean dependency injection without changing existing callers. Cassette files live in src/test/cassettes/ and are intended to be committed to git so CI runs without API access.
68 lines
2.3 KiB
TypeScript
68 lines
2.3 KiB
TypeScript
/**
|
|
* Cassette Normalizer
|
|
*
|
|
* Strips dynamic content from prompts and CLI args before hashing into a cassette key.
|
|
* Dynamic content (UUIDs, temp paths, timestamps, session numbers) varies between
|
|
* test runs but doesn't affect how the agent responds — so we replace them with
|
|
* stable placeholders to get a stable cache key.
|
|
*/
|
|
|
|
const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
|
|
const ISO_TIMESTAMP_RE = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?/g;
|
|
const UNIX_EPOCH_MS_RE = /\b1[0-9]{12}\b/g;
|
|
const SESSION_NUM_RE = /\bsession[_\s-]?\d+\b/gi;
|
|
|
|
/**
|
|
* Normalize a prompt for stable cassette key generation.
|
|
*
|
|
* Replacements applied in order (most-specific first to avoid partial matches):
|
|
* 1. Absolute workspace root path → __WORKSPACE__
|
|
* 2. UUIDs → __UUID__
|
|
* 3. ISO 8601 timestamps → __TIMESTAMP__
|
|
* 4. Unix epoch milliseconds → __EPOCH__
|
|
* 5. Session numbers → session__N__
|
|
*/
|
|
export function normalizePrompt(prompt: string, workspaceRoot: string): string {
|
|
let normalized = prompt;
|
|
|
|
if (workspaceRoot) {
|
|
normalized = normalized.replaceAll(workspaceRoot, '__WORKSPACE__');
|
|
}
|
|
|
|
normalized = normalized.replace(UUID_RE, '__UUID__');
|
|
normalized = normalized.replace(ISO_TIMESTAMP_RE, '__TIMESTAMP__');
|
|
normalized = normalized.replace(UNIX_EPOCH_MS_RE, '__EPOCH__');
|
|
normalized = normalized.replace(SESSION_NUM_RE, 'session__N__');
|
|
|
|
return normalized;
|
|
}
|
|
|
|
/**
|
|
* Strip the prompt value from CLI args to produce stable modelArgs for the cassette key.
|
|
*
|
|
* Handles all provider prompt flag styles:
|
|
* - Native: `-p <prompt>` (Claude)
|
|
* - Flag: `--prompt <prompt>`, `-p <prompt>` (Gemini, Cursor, Auggie, Amp, Opencode)
|
|
* - Also removes the bare prompt value if it appears as a positional arg.
|
|
*/
|
|
export function stripPromptFromArgs(args: string[], prompt: string): string[] {
|
|
if (!prompt) return [...args];
|
|
|
|
const result: string[] = [];
|
|
let i = 0;
|
|
while (i < args.length) {
|
|
const arg = args[i];
|
|
const PROMPT_FLAGS = ['-p', '--prompt', '--message'];
|
|
|
|
if (PROMPT_FLAGS.includes(arg) && args[i + 1] === prompt) {
|
|
i += 2; // skip flag + value
|
|
} else if (arg === prompt) {
|
|
i += 1; // skip bare positional prompt
|
|
} else {
|
|
result.push(arg);
|
|
i++;
|
|
}
|
|
}
|
|
return result;
|
|
}
|