@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 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
- // The test file renders its own output in a child process; propagate the
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
- process.exit(code);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sebastiantuyu/agest",
3
- "version": "0.3.3-next.10",
3
+ "version": "0.3.3-next.11",
4
4
  "description": "A testing library for agents",
5
5
  "repository": {
6
6
  "type": "git",