@kylecheng3146/agent-ops 0.5.2 → 0.6.0

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.
Files changed (103) hide show
  1. package/README.md +64 -0
  2. package/dist/packages/cli/src/args.js +214 -10
  3. package/dist/packages/cli/src/bin.js +77 -17
  4. package/dist/packages/cli/src/cli.js +42 -10
  5. package/dist/packages/cli/src/commands/advance.js +173 -0
  6. package/dist/packages/cli/src/commands/review-show.js +189 -0
  7. package/dist/packages/cli/src/commands/review.js +201 -34
  8. package/dist/packages/cli/src/commands/run.js +111 -0
  9. package/dist/packages/cli/src/commands/task.js +62 -17
  10. package/dist/packages/cli/src/commands/verify.js +1 -0
  11. package/dist/packages/cli/src/context.js +63 -1
  12. package/dist/packages/cli/src/hook-process.js +31 -1
  13. package/dist/packages/cli/src/owned-run-step.js +65 -0
  14. package/dist/packages/cli/src/parallel-deps.js +18 -5
  15. package/dist/packages/cli/src/run-deps.js +314 -0
  16. package/dist/packages/cli/src/run-entry.js +943 -0
  17. package/dist/packages/cli/src/run-setup-entry.js +32 -0
  18. package/dist/packages/cli/src/run-step-entry.js +58 -0
  19. package/dist/runtime/src/config/explain.js +6 -0
  20. package/dist/runtime/src/config/hash.js +1 -1
  21. package/dist/runtime/src/config/merge.js +9 -1
  22. package/dist/runtime/src/contracts.js +2 -2
  23. package/dist/runtime/src/hooks/completion-gate.js +8 -2
  24. package/dist/runtime/src/install/doctor.js +2 -0
  25. package/dist/runtime/src/install/harness.js +1 -1
  26. package/dist/runtime/src/install/preauth.js +10 -1
  27. package/dist/runtime/src/parallel/finish.js +217 -39
  28. package/dist/runtime/src/parallel/guard.js +5 -1
  29. package/dist/runtime/src/parallel/integrate.js +416 -0
  30. package/dist/runtime/src/parallel/manage.js +4 -1
  31. package/dist/runtime/src/parallel/receipt.js +153 -0
  32. package/dist/runtime/src/parallel/service.js +45 -3
  33. package/dist/runtime/src/review/attestation.js +60 -11
  34. package/dist/runtime/src/review/execute.js +24 -1
  35. package/dist/runtime/src/review/packet.js +35 -1
  36. package/dist/runtime/src/review/render.js +71 -0
  37. package/dist/runtime/src/review/runner.js +12 -1
  38. package/dist/runtime/src/review/scope.js +18 -1
  39. package/dist/runtime/src/review/slots.js +35 -10
  40. package/dist/runtime/src/run/authorization.js +60 -0
  41. package/dist/runtime/src/run/budget.js +30 -0
  42. package/dist/runtime/src/run/controls.js +139 -0
  43. package/dist/runtime/src/run/convergence.js +177 -0
  44. package/dist/runtime/src/run/hosts/claude.js +180 -0
  45. package/dist/runtime/src/run/hosts/codex.js +279 -0
  46. package/dist/runtime/src/run/hosts/index.js +4 -0
  47. package/dist/runtime/src/run/hosts/protocol.js +254 -0
  48. package/dist/runtime/src/run/hosts/types.js +1 -0
  49. package/dist/runtime/src/run/hosts/util.js +136 -0
  50. package/dist/runtime/src/run/integration.js +204 -0
  51. package/dist/runtime/src/run/macOS.js +189 -0
  52. package/dist/runtime/src/run/ownership.js +22 -0
  53. package/dist/runtime/src/run/planning.js +25 -0
  54. package/dist/runtime/src/run/policy-transition.js +1076 -0
  55. package/dist/runtime/src/run/policy.js +176 -0
  56. package/dist/runtime/src/run/ratchet.js +71 -0
  57. package/dist/runtime/src/run/scheduler.js +260 -0
  58. package/dist/runtime/src/run/service.js +425 -0
  59. package/dist/runtime/src/run/supervisor.js +635 -0
  60. package/dist/runtime/src/run/transport.js +159 -0
  61. package/dist/runtime/src/run/types.js +1 -0
  62. package/dist/runtime/src/run/usage.js +81 -0
  63. package/dist/runtime/src/run/verification.js +64 -0
  64. package/dist/runtime/src/run/worker-gate.js +48 -0
  65. package/dist/runtime/src/schema/acceptance.js +91 -0
  66. package/dist/runtime/src/schema/validate.js +72 -7
  67. package/dist/runtime/src/task/completion.js +26 -6
  68. package/dist/runtime/src/task/contract.js +17 -0
  69. package/dist/runtime/src/task/pin-finding.js +81 -0
  70. package/dist/runtime/src/task/render.js +3 -0
  71. package/dist/runtime/src/task/service.js +361 -10
  72. package/dist/runtime/src/task/store.js +57 -7
  73. package/dist/runtime/src/verify/acceptance-coverage.js +85 -0
  74. package/dist/runtime/src/verify/acceptance-protocol.js +252 -0
  75. package/dist/runtime/src/verify/acceptance-replay.js +239 -0
  76. package/dist/runtime/src/verify/adapters/index.js +5 -0
  77. package/dist/runtime/src/verify/adapters/jest.js +142 -0
  78. package/dist/runtime/src/verify/adapters/node-reporter.js +20 -0
  79. package/dist/runtime/src/verify/adapters/node.js +254 -0
  80. package/dist/runtime/src/verify/adapters/pytest.js +188 -0
  81. package/dist/runtime/src/verify/adapters/rust.js +84 -0
  82. package/dist/runtime/src/verify/adapters/shared.js +82 -0
  83. package/dist/runtime/src/verify/adapters/vitest.js +218 -0
  84. package/dist/runtime/src/verify/evidence.js +5 -2
  85. package/dist/runtime/src/verify/service.js +24 -8
  86. package/dist/runtime/src/verify/source-fingerprint.js +1 -1
  87. package/dist/runtime/src/verify/spawn.js +13 -2
  88. package/docs/en/guides/configuration.md +10 -0
  89. package/docs/en/spec/acceptance-and-evidence.md +58 -0
  90. package/docs/en/spec/delegation.md +4 -4
  91. package/docs/en/spec/loop-engineering.md +49 -0
  92. package/docs/en/spec/review.md +24 -2
  93. package/docs/zh-TW/guides/configuration.md +9 -0
  94. package/docs/zh-TW/spec/acceptance-and-evidence.md +31 -1
  95. package/docs/zh-TW/spec/delegation.md +4 -4
  96. package/docs/zh-TW/spec/loop-engineering.md +22 -0
  97. package/docs/zh-TW/spec/review.md +24 -2
  98. package/package.json +1 -1
  99. package/schemas/config.schema.json +221 -19
  100. package/schemas/evidence.schema.json +129 -6
  101. package/schemas/task.schema.json +268 -6
  102. package/templates/acceptance/agent_ops_acceptance.rs +36 -0
  103. package/templates/acceptance/agent_ops_pytest.py +46 -0
@@ -0,0 +1,142 @@
1
+ import { parseJsonDocument, parseJsonLines, stableCheckId } from "../acceptance-protocol.js";
2
+ import { array, hasRetry, lower, number, object, result, run, string } from "./shared.js";
3
+ function reportFromInput(input) {
4
+ if (typeof input === "string") {
5
+ const document = parseJsonDocument(input);
6
+ if (object(document) !== undefined)
7
+ return { report: object(document), diagnostics: [] };
8
+ const lines = parseJsonLines(input);
9
+ if (lines.values.length === 1 && object(lines.values[0]) !== undefined) {
10
+ return { report: object(lines.values[0]), diagnostics: lines.invalidLines.map((line) => `invalid-json-line:${line}`) };
11
+ }
12
+ return { report: undefined, diagnostics: ["malformed-output", ...lines.invalidLines.map((line) => `invalid-json-line:${line}`)] };
13
+ }
14
+ return object(input) === undefined
15
+ ? { report: undefined, diagnostics: ["malformed-report"] }
16
+ : { report: object(input), diagnostics: [] };
17
+ }
18
+ function suiteResults(suite) {
19
+ return array(suite.assertionResults) ?? array(suite.assertions) ?? array(suite.tests) ?? [];
20
+ }
21
+ function suiteFile(suite) {
22
+ return string(suite.testFilePath ?? suite.filePath ?? suite.file ?? suite.name) ?? "<jest-suite>";
23
+ }
24
+ function assertionId(suite, assertion) {
25
+ const ancestors = array(assertion.ancestorTitles)
26
+ ?.filter((item) => typeof item === "string") ?? [];
27
+ // Jest's fullName already contains ancestorTitles; prefer the leaf title so
28
+ // real JSON reports and reporter events share one identity format.
29
+ const name = string(assertion.title ?? assertion.name) ??
30
+ (ancestors.length === 0 ? string(assertion.fullName) : undefined);
31
+ if (name === undefined)
32
+ return undefined;
33
+ const location = object(assertion.location);
34
+ const line = number(location?.line);
35
+ const column = number(location?.column);
36
+ try {
37
+ return stableCheckId([
38
+ suiteFile(suite),
39
+ ...ancestors,
40
+ name,
41
+ ...(line === undefined || column === undefined ? [] : [`@${line}:${column}`])
42
+ ]);
43
+ }
44
+ catch {
45
+ return undefined;
46
+ }
47
+ }
48
+ function infrastructure(suite, report) {
49
+ const present = (value) => value !== undefined && value !== null &&
50
+ (!Array.isArray(value) || value.length > 0);
51
+ if (present(suite.testExecError) || present(suite.runtimeError) ||
52
+ present(report.runExecError) || present(report.unhandledErrors) || present(report.errors) ||
53
+ report.wasInterrupted === true)
54
+ return true;
55
+ const runtimeSuites = number(report.numRuntimeErrorTestSuites);
56
+ return runtimeSuites !== undefined && runtimeSuites > 0;
57
+ }
58
+ function evidence(suite, id) {
59
+ const file = suiteFile(suite);
60
+ return [`jest:assertion:${file}:${id}`];
61
+ }
62
+ /** Collect a final Jest JSON report or an equivalent structured reporter document. */
63
+ export function collectJestAcceptance(input, options) {
64
+ const parsed = reportFromInput(input);
65
+ const report = parsed.report;
66
+ if (report === undefined) {
67
+ return run(options, "jest", [], false, [], parsed.diagnostics);
68
+ }
69
+ const suites = array(report.testResults) ?? array(report.testFiles) ?? array(report.files);
70
+ if (suites === undefined) {
71
+ return run(options, "jest", [], false, [], [
72
+ ...parsed.diagnostics,
73
+ "missing-test-results"
74
+ ]);
75
+ }
76
+ const results = [];
77
+ const diagnostics = [...parsed.diagnostics];
78
+ for (const rawSuite of suites) {
79
+ const suite = object(rawSuite);
80
+ if (suite === undefined) {
81
+ diagnostics.push("malformed-suite");
82
+ continue;
83
+ }
84
+ const suiteHasInfrastructure = infrastructure(suite, report);
85
+ const assertions = suiteResults(suite);
86
+ if (assertions.length === 0 && suiteHasInfrastructure) {
87
+ diagnostics.push(`suite-infrastructure:${suiteFile(suite)}`);
88
+ }
89
+ for (const rawAssertion of assertions) {
90
+ const assertion = object(rawAssertion);
91
+ if (assertion === undefined) {
92
+ diagnostics.push(`malformed-assertion:${suiteFile(suite)}`);
93
+ continue;
94
+ }
95
+ const id = assertionId(suite, assertion);
96
+ if (id === undefined) {
97
+ diagnostics.push(`missing-check-id:${suiteFile(suite)}`);
98
+ continue;
99
+ }
100
+ const status = lower(assertion.status ?? assertion.state);
101
+ const observedAttempts = number(assertion.attempts ?? assertion.invocations);
102
+ const attemptCount = observedAttempts !== undefined && observedAttempts > 0 ? observedAttempts : null;
103
+ const evidenceRefs = evidence(suite, id);
104
+ const hooks = object(assertion.hooks);
105
+ const hookFailure = (Array.isArray(assertion.hookErrors) && assertion.hookErrors.length > 0) ||
106
+ (assertion.hookErrors !== undefined && assertion.hookErrors !== null && !Array.isArray(assertion.hookErrors)) ||
107
+ (hooks !== undefined && Object.values(hooks).some(state => state !== "pass" && state !== "passed"));
108
+ if (hasRetry(assertion)) {
109
+ results.push(result(options, "jest", id, "UNKNOWN", evidenceRefs, "retry", attemptCount));
110
+ }
111
+ else if (suiteHasInfrastructure) {
112
+ results.push(result(options, "jest", id, "UNKNOWN", evidenceRefs, "infrastructure-error", attemptCount));
113
+ }
114
+ else if (hookFailure) {
115
+ results.push(result(options, "jest", id, "UNKNOWN", evidenceRefs, "hook-error", attemptCount));
116
+ }
117
+ else if (status === "passed" || status === "pass") {
118
+ results.push(result(options, "jest", id, "PASS", evidenceRefs, "none", attemptCount));
119
+ }
120
+ else if (status === "failed" || status === "fail") {
121
+ // Native Jest JSON does not distinguish a test-call assertion from an
122
+ // assertion thrown by before/after hooks. Only classified collector
123
+ // metadata can establish behavioral red; failureMessages cannot.
124
+ const assertionFailure = assertion.assertion === true || assertion.failureClass === "assertion-failed";
125
+ results.push(assertionFailure
126
+ ? result(options, "jest", id, "FAIL", evidenceRefs, "assertion-failed", attemptCount)
127
+ : result(options, "jest", id, "UNKNOWN", evidenceRefs, "infrastructure-error", attemptCount));
128
+ }
129
+ else if (status === "pending" || status === "todo" || status === "skipped" || status === "disabled") {
130
+ results.push(result(options, "jest", id, "UNKNOWN", evidenceRefs, status === "todo" ? "todo" : "skipped", attemptCount));
131
+ }
132
+ else {
133
+ results.push(result(options, "jest", id, "UNKNOWN", evidenceRefs, "malformed-output", attemptCount));
134
+ }
135
+ }
136
+ }
137
+ if ((report.success === false || (number(report.numFailedTestSuites) ?? 0) > 0) &&
138
+ !results.some(item => item.status !== "PASS"))
139
+ diagnostics.push("unexplained-failed-report");
140
+ const completed = report.completed !== false && report.interrupted !== true;
141
+ return run(options, "jest", results, completed, completed ? ["jest:report:complete"] : [], diagnostics);
142
+ }
@@ -0,0 +1,20 @@
1
+ /** JSONL reporter for node --test; preserves assertion Error metadata. */
2
+ function errorData(value, seen = new Set()) {
3
+ if (value === null || typeof value !== "object")
4
+ return value;
5
+ if (seen.has(value))
6
+ return "[circular]";
7
+ seen.add(value);
8
+ if (value instanceof Error)
9
+ return { name: value.name,
10
+ code: value.code,
11
+ failureType: value.failureType,
12
+ cause: errorData(value.cause, seen) };
13
+ if (Array.isArray(value))
14
+ return value.map(item => errorData(item, seen));
15
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, errorData(item, seen)]));
16
+ }
17
+ export default async function* nodeAcceptanceReporter(events) {
18
+ for await (const event of events)
19
+ yield JSON.stringify(errorData(event)) + "\n";
20
+ }
@@ -0,0 +1,254 @@
1
+ import { resolve } from "node:path";
2
+ import { parseJsonDocument, parseJsonLines, stableCheckId } from "../acceptance-protocol.js";
3
+ import { attempts, hasRetry, lower, object, result, run, string, terminal, terminalEvidence } from "./shared.js";
4
+ function eventsFromInput(input) {
5
+ if (typeof input !== "string")
6
+ return { events: input, diagnostics: [] };
7
+ const document = parseJsonDocument(input);
8
+ if (Array.isArray(document))
9
+ return { events: document, diagnostics: [] };
10
+ const lines = parseJsonLines(input);
11
+ if (lines.values.length > 0 || lines.invalidLines.length > 0) {
12
+ return {
13
+ events: lines.values,
14
+ diagnostics: lines.invalidLines.map((line) => `invalid-json-line:${line}`)
15
+ };
16
+ }
17
+ return document === undefined
18
+ ? { events: [], diagnostics: ["malformed-output"] }
19
+ : { events: Array.isArray(document) ? document : [document], diagnostics: [] };
20
+ }
21
+ function eventData(value) {
22
+ return object(value.data) ?? value;
23
+ }
24
+ function eventType(value) {
25
+ return lower(value.type ?? value.event ?? value.kind);
26
+ }
27
+ function checkId(data) {
28
+ const file = string(data.file ?? data.filePath ?? data.filename);
29
+ const ancestors = Array.isArray(data.ancestorNames)
30
+ ? data.ancestorNames.filter((item) => typeof item === "string")
31
+ : Array.isArray(data.nesting)
32
+ ? data.nesting.filter((item) => typeof item === "string")
33
+ : Array.isArray(data.ancestorTitles)
34
+ ? data.ancestorTitles.filter((item) => typeof item === "string")
35
+ : [];
36
+ const name = string(data.name ?? data.title ?? data.fullName);
37
+ if (name === undefined)
38
+ return undefined;
39
+ const line = typeof data.line === "number" && Number.isSafeInteger(data.line) ? data.line : undefined;
40
+ const column = typeof data.column === "number" && Number.isSafeInteger(data.column) ? data.column : undefined;
41
+ try {
42
+ return stableCheckId([
43
+ file ?? "<node-test>",
44
+ ...ancestors,
45
+ name,
46
+ ...(line === undefined || column === undefined ? [] : [`@${line}:${column}`])
47
+ ]);
48
+ }
49
+ catch {
50
+ return undefined;
51
+ }
52
+ }
53
+ function assertionFailure(data) {
54
+ if (nonAssertionFailure(data) !== undefined)
55
+ return false;
56
+ if (data.assertion === true || data.isAssertion === true)
57
+ return true;
58
+ const explicit = lower(data.failureClass ?? data.failureKind ?? data.errorType);
59
+ if (explicit === "assertion" || explicit === "assertion-failed" || explicit === "test-assertion") {
60
+ return true;
61
+ }
62
+ const details = object(data.details);
63
+ const error = object(data.error) ?? object(details?.error);
64
+ const cause = object(error?.cause);
65
+ const errorName = lower(error?.name ?? error?.type);
66
+ const errorCode = lower(error?.code);
67
+ const causeName = lower(cause?.name ?? cause?.type);
68
+ const causeCode = lower(cause?.code);
69
+ return errorName === "assertionerror" || errorCode === "err_assertion" ||
70
+ causeName === "assertionerror" || causeCode === "err_assertion";
71
+ }
72
+ function nonAssertionFailure(data) {
73
+ const details = object(data.details);
74
+ const kind = lower(data.kind ?? data.scope ?? data.type ?? details?.type);
75
+ if (kind === "hook")
76
+ return "hook-error";
77
+ if (kind === "fixture")
78
+ return "fixture-error";
79
+ if (kind === "collection" || kind === "root" || kind === "suite")
80
+ return "collection-error";
81
+ if (data.timeout === true)
82
+ return "timeout";
83
+ const error = object(data.error) ?? object(details?.error);
84
+ const nativeFailure = lower(error?.failureType);
85
+ if (nativeFailure === "hookfailed")
86
+ return "hook-error";
87
+ if (nativeFailure === "testtimeoutfailure")
88
+ return "timeout";
89
+ if (nativeFailure === "cancelledbyparent" || nativeFailure === "testaborted")
90
+ return "cancelled";
91
+ if (nativeFailure !== undefined && nativeFailure !== "testcodefailure")
92
+ return "infrastructure-error";
93
+ const explicit = lower(data.failureClass ?? data.failureKind ?? data.errorType);
94
+ if (explicit !== undefined && !["assertion", "assertion-failed", "test-assertion"].includes(explicit)) {
95
+ return explicit;
96
+ }
97
+ return undefined;
98
+ }
99
+ function eventEvidence(type, data, id) {
100
+ const sequence = string(data.sequence ?? data.id ?? data.testId);
101
+ return [`node:${type}:${id}`, ...(sequence === undefined ? [] : [`sequence:${sequence}`])];
102
+ }
103
+ /** Collect node:test TestsStream/custom reporter events without parsing stdout text. */
104
+ export function collectNodeAcceptance(input, options) {
105
+ const parsed = eventsFromInput(input);
106
+ const results = [];
107
+ const seenCheckIds = new Set();
108
+ const duplicateCheckIds = new Set();
109
+ const pendingComplete = new Map();
110
+ const diagnostics = [...parsed.diagnostics];
111
+ let completed = false;
112
+ let failedSummary = false;
113
+ let completionEvidence = [];
114
+ // start/pass/fail follow definition order, even for concurrent suites.
115
+ // Execution-ordered complete events cannot supply unambiguous lineage.
116
+ const stacks = new Map();
117
+ const append = (id, data, evidence, attemptCount, passed) => {
118
+ const failure = hasRetry(data) ? "retry" : (data.skip === true || typeof data.skip === "string") ? "skipped"
119
+ : (data.todo === true || typeof data.todo === "string") ? "todo" : nonAssertionFailure(data);
120
+ results.push(failure !== undefined
121
+ ? result(options, "node:test", id, "UNKNOWN", evidence, failure, attemptCount)
122
+ : passed === true ? result(options, "node:test", id, "PASS", evidence, "none", attemptCount)
123
+ : passed === false && assertionFailure(data)
124
+ ? result(options, "node:test", id, "FAIL", evidence, "assertion-failed", attemptCount)
125
+ : result(options, "node:test", id, "UNKNOWN", evidence, passed === undefined ? "incomplete-output" : "infrastructure-error", attemptCount));
126
+ if (seenCheckIds.has(id))
127
+ duplicateCheckIds.add(id);
128
+ seenCheckIds.add(id);
129
+ };
130
+ for (const raw of parsed.events) {
131
+ const value = object(raw);
132
+ if (value === undefined) {
133
+ diagnostics.push("malformed-event");
134
+ continue;
135
+ }
136
+ const type = eventType(value);
137
+ const rawData = eventData(value);
138
+ const depth = rawData.nesting;
139
+ const rawFile = string(rawData.file);
140
+ const rawName = string(rawData.name);
141
+ let ancestors;
142
+ if (typeof depth === "number" && Number.isSafeInteger(depth) && depth >= 0 && depth <= 128 && rawFile !== undefined) {
143
+ const stack = stacks.get(rawFile) ?? [];
144
+ if (stack.length >= depth && !Array.isArray(rawData.ancestorNames) && !Array.isArray(rawData.ancestorTitles)) {
145
+ ancestors = stack.slice(0, depth);
146
+ }
147
+ if (type === "test:start" && rawName !== undefined) {
148
+ stack.length = depth;
149
+ stack.push(rawName);
150
+ stacks.set(rawFile, stack);
151
+ }
152
+ }
153
+ const data = ancestors === undefined ? rawData : { ...rawData, ancestorNames: ancestors };
154
+ if (terminal(value) || (type === "test:complete" &&
155
+ (value.completed === true || data.completed === true || value.terminal === true))) {
156
+ completed = true;
157
+ completionEvidence = [terminalEvidence(value, options.executionId)];
158
+ continue;
159
+ }
160
+ if (type === undefined) {
161
+ diagnostics.push("missing-event-type");
162
+ continue;
163
+ }
164
+ if (type === "test:summary" && data.success === false)
165
+ failedSummary = true;
166
+ if (type === "test:enqueue" || type === "test:dequeue" ||
167
+ type === "test:start" || type === "test:diagnostic" || type === "test:summary" ||
168
+ type === "test:coverage" || type === "test:stdout" || type === "test:stderr") {
169
+ if (type === "test:diagnostic" && (data.error !== undefined || data.failureClass !== undefined)) {
170
+ diagnostics.push(`node-diagnostic:${string(data.failureClass) ?? "infrastructure-error"}`);
171
+ }
172
+ continue;
173
+ }
174
+ const details = object(data.details);
175
+ const file = string(data.file ?? data.filePath);
176
+ const name = string(data.name);
177
+ const summary = lower(data.type ?? details?.type) === "suite" ||
178
+ (file !== undefined && name !== undefined && resolve(name) === resolve(file));
179
+ if (summary) {
180
+ const error = object(details?.error);
181
+ // A failed child already has its own result. Independent suite/file
182
+ // failures still poison the run; summaries never become check evidence.
183
+ if (error !== undefined && lower(error.failureType) !== "subtestsfailed") {
184
+ diagnostics.push(`node-summary:${nonAssertionFailure(data) ?? "infrastructure-error"}`);
185
+ }
186
+ continue;
187
+ }
188
+ if (type === "test:complete" && typeof data.nesting === "number")
189
+ continue;
190
+ if (type === "test:plan" || type === "plan") {
191
+ const count = data.count;
192
+ const nesting = typeof data.nesting === "number" ? data.nesting : 0;
193
+ if (nesting === 0 && typeof count === "number" && Number.isSafeInteger(count) && count >= 0) {
194
+ completed = true;
195
+ completionEvidence = [`node:test:plan:${count}`];
196
+ }
197
+ else if (typeof count !== "number" || !Number.isSafeInteger(count) || count < 0) {
198
+ diagnostics.push("malformed-plan");
199
+ }
200
+ continue;
201
+ }
202
+ const id = checkId(data);
203
+ if (id === undefined) {
204
+ diagnostics.push(`missing-check-id:${type}`);
205
+ continue;
206
+ }
207
+ const evidence = eventEvidence(type, data, id);
208
+ const attemptCount = attempts(data.attempts, hasRetry(data) ? 2 : 1);
209
+ if (type === "test:complete" || type === "test-complete") {
210
+ if (!seenCheckIds.has(id)) {
211
+ pendingComplete.set(id, { data, type, evidence, attempts: attemptCount });
212
+ }
213
+ continue;
214
+ }
215
+ if (hasRetry(data)) {
216
+ append(id, data, evidence, attemptCount);
217
+ continue;
218
+ }
219
+ if (type === "test:skip" || type === "test-skip" || type === "skip") {
220
+ append(id, { ...data, skip: true }, evidence, attemptCount);
221
+ continue;
222
+ }
223
+ if (type === "test:todo" || type === "test-todo" || type === "todo") {
224
+ append(id, { ...data, todo: true }, evidence, attemptCount);
225
+ continue;
226
+ }
227
+ if (type === "test:pass" || type === "test-pass" || type === "pass" ||
228
+ (type === "test:result" && lower(data.status) === "passed")) {
229
+ append(id, data, evidence, attemptCount, true);
230
+ continue;
231
+ }
232
+ if (type === "test:fail" || type === "test-fail" || type === "fail" ||
233
+ (type === "test:result" && lower(data.status) === "failed")) {
234
+ append(id, data, evidence, attemptCount, false);
235
+ continue;
236
+ }
237
+ diagnostics.push(`unsupported-event:${type}`);
238
+ }
239
+ for (const [id, pending] of pendingComplete.entries()) {
240
+ if (seenCheckIds.has(id))
241
+ continue;
242
+ const details = object(pending.data.details);
243
+ append(id, pending.data, pending.evidence, pending.attempts, typeof details?.passed === "boolean" ? details.passed : undefined);
244
+ }
245
+ for (let i = 0; i < results.length; i++) {
246
+ if (duplicateCheckIds.has(results[i].checkId)) {
247
+ results[i] = { ...results[i], status: "UNKNOWN", failureClass: "duplicate-check" };
248
+ }
249
+ }
250
+ if (failedSummary && results.every(result => result.status === "PASS")) {
251
+ diagnostics.push("unexplained-summary-failure");
252
+ }
253
+ return run(options, "node:test", results, completed, completionEvidence, diagnostics);
254
+ }
@@ -0,0 +1,188 @@
1
+ import { parseJsonDocument, parseJsonLines, stableCheckId } from "../acceptance-protocol.js";
2
+ import { array, hasRetry, lower, number, object, result, run, string, terminalEvidence } from "./shared.js";
3
+ function inputEvents(input) {
4
+ if (Array.isArray(input))
5
+ return { events: input, diagnostics: [] };
6
+ if (typeof input !== "string") {
7
+ const value = object(input);
8
+ if (value === undefined)
9
+ return { events: [], diagnostics: ["malformed-output"] };
10
+ return { events: [value], diagnostics: [] };
11
+ }
12
+ const document = parseJsonDocument(input);
13
+ if (Array.isArray(document))
14
+ return { events: document, diagnostics: [] };
15
+ if (object(document) !== undefined) {
16
+ const value = object(document);
17
+ return { events: [value], diagnostics: [] };
18
+ }
19
+ const lines = parseJsonLines(input);
20
+ return {
21
+ events: lines.values,
22
+ diagnostics: lines.invalidLines.map((line) => `invalid-json-line:${line}`)
23
+ };
24
+ }
25
+ function eventType(value) {
26
+ return lower(value.type ?? value.event ?? value.kind);
27
+ }
28
+ function nodeId(value) {
29
+ return string(value.nodeid ?? value.nodeId ?? value.testId ?? value.name ?? value.title);
30
+ }
31
+ function phaseValue(value) {
32
+ return object(value.report) ?? object(value.result) ?? value;
33
+ }
34
+ function phaseName(value) {
35
+ return lower(value.when ?? value.phase ?? value.stage) ?? "unknown";
36
+ }
37
+ function outcome(value) {
38
+ return lower(value.outcome ?? value.status ?? value.state);
39
+ }
40
+ function assertionFailure(value) {
41
+ if (value.assertion === true || value.isAssertion === true)
42
+ return true;
43
+ const type = lower(value.errorType ?? value.exceptionType ?? value.errorName);
44
+ return type === "assertionerror" || type === "assertion-error";
45
+ }
46
+ function addRecord(grouped, raw, diagnostics) {
47
+ const id = nodeId(raw);
48
+ if (id === undefined) {
49
+ diagnostics.push("missing-check-id");
50
+ return;
51
+ }
52
+ let stable;
53
+ try {
54
+ stable = stableCheckId([id]);
55
+ }
56
+ catch {
57
+ diagnostics.push("missing-check-id");
58
+ return;
59
+ }
60
+ const phase = phaseValue(raw) ?? raw;
61
+ const evidenceId = string(raw.id ?? raw.reportId ?? raw.nodeid ?? raw.nodeId) ?? stable;
62
+ const attemptCount = number(raw.attempts ?? phase.attempts);
63
+ const list = grouped.get(stable) ?? [];
64
+ list.push({
65
+ phase: phaseName(phase),
66
+ outcome: outcome(phase),
67
+ assertion: assertionFailure(phase),
68
+ xfail: phase.wasxfail === true ||
69
+ (typeof phase.wasxfail === "string" && phase.wasxfail.length > 0) ||
70
+ phase.xfail === true || outcome(phase) === "xfailed" || outcome(phase) === "xpassed",
71
+ retry: hasRetry(raw) || hasRetry(phase),
72
+ attempts: attemptCount !== undefined && attemptCount > 0 ? attemptCount : null,
73
+ evidence: `pytest:${stable}:${evidenceId}`,
74
+ infrastructure: (phase.error !== undefined && phase.error !== null) ||
75
+ (phase.crash !== undefined && phase.crash !== null && phase.crash !== false)
76
+ });
77
+ grouped.set(stable, list);
78
+ }
79
+ function appendReportTests(report, grouped, diagnostics) {
80
+ const tests = array(report.tests) ?? [];
81
+ for (const raw of tests) {
82
+ const test = object(raw);
83
+ if (test === undefined) {
84
+ diagnostics.push("malformed-test");
85
+ continue;
86
+ }
87
+ const call = object(test.call);
88
+ const setup = object(test.setup);
89
+ const teardown = object(test.teardown);
90
+ if (setup !== undefined)
91
+ addRecord(grouped, { ...test, ...setup, phase: "setup" }, diagnostics);
92
+ if (call !== undefined)
93
+ addRecord(grouped, { ...test, ...call, phase: "call" }, diagnostics);
94
+ else
95
+ addRecord(grouped, test, diagnostics);
96
+ if (teardown !== undefined)
97
+ addRecord(grouped, { ...test, ...teardown, phase: "teardown" }, diagnostics);
98
+ }
99
+ }
100
+ /** Collect a structured pytest hook stream or pytest-json-report-like document. */
101
+ export function collectPytestAcceptance(input, options) {
102
+ const parsed = inputEvents(input);
103
+ const grouped = new Map();
104
+ const diagnostics = [...parsed.diagnostics];
105
+ let completed = false;
106
+ let completionEvidence = [];
107
+ for (const raw of parsed.events) {
108
+ const value = object(raw);
109
+ if (value === undefined) {
110
+ diagnostics.push("malformed-event");
111
+ continue;
112
+ }
113
+ if (Array.isArray(value.tests)) {
114
+ appendReportTests(value, grouped, diagnostics);
115
+ if (value.completed !== false && value.interrupted !== true) {
116
+ completed = true;
117
+ completionEvidence = ["pytest:report:complete"];
118
+ }
119
+ continue;
120
+ }
121
+ const type = eventType(value);
122
+ if (["session-finished", "session-finish", "complete", "completed", "finished", "run-complete"].includes(type ?? "") &&
123
+ value.completed !== false && value.interrupted !== true) {
124
+ completed = true;
125
+ completionEvidence = [terminalEvidence(value, options.executionId)];
126
+ continue;
127
+ }
128
+ if (type === "session-error" || type === "internal-error" || type === "collection-error") {
129
+ diagnostics.push(`pytest:${type}`);
130
+ continue;
131
+ }
132
+ if (type === "test-report" || type === "runtest" || type === "test") {
133
+ addRecord(grouped, value, diagnostics);
134
+ continue;
135
+ }
136
+ diagnostics.push(`unsupported-event:${type ?? "unknown"}`);
137
+ }
138
+ const results = [];
139
+ for (const [id, records] of grouped.entries()) {
140
+ const refs = records.map((record) => record.evidence);
141
+ const attemptsCount = records.reduce((max, record) => {
142
+ if (max === null || record.attempts === null)
143
+ return null;
144
+ return Math.max(max, record.attempts);
145
+ }, 1);
146
+ if (records.some((record) => record.retry || (record.attempts !== null && record.attempts > 1))) {
147
+ results.push(result(options, "pytest", id, "UNKNOWN", refs, "retry", attemptsCount === null ? null : Math.max(attemptsCount, 2)));
148
+ continue;
149
+ }
150
+ const setupOrTeardownFailure = records.some((record) => (record.phase === "setup" || record.phase === "teardown") &&
151
+ record.outcome !== "passed" && record.outcome !== "pass");
152
+ if (setupOrTeardownFailure || records.some((record) => record.infrastructure)) {
153
+ results.push(result(options, "pytest", id, "UNKNOWN", refs, "fixture-error", attemptsCount));
154
+ continue;
155
+ }
156
+ const phases = records.map(record => record.phase);
157
+ if (new Set(phases).size !== phases.length) {
158
+ results.push(result(options, "pytest", id, "UNKNOWN", refs, "duplicate-check", attemptsCount));
159
+ continue;
160
+ }
161
+ const call = records.find((record) => record.phase === "call");
162
+ if (phases.join(",") !== "setup,call,teardown" || call === undefined) {
163
+ results.push(result(options, "pytest", id, "UNKNOWN", refs, "incomplete-output", attemptsCount));
164
+ continue;
165
+ }
166
+ if (call.xfail) {
167
+ results.push(result(options, "pytest", id, "UNKNOWN", refs, "xfail", attemptsCount));
168
+ }
169
+ else if (call.outcome === "passed" || call.outcome === "pass") {
170
+ results.push(result(options, "pytest", id, "PASS", refs, "none", attemptsCount));
171
+ }
172
+ else if (call.outcome === "failed" || call.outcome === "fail") {
173
+ results.push(call.assertion
174
+ ? result(options, "pytest", id, "FAIL", refs, "assertion-failed", attemptsCount)
175
+ : result(options, "pytest", id, "UNKNOWN", refs, "infrastructure-error", attemptsCount));
176
+ }
177
+ else if (call.outcome === "skipped" || call.outcome === "skip") {
178
+ results.push(result(options, "pytest", id, "UNKNOWN", refs, "skipped", attemptsCount));
179
+ }
180
+ else if (call.outcome === "xfailed" || call.outcome === "xpassed") {
181
+ results.push(result(options, "pytest", id, "UNKNOWN", refs, "xfail", attemptsCount));
182
+ }
183
+ else {
184
+ results.push(result(options, "pytest", id, "UNKNOWN", refs, "incomplete-output", attemptsCount));
185
+ }
186
+ }
187
+ return run(options, "pytest", results, completed, completionEvidence, diagnostics);
188
+ }