Files
Codewalkers/apps/server/test/cassette/normalizer.ts
Lukas May 34578d39c6 refactor: Restructure monorepo to apps/server/ and apps/web/ layout
Move src/ → apps/server/ and packages/web/ → apps/web/ to adopt
standard monorepo conventions (apps/ for runnable apps, packages/
for reusable libraries). Update all config files, shared package
imports, test fixtures, and documentation to reflect new paths.

Key fixes:
- Update workspace config to ["apps/*", "packages/*"]
- Update tsconfig.json rootDir/include for apps/server/
- Add apps/web/** to vitest exclude list
- Update drizzle.config.ts schema path
- Fix ensure-schema.ts migration path detection (3 levels up in dev,
  2 levels up in dist)
- Fix tests/integration/cli-server.test.ts import paths
- Update packages/shared imports to apps/server/ paths
- Update all docs/ files with new paths
2026-03-03 11:22:53 +01:00

77 lines
2.9 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 NANOID_RE = /(?<![A-Za-z0-9])[A-Za-z0-9_-]{21}(?![A-Za-z0-9_-])/g;
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;
// Agent worktree paths: agent-workdirs/<random-agent-name> (with or without trailing slash)
// The agent name (e.g. "available-sheep") changes every run but is not a UUID or nanoid.
// Stop at the first slash so the project name after it is preserved.
const AGENT_WORKDIR_RE = /agent-workdirs\/[^\s/\\]+/g;
/**
* 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__
* 2.5. Nanoid IDs (21-char alphanumeric) → __ID__
* 3. ISO 8601 timestamps → __TIMESTAMP__
* 4. Unix epoch milliseconds → __EPOCH__
* 5. Session numbers → session__N__
* 6. Agent worktree path segment → agent-workdirs/__AGENT__/
*/
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(NANOID_RE, '__ID__');
normalized = normalized.replace(ISO_TIMESTAMP_RE, '__TIMESTAMP__');
normalized = normalized.replace(UNIX_EPOCH_MS_RE, '__EPOCH__');
normalized = normalized.replace(SESSION_NUM_RE, 'session__N__');
normalized = normalized.replace(AGENT_WORKDIR_RE, 'agent-workdirs/__AGENT__');
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;
}