ccqa 0.1.6 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,215 +0,0 @@
1
- import { readFile, writeFile, stat, unlink } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import { Command } from "commander";
4
- import {
5
- ensureCcqaDir,
6
- readSetupSpecFile,
7
- getSetupActions,
8
- getSetupDir,
9
- saveSetupTestScript,
10
- } from "../store/index.ts";
11
- import { actionsToScript } from "../codegen/actions-to-script.ts";
12
- import { buildCleanupPrompt, buildAutoFixPrompt } from "../prompts/codegen.ts";
13
- import { invokeClaudeStreaming } from "../claude/invoke.ts";
14
- import { parseSetupSpec } from "../spec/parser.ts";
15
- import type { TraceAction } from "../types.ts";
16
- import * as log from "./logger.ts";
17
-
18
- export const generateSetupCommand = new Command("generate-setup")
19
- .argument("<name>", "Setup name to generate (e.g. login)")
20
- .description("Clean up, validate, and templatize setup actions")
21
- .option("--max-retries <n>", "Maximum number of auto-fix retries", "3")
22
- .option("--from-dummy", "Resume from existing test.dummy.spec.ts (after manual fix)")
23
- .action(async (name: string, opts: { maxRetries: string; fromDummy?: boolean }) => {
24
- await runGenerateSetup(name, parseInt(opts.maxRetries, 10), opts.fromDummy ?? false);
25
- });
26
-
27
- async function runGenerateSetup(name: string, maxRetries: number, fromDummy: boolean): Promise<void> {
28
- log.header("generate-setup", name);
29
-
30
- await ensureCcqaDir();
31
-
32
- const specContent = await readSetupSpecFile(name);
33
- const spec = parseSetupSpec(specContent);
34
- const dummyPath = join(getSetupDir(name), "test.dummy.spec.ts");
35
- const finalPath = join(getSetupDir(name), "test.spec.ts");
36
-
37
- // Phase 1: Generate or reuse test.dummy.spec.ts
38
- if (fromDummy) {
39
- // --from-dummy: use existing test.dummy.spec.ts
40
- const exists = await stat(dummyPath).then(() => true).catch(() => false);
41
- if (!exists) {
42
- log.warn(`test.dummy.spec.ts not found. Run without --from-dummy first.`);
43
- process.exit(1);
44
- }
45
- log.info("Resuming from existing test.dummy.spec.ts");
46
- } else {
47
- // Normal: generate from actions.json
48
- const { actions } = await getSetupActions(name);
49
- log.meta("setup", spec.title);
50
- log.meta("actions", actions.length);
51
- log.blank();
52
-
53
- const cleanedActions = await cleanupActions(actions);
54
- if (cleanedActions.length !== actions.length) {
55
- log.meta("cleaned", cleanedActions.length);
56
- }
57
-
58
- const script = actionsToScript(cleanedActions, spec.title);
59
- await writeFile(dummyPath, script, "utf-8");
60
- log.meta("saved", dummyPath);
61
- }
62
- log.blank();
63
-
64
- // Phase 2: Run vitest on test.dummy.spec.ts with auto-fix
65
- let { exitCode, output, currentScript } = await runVitest(dummyPath);
66
-
67
- if (exitCode !== 0) {
68
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
69
- log.info(`auto-fix attempt ${attempt}/${maxRetries}...`);
70
- log.blank();
71
-
72
- const fixed = await autoFixWithLLM(currentScript, output);
73
- if (!fixed) {
74
- log.warn("could not determine fix from failure log");
75
- break;
76
- }
77
-
78
- await writeFile(dummyPath, fixed, "utf-8");
79
- log.meta("saved", dummyPath);
80
- log.blank();
81
-
82
- ({ exitCode, output, currentScript } = await runVitest(dummyPath));
83
- if (exitCode === 0) break;
84
- }
85
-
86
- if (exitCode !== 0) {
87
- log.warn("auto-fix exhausted — setup test still failing");
88
- log.hint(`edit ${dummyPath} manually, then run: ccqa generate-setup ${name} --from-dummy`);
89
- process.exit(1);
90
- }
91
- }
92
-
93
- // Phase 3: Reverse-replace dummy values → {{placeholders}}, save as test.spec.ts
94
- const templatizedScript = reversePlaceholdersInScript(
95
- currentScript,
96
- spec.placeholders as Record<string, { dummy: string; description?: string }> | undefined,
97
- );
98
-
99
- await writeFile(finalPath, templatizedScript, "utf-8");
100
- await unlink(dummyPath).catch(() => {});
101
-
102
- log.blank();
103
- log.meta("saved", finalPath);
104
- log.hint(`setup '${name}' is ready — reference it in test-spec.md with setups: [{name: ${name}, params: {...}}]`);
105
- }
106
-
107
- /**
108
- * Replace dummy values with {{placeholder}} directly in the test script text.
109
- * Longer dummy values are replaced first to avoid partial matches.
110
- */
111
- function reversePlaceholdersInScript(
112
- script: string,
113
- placeholders?: Record<string, { dummy: string; description?: string }>,
114
- ): string {
115
- if (!placeholders) return script;
116
-
117
- const entries = Object.entries(placeholders).sort(
118
- (a, b) => b[1].dummy.length - a[1].dummy.length,
119
- );
120
-
121
- let result = script;
122
- for (const [key, def] of entries) {
123
- result = result.replaceAll(def.dummy, `{{${key}}}`);
124
- }
125
- return result;
126
- }
127
-
128
- // --- Shared utilities ---
129
-
130
- interface SleepInsert { line: number; seconds: number; reason: string }
131
- interface SleepIncrease { line: number; increase_to: number; reason: string }
132
- type AutoFixAction = SleepInsert | SleepIncrease;
133
-
134
- async function autoFixWithLLM(script: string, failureLog: string): Promise<string | null> {
135
- try {
136
- const prompt = buildAutoFixPrompt(script, failureLog);
137
- const { result, isError } = await invokeClaudeStreaming(
138
- { prompt, disableBuiltinTools: true, maxTurns: 1 },
139
- () => {},
140
- );
141
- if (isError || !result) return null;
142
-
143
- const json = result.trim().replace(/^```(?:json)?\n?([\s\S]*?)\n?```$/, "$1").trim();
144
- const fixes = JSON.parse(json) as AutoFixAction[];
145
- if (!Array.isArray(fixes) || fixes.length === 0) return null;
146
-
147
- return applySleepFixes(script, fixes);
148
- } catch {
149
- return null;
150
- }
151
- }
152
-
153
- function applySleepFixes(script: string, fixes: AutoFixAction[]): string {
154
- const lines = script.split("\n");
155
-
156
- for (const fix of fixes) {
157
- if ("increase_to" in fix) {
158
- const idx = fix.line - 1;
159
- if (idx >= 0 && idx < lines.length) {
160
- lines[idx] = lines[idx]!.replace(
161
- /spawnSync\("sleep",\s*\["\d+"\]/,
162
- `spawnSync("sleep", ["${fix.increase_to}"]`,
163
- );
164
- }
165
- }
166
- }
167
-
168
- const inserts = fixes
169
- .filter((f): f is SleepInsert => "seconds" in f && !("increase_to" in f))
170
- .sort((a, b) => b.line - a.line);
171
-
172
- for (const fix of inserts) {
173
- const idx = fix.line - 1;
174
- if (idx >= 0 && idx <= lines.length) {
175
- lines.splice(idx, 0, ` spawnSync("sleep", ["${fix.seconds}"], { stdio: "inherit" });`);
176
- }
177
- }
178
-
179
- return lines.join("\n");
180
- }
181
-
182
- async function runVitest(scriptPath: string): Promise<{ exitCode: number; output: string; currentScript: string }> {
183
- const proc = Bun.spawn(["bunx", "vitest", "run", scriptPath], {
184
- stdout: "pipe",
185
- stderr: "pipe",
186
- });
187
-
188
- const [stdoutText, stderrText, exitCode] = await Promise.all([
189
- new Response(proc.stdout).text(),
190
- new Response(proc.stderr).text(),
191
- proc.exited,
192
- ]);
193
- const currentScript = await Bun.file(scriptPath).text();
194
-
195
- process.stdout.write(stdoutText);
196
- if (stderrText) process.stderr.write(stderrText);
197
- return { exitCode, output: stdoutText + stderrText, currentScript };
198
- }
199
-
200
- async function cleanupActions(actions: TraceAction[]): Promise<TraceAction[]> {
201
- try {
202
- const prompt = buildCleanupPrompt(actions);
203
- const { result, isError } = await invokeClaudeStreaming(
204
- { prompt, disableBuiltinTools: true, maxTurns: 1 },
205
- () => {},
206
- );
207
- if (isError || !result) return actions;
208
- const json = result.trim().replace(/^```(?:json)?\n?([\s\S]*?)\n?```$/, "$1").trim();
209
- const parsed = JSON.parse(json) as TraceAction[];
210
- if (Array.isArray(parsed) && parsed.length > 0) return parsed;
211
- } catch {
212
- // Fall through
213
- }
214
- return actions;
215
- }
@@ -1,224 +0,0 @@
1
- import { writeFile } from "node:fs/promises";
2
- import { Command } from "commander";
3
- import { readFile } from "node:fs/promises";
4
- import { join } from "node:path";
5
- import {
6
- ensureCcqaDir,
7
- parseSpecPath,
8
- getTraceActions,
9
- getSetupDir,
10
- readSpecFile,
11
- saveTestScript,
12
- } from "../store/index.ts";
13
- import { actionsToScript } from "../codegen/actions-to-script.ts";
14
- import type { SetupScript } from "../codegen/actions-to-script.ts";
15
- import { buildCleanupPrompt, buildAutoFixPrompt } from "../prompts/codegen.ts";
16
- import { invokeClaudeStreaming } from "../claude/invoke.ts";
17
- import { parseTestSpec } from "../spec/parser.ts";
18
- import type { TraceAction } from "../types.ts";
19
- import * as log from "./logger.ts";
20
-
21
- export const generateCommand = new Command("generate")
22
- .argument("<feature/spec>", "Spec to generate test for (e.g. tasks/create-and-complete)")
23
- .description("Generate agent-browser test script from recorded trace actions")
24
- .option("--max-retries <n>", "Maximum number of auto-fix retries", "3")
25
- .action(async (specPath: string, opts: { maxRetries: string }) => {
26
- const { featureName, specName } = parseSpecPath(specPath);
27
- await runGenerate(featureName, specName, parseInt(opts.maxRetries, 10));
28
- });
29
-
30
- async function runGenerate(featureName: string, specName: string, maxRetries: number): Promise<void> {
31
- log.header("generate", `${featureName}/${specName}`);
32
-
33
- await ensureCcqaDir();
34
-
35
- const { path: actionsPath, actions } = await getTraceActions(featureName, specName);
36
-
37
- log.meta("trace", actionsPath);
38
- log.meta("actions", actions.length);
39
-
40
- // Load setup actions if test-spec references setups
41
- const specContent = await readSpecFile(featureName, specName);
42
- const spec = parseTestSpec(specContent);
43
- const setupScripts = await loadSetupScripts(
44
- spec.setups as Array<{ name: string; params?: Record<string, string> }> | undefined,
45
- );
46
- if (setupScripts.length > 0) {
47
- log.meta("setups", setupScripts.map((s) => s.name).join(", "));
48
- }
49
- log.blank();
50
-
51
- const cleanedActions = await cleanupActions(actions);
52
- if (cleanedActions.length !== actions.length) {
53
- log.meta("cleaned", cleanedActions.length);
54
- }
55
-
56
- const script = actionsToScript(cleanedActions, spec.title, setupScripts.length > 0 ? setupScripts : undefined);
57
- const scriptPath = await saveTestScript(featureName, specName, script);
58
- log.meta("saved", scriptPath);
59
- log.blank();
60
-
61
- let { exitCode, output, currentScript } = await runVitest(scriptPath);
62
- if (exitCode === 0) {
63
- log.hint(`run 'ccqa run ${featureName}/${specName}' to execute the test`);
64
- return;
65
- }
66
-
67
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
68
- log.info(`auto-fix attempt ${attempt}/${maxRetries}...`);
69
- log.blank();
70
-
71
- const fixed = await autoFixWithLLM(currentScript, output);
72
- if (!fixed) {
73
- log.warn("could not determine fix from failure log");
74
- break;
75
- }
76
-
77
- await writeFile(scriptPath, fixed, "utf-8");
78
- log.meta("saved", scriptPath);
79
- log.blank();
80
-
81
- ({ exitCode, output, currentScript } = await runVitest(scriptPath));
82
- if (exitCode === 0) {
83
- log.hint(`run 'ccqa run ${featureName}/${specName}' to execute the test`);
84
- return;
85
- }
86
- }
87
-
88
- log.warn("auto-fix exhausted — test still failing");
89
- process.exit(1);
90
- }
91
-
92
- /**
93
- * Load setup test scripts, extract test body, and replace {{placeholders}} with params values.
94
- */
95
- async function loadSetupScripts(
96
- setups?: Array<{ name: string; params?: Record<string, string> }>,
97
- ): Promise<SetupScript[]> {
98
- if (!setups?.length) return [];
99
-
100
- const result: SetupScript[] = [];
101
- for (const ref of setups) {
102
- const scriptPath = join(getSetupDir(ref.name), "test.spec.ts");
103
- const script = await readFile(scriptPath, "utf-8").catch(() => {
104
- throw new Error(`Setup test script not found: ${scriptPath}. Run \`ccqa generate-setup ${ref.name}\` first.`);
105
- });
106
- const body = extractTestBody(script);
107
- const resolved = replacePlaceholders(body, ref.params ?? {});
108
- result.push({ name: ref.name, body: resolved });
109
- }
110
- return result;
111
- }
112
-
113
- /**
114
- * Extract the test body (lines inside the first test() block) from a setup test script.
115
- */
116
- function extractTestBody(script: string): string {
117
- const lines = script.split("\n");
118
- const startIdx = lines.findIndex((l) => /^\s*test\(/.test(l));
119
- if (startIdx === -1) return "";
120
- const bodyLines: string[] = [];
121
- for (let i = startIdx + 1; i < lines.length; i++) {
122
- // Match closing line: "}, N * 60 * 1000);" or "})" at the end of a test block
123
- if (/^\s*\}[\s,);]/.test(lines[i]!)) break;
124
- bodyLines.push(lines[i]!);
125
- }
126
- return bodyLines.join("\n");
127
- }
128
-
129
- function replacePlaceholders(body: string, params: Record<string, string>): string {
130
- let result = body;
131
- for (const [key, value] of Object.entries(params)) {
132
- result = result.replaceAll(`{{${key}}}`, value);
133
- }
134
- return result;
135
- }
136
-
137
- // --- Auto-fix ---
138
-
139
- interface SleepInsert { line: number; seconds: number; reason: string }
140
- interface SleepIncrease { line: number; increase_to: number; reason: string }
141
- type AutoFixAction = SleepInsert | SleepIncrease;
142
-
143
- async function autoFixWithLLM(script: string, failureLog: string): Promise<string | null> {
144
- try {
145
- const prompt = buildAutoFixPrompt(script, failureLog);
146
- const { result, isError } = await invokeClaudeStreaming(
147
- { prompt, disableBuiltinTools: true, maxTurns: 1 },
148
- () => {},
149
- );
150
- if (isError || !result) return null;
151
-
152
- const json = result.trim().replace(/^```(?:json)?\n?([\s\S]*?)\n?```$/, "$1").trim();
153
- const fixes = JSON.parse(json) as AutoFixAction[];
154
- if (!Array.isArray(fixes) || fixes.length === 0) return null;
155
-
156
- return applySleepFixes(script, fixes);
157
- } catch {
158
- return null;
159
- }
160
- }
161
-
162
- function applySleepFixes(script: string, fixes: AutoFixAction[]): string {
163
- const lines = script.split("\n");
164
-
165
- for (const fix of fixes) {
166
- if ("increase_to" in fix) {
167
- const idx = fix.line - 1;
168
- if (idx >= 0 && idx < lines.length) {
169
- lines[idx] = lines[idx]!.replace(
170
- /spawnSync\("sleep",\s*\["\d+"\]/,
171
- `spawnSync("sleep", ["${fix.increase_to}"]`,
172
- );
173
- }
174
- }
175
- }
176
-
177
- const inserts = fixes
178
- .filter((f): f is SleepInsert => "seconds" in f && !("increase_to" in f))
179
- .sort((a, b) => b.line - a.line);
180
-
181
- for (const fix of inserts) {
182
- const idx = fix.line - 1;
183
- if (idx >= 0 && idx <= lines.length) {
184
- lines.splice(idx, 0, ` spawnSync("sleep", ["${fix.seconds}"], { stdio: "inherit" });`);
185
- }
186
- }
187
-
188
- return lines.join("\n");
189
- }
190
-
191
- async function runVitest(scriptPath: string): Promise<{ exitCode: number; output: string; currentScript: string }> {
192
- const proc = Bun.spawn(["bunx", "vitest", "run", scriptPath], {
193
- stdout: "pipe",
194
- stderr: "pipe",
195
- });
196
-
197
- const [stdoutText, stderrText, exitCode] = await Promise.all([
198
- new Response(proc.stdout).text(),
199
- new Response(proc.stderr).text(),
200
- proc.exited,
201
- ]);
202
- const currentScript = await Bun.file(scriptPath).text();
203
-
204
- process.stdout.write(stdoutText);
205
- if (stderrText) process.stderr.write(stderrText);
206
- return { exitCode, output: stdoutText + stderrText, currentScript };
207
- }
208
-
209
- async function cleanupActions(actions: TraceAction[]): Promise<TraceAction[]> {
210
- try {
211
- const prompt = buildCleanupPrompt(actions);
212
- const { result, isError } = await invokeClaudeStreaming(
213
- { prompt, disableBuiltinTools: true, maxTurns: 1 },
214
- () => {},
215
- );
216
- if (isError || !result) return actions;
217
- const json = result.trim().replace(/^```(?:json)?\n?([\s\S]*?)\n?```$/, "$1").trim();
218
- const parsed = JSON.parse(json) as TraceAction[];
219
- if (Array.isArray(parsed) && parsed.length > 0) return parsed;
220
- } catch {
221
- // Fall through
222
- }
223
- return actions;
224
- }
package/src/cli/index.ts DELETED
@@ -1,25 +0,0 @@
1
- import { Command } from "commander";
2
- import { readFileSync } from "node:fs";
3
- import { traceCommand } from "./trace.ts";
4
- import { generateCommand } from "./generate.ts";
5
- import { runCommand } from "./run.ts";
6
- import { traceSetupCommand } from "./trace-setup.ts";
7
- import { generateSetupCommand } from "./generate-setup.ts";
8
-
9
- const packageJsonPath = new URL("../../package.json", import.meta.url);
10
- const { version } = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { version: string };
11
-
12
- const program = new Command();
13
-
14
- program
15
- .name("ccqa")
16
- .description("E2E test CLI using Claude Code + agent-browser")
17
- .version(version);
18
-
19
- program.addCommand(traceCommand);
20
- program.addCommand(generateCommand);
21
- program.addCommand(runCommand);
22
- program.addCommand(traceSetupCommand);
23
- program.addCommand(generateSetupCommand);
24
-
25
- program.parse();
package/src/cli/logger.ts DELETED
@@ -1,45 +0,0 @@
1
- import type { StepStatus } from "../types.ts";
2
-
3
- const STEP_ICONS: Record<StepStatus, string> = {
4
- STEP_START: "▶",
5
- STEP_DONE: "✓",
6
- ASSERTION_FAILED: "✗",
7
- STEP_SKIPPED: "⊘",
8
- RUN_COMPLETED: "■",
9
- };
10
-
11
- export function header(command: string, target?: string): void {
12
- process.stdout.write(`\nccqa ${command}${target ? ` ${target}` : ""}\n\n`);
13
- }
14
-
15
- export function meta(key: string, value: string | number): void {
16
- process.stdout.write(` ${key}: ${value}\n`);
17
- }
18
-
19
- export function blank(): void {
20
- process.stdout.write("\n");
21
- }
22
-
23
- export function info(message: string): void {
24
- process.stdout.write(`${message}\n`);
25
- }
26
-
27
- export function step(type: StepStatus, stepId: string, detail: string): void {
28
- process.stdout.write(` ${STEP_ICONS[type]} [${stepId}] ${detail}\n`);
29
- }
30
-
31
- export function bash(command: string): void {
32
- process.stdout.write(` $ ${command.slice(0, 120)}\n`);
33
- }
34
-
35
- export function error(message: string): void {
36
- process.stderr.write(`error: ${message}\n`);
37
- }
38
-
39
- export function warn(message: string): void {
40
- process.stderr.write(`warn: ${message}\n`);
41
- }
42
-
43
- export function hint(message: string): void {
44
- process.stdout.write(`\nhint: ${message}\n`);
45
- }