@sebastiantuyu/agest 0.3.3-next.10 → 0.3.3-next.11
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.
- package/dist/cli.d.ts +35 -0
- package/dist/cli.js +91 -6
- package/dist/context.js +21 -0
- package/package.json +1 -1
package/dist/cli.d.ts
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* One record per `agent()` run, appended by the child process (see
|
|
4
|
+
* AgentContext.execute → AGEST_SUMMARY_FILE). The parent reads them all back to
|
|
5
|
+
* print a vitest-style footer across files.
|
|
6
|
+
*/
|
|
7
|
+
interface RunSummaryRecord {
|
|
8
|
+
file: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
total: number;
|
|
11
|
+
passed: number;
|
|
12
|
+
failed: number;
|
|
13
|
+
duration: number;
|
|
14
|
+
costUsd: number | null;
|
|
15
|
+
}
|
|
2
16
|
export interface ParsedRunArgs {
|
|
3
17
|
pattern?: string;
|
|
4
18
|
targets: string[];
|
|
@@ -13,4 +27,25 @@ export interface ParsedRunArgs {
|
|
|
13
27
|
*/
|
|
14
28
|
export declare function getCommandArgs(argv: string[]): string[];
|
|
15
29
|
export declare function parseRunArgs(args: string[]): ParsedRunArgs;
|
|
30
|
+
export interface RunSummary {
|
|
31
|
+
/** Whether the footer should print — false for a single scene in one file. */
|
|
32
|
+
show: boolean;
|
|
33
|
+
discoveredFiles: number;
|
|
34
|
+
filesPassed: number;
|
|
35
|
+
filesFailed: number;
|
|
36
|
+
totalCases: number;
|
|
37
|
+
casesPassed: number;
|
|
38
|
+
casesFailed: number;
|
|
39
|
+
duration: number;
|
|
40
|
+
cost: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Aggregate every child's records into the footer totals. The footer only
|
|
44
|
+
* shows when more than one case ran (multiple files, or one file with multiple
|
|
45
|
+
* scenes) — a single scene already prints its own one-line summary. A file
|
|
46
|
+
* counts as failed if any of its agent() runs had a failing case, or if it
|
|
47
|
+
* never wrote a record (crashed before reporting).
|
|
48
|
+
*/
|
|
49
|
+
export declare function aggregateRunSummary(records: RunSummaryRecord[], discoveredFiles: number): RunSummary;
|
|
16
50
|
export declare function main(argv: string[]): Promise<void>;
|
|
51
|
+
export {};
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { realpathSync } from "node:fs";
|
|
4
|
+
import { realpathSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join, dirname } from "node:path";
|
|
5
7
|
import { main as stats } from "./stats.js";
|
|
6
8
|
import { main as preview } from "./preview.js";
|
|
7
9
|
import { DEFAULT_PATTERN, discoverTestFiles } from "./discover.js";
|
|
10
|
+
import { c } from "./logger.js";
|
|
8
11
|
/**
|
|
9
12
|
* Extract the args that follow the command word from a full `process.argv`.
|
|
10
13
|
* `argv = [execPath, scriptPath, command, ...commandArgs]`, so the command's
|
|
@@ -48,19 +51,101 @@ async function run(args) {
|
|
|
48
51
|
console.error(` No test files found (pattern: ${effective})`);
|
|
49
52
|
process.exit(1);
|
|
50
53
|
}
|
|
54
|
+
// Each child appends a summary record here; the parent reads them back for
|
|
55
|
+
// the aggregate footer. A unique dir keeps concurrent `agest run`s isolated.
|
|
56
|
+
const summaryFile = join(mkdtempSync(join(tmpdir(), "agest-")), "summary.jsonl");
|
|
57
|
+
const childEnv = {
|
|
58
|
+
...process.env,
|
|
59
|
+
AGEST_SUMMARY_FILE: summaryFile,
|
|
60
|
+
// The test file renders its own output in a child process; propagate
|
|
61
|
+
// --full so it emits the waterfall + full report rather than lean results.
|
|
62
|
+
...(full ? { AGEST_FULL: "1" } : {}),
|
|
63
|
+
};
|
|
64
|
+
let anyChildCrashed = false;
|
|
65
|
+
// Run every file (vitest-style) instead of bailing on the first failure, so
|
|
66
|
+
// the footer reflects the whole run. Exit non-zero at the end if any failed.
|
|
51
67
|
for (const file of files) {
|
|
52
68
|
const child = spawn("npx", ["tsx", file], {
|
|
53
69
|
stdio: "inherit",
|
|
54
70
|
shell: true,
|
|
55
|
-
|
|
56
|
-
// --full flag through the environment so it knows to emit the waterfall
|
|
57
|
-
// and full report rather than just per-scene results.
|
|
58
|
-
env: full ? { ...process.env, AGEST_FULL: "1" } : process.env,
|
|
71
|
+
env: childEnv,
|
|
59
72
|
});
|
|
60
73
|
const code = await new Promise((resolve) => child.on("close", (c) => resolve(c ?? 1)));
|
|
74
|
+
// A non-zero code means the file itself threw/crashed. Failing scenes do
|
|
75
|
+
// NOT surface here — the child resolves cleanly — so failure is read back
|
|
76
|
+
// from the summary records below.
|
|
61
77
|
if (code !== 0)
|
|
62
|
-
|
|
78
|
+
anyChildCrashed = true;
|
|
63
79
|
}
|
|
80
|
+
const records = readSummary(summaryFile);
|
|
81
|
+
printRunSummary(records, files.length);
|
|
82
|
+
try {
|
|
83
|
+
rmSync(dirname(summaryFile), { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
/* best-effort cleanup */
|
|
87
|
+
}
|
|
88
|
+
const casesFailed = records.reduce((sum, r) => sum + r.failed, 0);
|
|
89
|
+
if (anyChildCrashed || casesFailed > 0)
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
function readSummary(summaryFile) {
|
|
93
|
+
try {
|
|
94
|
+
return readFileSync(summaryFile, "utf8")
|
|
95
|
+
.split("\n")
|
|
96
|
+
.filter(Boolean)
|
|
97
|
+
.map((line) => JSON.parse(line));
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return []; // no children wrote results (older lib, or all crashed early)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Aggregate every child's records into the footer totals. The footer only
|
|
105
|
+
* shows when more than one case ran (multiple files, or one file with multiple
|
|
106
|
+
* scenes) — a single scene already prints its own one-line summary. A file
|
|
107
|
+
* counts as failed if any of its agent() runs had a failing case, or if it
|
|
108
|
+
* never wrote a record (crashed before reporting).
|
|
109
|
+
*/
|
|
110
|
+
export function aggregateRunSummary(records, discoveredFiles) {
|
|
111
|
+
const totalCases = records.reduce((sum, r) => sum + r.total, 0);
|
|
112
|
+
const failsByFile = new Map();
|
|
113
|
+
for (const r of records) {
|
|
114
|
+
failsByFile.set(r.file, (failsByFile.get(r.file) ?? 0) + r.failed);
|
|
115
|
+
}
|
|
116
|
+
const missing = Math.max(0, discoveredFiles - failsByFile.size);
|
|
117
|
+
const filesFailed = [...failsByFile.values()].filter((f) => f > 0).length + missing;
|
|
118
|
+
const casesPassed = records.reduce((sum, r) => sum + r.passed, 0);
|
|
119
|
+
return {
|
|
120
|
+
show: records.length > 0 && (discoveredFiles > 1 || totalCases > 1),
|
|
121
|
+
discoveredFiles,
|
|
122
|
+
filesPassed: discoveredFiles - filesFailed,
|
|
123
|
+
filesFailed,
|
|
124
|
+
totalCases,
|
|
125
|
+
casesPassed,
|
|
126
|
+
casesFailed: totalCases - casesPassed,
|
|
127
|
+
duration: records.reduce((sum, r) => sum + (r.duration || 0), 0),
|
|
128
|
+
cost: records.reduce((sum, r) => sum + (r.costUsd ?? 0), 0),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Print the vitest-style footer. Delegates the math to aggregateRunSummary and
|
|
133
|
+
* only renders when that says so.
|
|
134
|
+
*/
|
|
135
|
+
function printRunSummary(records, discoveredFiles) {
|
|
136
|
+
const s = aggregateRunSummary(records, discoveredFiles);
|
|
137
|
+
if (!s.show)
|
|
138
|
+
return;
|
|
139
|
+
const tally = (failed, passed, total) => failed > 0
|
|
140
|
+
? `${c.red(`${failed} failed`)} ${c.dim("|")} ${c.green(`${passed} passed`)} ${c.dim(`(${total})`)}`
|
|
141
|
+
: `${c.green(`${passed} passed`)} ${c.dim(`(${total})`)}`;
|
|
142
|
+
const line = (label, value) => console.log(`${c.dim(label.padStart(11))} ${value}`);
|
|
143
|
+
console.log("");
|
|
144
|
+
line("Test Files", tally(s.filesFailed, s.filesPassed, s.discoveredFiles));
|
|
145
|
+
line("Tests", tally(s.casesFailed, s.casesPassed, s.totalCases));
|
|
146
|
+
line("Duration", `${s.duration}ms`);
|
|
147
|
+
if (s.cost > 0)
|
|
148
|
+
line("Cost", c.green(`$${Number(s.cost.toFixed(4))}`));
|
|
64
149
|
}
|
|
65
150
|
function printUsage() {
|
|
66
151
|
console.log(`
|
package/dist/context.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "crypto";
|
|
2
|
+
import { appendFileSync } from "node:fs";
|
|
2
3
|
import { executeScene } from "./runner";
|
|
3
4
|
import { resolveText } from "./resolve";
|
|
4
5
|
import { formatReport, writeReport, writeDiffEntry } from "./reporter";
|
|
@@ -288,6 +289,26 @@ export class AgentContext {
|
|
|
288
289
|
}
|
|
289
290
|
const filepath = await writeReport(formatted, report.timestamp, report.name, report.dimensions);
|
|
290
291
|
logger.info(`${c.dim("Report saved to:")} ${c.cyan(filepath)}${full ? "" : c.dim(" (run with --full to print it)")}`);
|
|
292
|
+
// When launched by `agest run`, append a record so the parent can print a
|
|
293
|
+
// cross-file aggregate footer. Best-effort: never let it break a run.
|
|
294
|
+
const summaryFile = process.env.AGEST_SUMMARY_FILE;
|
|
295
|
+
if (summaryFile) {
|
|
296
|
+
const passed = results.filter((r) => r.passed).length;
|
|
297
|
+
try {
|
|
298
|
+
appendFileSync(summaryFile, JSON.stringify({
|
|
299
|
+
file: process.argv[1],
|
|
300
|
+
name: this._name,
|
|
301
|
+
total: results.length,
|
|
302
|
+
passed,
|
|
303
|
+
failed: results.length - passed,
|
|
304
|
+
duration: Math.round(totalDuration),
|
|
305
|
+
costUsd: totalCostUsd ?? null,
|
|
306
|
+
}) + "\n");
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
/* ignore */
|
|
310
|
+
}
|
|
311
|
+
}
|
|
291
312
|
return report;
|
|
292
313
|
}
|
|
293
314
|
}
|