@tea-agent/loop-agent 0.44.0-next.10 → 0.44.0-next.12
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/CHANGELOG.md +11 -0
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi/sessions/index.js +6 -0
- package/dist/executors/dag-pi/sessions/plan-batches.js +280 -0
- package/dist/executors/dag-pi/sessions/plan-prompts.js +367 -0
- package/dist/executors/dag-pi/sessions/scout-parallel.js +197 -0
- package/dist/executors/dag-pi/sessions/segmented-plan.js +1184 -0
- package/dist/executors/dag-pi/sessions/writer-evidence.js +60 -0
- package/dist/executors/dag-pi-executor.js +12 -2075
- package/dist/executors/shell-executor.js +39 -11
- package/dist/worker/console/routes.js +5 -0
- package/dist/worker/console/static/app-icon.svg +39 -0
- package/dist/worker/console/static/index.html +3 -0
- package/dist/worker/console/static/manifest.webmanifest +19 -0
- package/dist/worker/observe/static/console-theme.js +38 -0
- package/dist/workflows/dag/checkpoint.js +686 -0
- package/dist/workflows/dag/frontend-repair.js +24 -0
- package/dist/workflows/dag/frontend-review-scopes.js +10 -2
- package/dist/workflows/dag/frontend-test-execution-evidence.js +163 -67
- package/dist/workflows/dag/frontend-test-framework-adapters.js +309 -0
- package/dist/workflows/dag/hybrid/sources.js +1812 -0
- package/dist/workflows/dag/hybrid/templates/backend-test.js +1807 -0
- package/dist/workflows/dag/hybrid/templates/frontend-test.js +702 -0
- package/dist/workflows/dag/hybrid/templates/frontend.js +1529 -0
- package/dist/workflows/dag/hybrid/templates/index.js +8 -0
- package/dist/workflows/dag/hybrid/templates/kg-bootstrap.js +449 -0
- package/dist/workflows/dag/hybrid/templates/knowledge-sync.js +495 -0
- package/dist/workflows/dag/hybrid/templates/shared.js +101 -0
- package/dist/workflows/dag/hybrid/templates/standard.js +265 -0
- package/dist/workflows/dag/hybrid/types.js +32 -0
- package/dist/workflows/dag/init-hybrid.js +42 -7162
- package/dist/workflows/dag/runner.js +11 -1178
- package/dist/workflows/dag/terminal-status.js +502 -0
- package/docs/architecture/dag-execution.md +4 -4
- package/package.json +1 -1
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { FRONTEND_NO_VERIFICATION_MARKER_TEXT, } from "./frontend-verification-trace.js";
|
|
3
|
+
import { FRONTEND_TEST_OBSERVATION_FAILURES } from "./frontend-test-execution-evidence.js";
|
|
4
|
+
/**
|
|
5
|
+
* Substrings that prove a behavior-verification failure came from the
|
|
6
|
+
* verification configuration rather than the candidate's code. Kept next to the
|
|
7
|
+
* taxonomy so a new failure code is routed deliberately instead of falling
|
|
8
|
+
* through to the string-blob heuristics below.
|
|
9
|
+
*/
|
|
10
|
+
const FRONTEND_TEST_VERIFICATION_CONFIG_MARKERS = [
|
|
11
|
+
FRONTEND_TEST_OBSERVATION_FAILURES.bindingDrift,
|
|
12
|
+
FRONTEND_TEST_OBSERVATION_FAILURES.reportProtocol,
|
|
13
|
+
FRONTEND_TEST_OBSERVATION_FAILURES.capabilityGap,
|
|
14
|
+
"FRONTEND_TEST_OBSERVATION_INVALID",
|
|
15
|
+
"frontend test execution report unavailable",
|
|
16
|
+
];
|
|
3
17
|
export const frontendRepairFailureClassSchema = z.enum([
|
|
4
18
|
"typecheck",
|
|
5
19
|
"build",
|
|
@@ -73,6 +87,16 @@ export function classifyFrontendFailure(input) {
|
|
|
73
87
|
if (input.failureCategory === "write-guard") {
|
|
74
88
|
return "path";
|
|
75
89
|
}
|
|
90
|
+
// Verification-config faults must never be mistaken for product defects.
|
|
91
|
+
// A missing/unreadable report, a stale report, an unsupported runner or a
|
|
92
|
+
// drifted command binding are all tooling/contract problems: rewriting the
|
|
93
|
+
// candidate's business code cannot fix them and burns a repair round. These
|
|
94
|
+
// are detected structurally, before any blob heuristic can see a runner name
|
|
95
|
+
// (the `unit-test` rule matches the literal word "jest") and misroute the
|
|
96
|
+
// failure to the implementation owner.
|
|
97
|
+
if (FRONTEND_TEST_VERIFICATION_CONFIG_MARKERS.some((marker) => `${input.stderr ?? ""}\n${input.stdout ?? ""}`.includes(marker))) {
|
|
98
|
+
return "contract";
|
|
99
|
+
}
|
|
76
100
|
if (`${input.stderr ?? ""}\n${input.stdout ?? ""}`.includes(FRONTEND_NO_VERIFICATION_MARKER_TEXT)) {
|
|
77
101
|
// Verification unavailability is a generation-time contract gap: no
|
|
78
102
|
// authorized verification command exists. Bounded repair cannot fix it;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parseFrontendTestExecutionReport } from "./frontend-test-execution-evidence.js";
|
|
2
|
+
import { isFrontendTestReportScheme } from "./frontend-test-framework-adapters.js";
|
|
2
3
|
import { readFileBoundarySafe } from "../../shared/path-safety.js";
|
|
3
4
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
5
|
import { readFile, realpath, lstat } from "node:fs/promises";
|
|
@@ -109,12 +110,16 @@ export async function loadFrontendReviewScopes(runDir, phase, targetBytes, works
|
|
|
109
110
|
const reportCoverage = new Map();
|
|
110
111
|
const reportedTests = [];
|
|
111
112
|
for (const ref of payload.verificationTrace?.testExecutionEvidence ?? []) {
|
|
112
|
-
if (!isArtifactReference(ref) || typeof ref.reportPath !== "string" || !/^[a-f0-9]{64}$/.test(ref.reportSha256))
|
|
113
|
+
if (!isArtifactReference(ref) || typeof ref.reportPath !== "string" || !/^[a-f0-9]{64}$/.test(ref.reportSha256) || !isFrontendTestReportScheme(ref.scheme))
|
|
113
114
|
throw invalidScope("invalid test execution reference");
|
|
114
115
|
const excerpt = await read(ref.path, ref.sha256);
|
|
115
116
|
const data = JSON.parse(excerpt.text);
|
|
116
117
|
if (data.reportSha256 !== ref.reportSha256)
|
|
117
118
|
throw invalidScope("test execution report binding mismatch");
|
|
119
|
+
// The excerpt records the protocol that produced it. A mismatch means the
|
|
120
|
+
// trace and its report disagree, so refuse rather than guess an adapter.
|
|
121
|
+
if (data.scheme !== ref.scheme)
|
|
122
|
+
throw invalidScope("test execution report protocol binding mismatch");
|
|
118
123
|
const coverage = reportCoverage.get(ref.reportPath) ?? { count: 0, total: data.total };
|
|
119
124
|
if (!Number.isInteger(data.total) || data.total < 1 || data.total !== coverage.total || data.offset !== coverage.count || !Array.isArray(data.tests) || !data.tests.length || data.tests.length > 24)
|
|
120
125
|
throw invalidScope("test report excerpt coverage invalid");
|
|
@@ -128,7 +133,10 @@ export async function loadFrontendReviewScopes(runDir, phase, targetBytes, works
|
|
|
128
133
|
const cwdPath = await realpath(path.resolve(workspaceRoot, data.cwd));
|
|
129
134
|
if (cwdPath !== rootPath && !cwdPath.startsWith(rootPath + path.sep))
|
|
130
135
|
throw invalidScope("test report cwd escaped workspace");
|
|
131
|
-
|
|
136
|
+
// Parse with the protocol the runner actually used. Defaulting to
|
|
137
|
+
// Vitest here failed every Playwright/Jest report at review time even
|
|
138
|
+
// though execution and trace had accepted it.
|
|
139
|
+
const actual = parseFrontendTestExecutionReport(JSON.parse(report.text), cwdPath, rootPath, ref.scheme);
|
|
132
140
|
const expected = actual.slice(data.offset, data.offset + 24).map(test => ({ file: test.file, status: test.status, title: test.title.slice(0, 500), ...(test.title.length > 500 ? { titleTruncated: true } : {}) }));
|
|
133
141
|
if (data.total !== actual.length || !expected.length || sha256OfCanonicalJson(expected) !== sha256OfCanonicalJson(data.tests))
|
|
134
142
|
throw invalidScope("test report excerpt differs from actual runner report");
|
|
@@ -3,14 +3,32 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
4
4
|
import { readFile, realpath } from "node:fs/promises";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
+
import { FRONTEND_TEST_ADAPTERS, detectUnadaptedRunner, frontendTestReportSchemeSchema, resolveFrontendTestAdapter, resolveRunnerArgv, } from "./frontend-test-framework-adapters.js";
|
|
7
|
+
/** Scheme → adapter lookup, built once from the adapter registry. */
|
|
8
|
+
const FRONTEND_TEST_ADAPTERS_BY_SCHEME = Object.fromEntries(FRONTEND_TEST_ADAPTERS.map((adapter) => [adapter.scheme, adapter]));
|
|
9
|
+
/**
|
|
10
|
+
* Failure taxonomy for behavior verification. Kept separate from the writer's
|
|
11
|
+
* `failureCategory` so a broken report protocol never reads as broken product
|
|
12
|
+
* code and trigger a pointless implementation rewrite.
|
|
13
|
+
*/
|
|
14
|
+
export const FRONTEND_TEST_OBSERVATION_FAILURES = {
|
|
15
|
+
/** The frozen command drift from what the contract bound. */
|
|
16
|
+
bindingDrift: "FRONTEND_TEST_BINDING_DRIFT",
|
|
17
|
+
/** The runner produced no readable report, or the report was stale. */
|
|
18
|
+
reportProtocol: "FRONTEND_TEST_REPORT_PROTOCOL",
|
|
19
|
+
/** The runner itself is not adapted (Cypress, `node --test`, …). */
|
|
20
|
+
capabilityGap: "FRONTEND_TEST_CAPABILITY_GAP",
|
|
21
|
+
};
|
|
6
22
|
export const frontendTestObservationSchema = z.object({
|
|
7
|
-
scheme:
|
|
23
|
+
scheme: frontendTestReportSchemeSchema,
|
|
8
24
|
commandId: z.string().optional(),
|
|
9
25
|
status: z.enum(["supported", "deferred", "unsupported"]),
|
|
10
26
|
commandSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
11
27
|
cwd: z.string(),
|
|
12
28
|
packageScript: z.object({ file: z.string(), name: z.string(), sha256: z.string().nullable() }).optional(),
|
|
13
29
|
reason: z.string().optional(),
|
|
30
|
+
/** Set when `status: "unsupported"` because no adapter exists yet. */
|
|
31
|
+
capabilityGap: z.string().optional(),
|
|
14
32
|
}).strict();
|
|
15
33
|
const hash = (text) => createHash("sha256").update(text).digest("hex");
|
|
16
34
|
const quote = (text) => `'${text.replace(/'/g, `'"'"'`)}'`;
|
|
@@ -124,20 +142,68 @@ function literalCommand(command, cwd, depth = 0) {
|
|
|
124
142
|
}
|
|
125
143
|
return { argv, cwd, append: suffix => command.trimEnd() + suffix };
|
|
126
144
|
}
|
|
127
|
-
function directVitest(argv) {
|
|
128
|
-
const args = [...argv];
|
|
129
|
-
if (["npx"].includes(args[0] ?? ""))
|
|
130
|
-
args.shift();
|
|
131
|
-
else if (["pnpm", "yarn"].includes(args[0] ?? "") && args[1] === "exec")
|
|
132
|
-
args.splice(0, 2);
|
|
133
|
-
return args[0] === "vitest" && args[1] === "run";
|
|
134
|
-
}
|
|
135
145
|
function hasReporter(argv) { return argv.some(arg => /^--(?:reporter|outputFile)(?:[.=]|$)/.test(arg)); }
|
|
136
|
-
/**
|
|
137
|
-
|
|
146
|
+
/** Locate the `npm|pnpm|yarn` script invocation in a literal argv. */
|
|
147
|
+
function scriptInvocation(argv) {
|
|
148
|
+
const manager = argv[0];
|
|
149
|
+
if (manager !== "npm" && manager !== "pnpm" && manager !== "yarn")
|
|
150
|
+
return undefined;
|
|
151
|
+
// `exec`/`dlx`/`create` run a binary rather than a package script, so they
|
|
152
|
+
// are not script invocations — `yarn exec vitest run` must resolve as a
|
|
153
|
+
// direct runner command, not as a script literally named "exec".
|
|
154
|
+
const subcommands = new Set(["exec", "dlx", "create", "init", "add", "install", "why", "config"]);
|
|
155
|
+
// npm test | npm run <name> | pnpm test | pnpm run <name> | yarn <name>
|
|
156
|
+
let name;
|
|
157
|
+
let rest = [];
|
|
158
|
+
if (argv[1] === "test") {
|
|
159
|
+
name = "test";
|
|
160
|
+
rest = argv.slice(2);
|
|
161
|
+
}
|
|
162
|
+
else if (argv[1] === "run") {
|
|
163
|
+
name = argv[2];
|
|
164
|
+
rest = argv.slice(3);
|
|
165
|
+
}
|
|
166
|
+
else if (manager === "yarn" && argv[1] && !argv[1].startsWith("-") && !subcommands.has(argv[1])) {
|
|
167
|
+
name = argv[1];
|
|
168
|
+
rest = argv.slice(2);
|
|
169
|
+
}
|
|
170
|
+
if (!name || !/^[\w:-]+$/.test(name))
|
|
171
|
+
return undefined;
|
|
172
|
+
if (rest.length && rest[0] !== "--")
|
|
173
|
+
throw Error("package manager script arguments require --");
|
|
174
|
+
return { name, rest };
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Build the report invocation for an adapter.
|
|
178
|
+
*
|
|
179
|
+
* When the adapter names a `reportPathEnv`, the path travels in the environment
|
|
180
|
+
* and the suffix only selects the reporter — appending the path to the command
|
|
181
|
+
* would hand it to the runner as a positional argument (Playwright treats a
|
|
182
|
+
* stray argument as a test-name filter and then finds no tests at all).
|
|
183
|
+
*
|
|
184
|
+
* Otherwise the path is joined to the suffix: a suffix ending in `=` (Vitest's
|
|
185
|
+
* `--outputFile=`) binds it directly, and any other suffix names a flag
|
|
186
|
+
* (`--outputFile`, `--json`) that takes the path as the next argument.
|
|
187
|
+
*/
|
|
188
|
+
function reportInvocation(adapter, append, suffix, reportPath) {
|
|
189
|
+
if (adapter.reportPathEnv) {
|
|
190
|
+
return {
|
|
191
|
+
command: append(suffix),
|
|
192
|
+
env: { [adapter.reportPathEnv]: reportPath },
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const joiner = suffix.endsWith("=") ? "" : " ";
|
|
196
|
+
return { command: append(`${suffix}${joiner}${quote(reportPath)}`) };
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Shared read-only capability resolver, used at generation, admission and
|
|
200
|
+
* immediately before execution. It never executes or rewrites an npm script.
|
|
201
|
+
*/
|
|
138
202
|
export async function inspectFrontendTestCommand(input) {
|
|
139
203
|
const root = path.resolve(input.workspaceRoot ?? input.cwd);
|
|
140
204
|
let cwd = path.resolve(input.cwd);
|
|
205
|
+
// `scheme` is unknowable until an adapter resolves; the placeholder is only
|
|
206
|
+
// ever surfaced on the unsupported path, where no report is produced.
|
|
141
207
|
const base = { scheme: "vitest-json-v1", status: "unsupported", commandSha256: hash(input.command), cwd: path.relative(root, cwd).replace(/\\/g, "/") || "." };
|
|
142
208
|
try {
|
|
143
209
|
const parsed = literalCommand(input.command, cwd);
|
|
@@ -154,79 +220,109 @@ export async function inspectFrontendTestCommand(input) {
|
|
|
154
220
|
throw Error("test cwd resolves outside the workspace");
|
|
155
221
|
if (hasReporter(parsed.argv))
|
|
156
222
|
throw Error("frozen command already owns reporter/outputFile flags");
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
if (!
|
|
162
|
-
throw
|
|
163
|
-
const
|
|
164
|
-
if (
|
|
165
|
-
throw Error(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
throw Error("package.json is unreadable or invalid");
|
|
174
|
-
}
|
|
175
|
-
const script = manifest?.scripts?.[name];
|
|
176
|
-
base.packageScript = { file, name, sha256: typeof script === "string" ? hash(JSON.stringify({ script, pre: manifest.scripts?.[`pre${name}`] ?? null, post: manifest.scripts?.[`post${name}`] ?? null })) : null };
|
|
177
|
-
if (script === undefined) {
|
|
178
|
-
const authorized = input.allowedPaths?.some(pattern => pathMatchesPattern(file, pattern)) && !input.forbiddenPaths?.some(pattern => pathMatchesPattern(file, pattern));
|
|
179
|
-
if (!authorized)
|
|
180
|
-
throw Error(`missing npm script ${name}; ${file} is not an authorized delivery`);
|
|
181
|
-
return { observation: { ...base, status: "deferred", reason: `Awaiting authorized ${file} script ${name} with vitest-json-v1 capability` }, effectiveCwd: cwd };
|
|
182
|
-
}
|
|
183
|
-
if (typeof script !== "string")
|
|
184
|
-
throw Error("npm script must be a string");
|
|
185
|
-
const scriptWords = words(script);
|
|
186
|
-
if (scriptWords.some(word => word.operator) || !directVitest(scriptWords.map(w => w.value)) || hasReporter(scriptWords.map(w => w.value)))
|
|
187
|
-
throw Error("npm script has no supported Vitest JSON reporter capability");
|
|
188
|
-
separator = rest.length ? "" : " --";
|
|
223
|
+
const runner = resolveRunnerArgv(parsed.argv);
|
|
224
|
+
const invocation = scriptInvocation(parsed.argv);
|
|
225
|
+
if (!invocation) {
|
|
226
|
+
const adapter = resolveFrontendTestAdapter({ argv: parsed.argv, runner });
|
|
227
|
+
if (!adapter)
|
|
228
|
+
throw capabilityGapError(runner);
|
|
229
|
+
const suffix = adapter.reportSuffix({ argv: parsed.argv, runner });
|
|
230
|
+
if (suffix === undefined)
|
|
231
|
+
throw Error(`${adapter.runner} project configuration owns report output; declare an explicit regression command instead of overriding it`);
|
|
232
|
+
return { observation: { ...base, scheme: adapter.scheme, status: "supported" }, effectiveCwd: cwd,
|
|
233
|
+
withReporter: reportPath => reportInvocation(adapter, inner => parsed.append(` ${inner}`), suffix, reportPath) };
|
|
234
|
+
}
|
|
235
|
+
const file = path.relative(root, path.join(cwd, "package.json")).replace(/\\/g, "/");
|
|
236
|
+
let manifest = {};
|
|
237
|
+
try {
|
|
238
|
+
manifest = JSON.parse(await readFile(path.join(cwd, "package.json"), "utf8"));
|
|
189
239
|
}
|
|
190
|
-
|
|
191
|
-
|
|
240
|
+
catch (error) {
|
|
241
|
+
if (error.code !== "ENOENT")
|
|
242
|
+
throw Error("package.json is unreadable or invalid");
|
|
243
|
+
}
|
|
244
|
+
const script = manifest?.scripts?.[invocation.name];
|
|
245
|
+
base.packageScript = { file, name: invocation.name, sha256: typeof script === "string" ? hash(JSON.stringify({ script, pre: manifest.scripts?.[`pre${invocation.name}`] ?? null, post: manifest.scripts?.[`post${invocation.name}`] ?? null })) : null };
|
|
246
|
+
if (script === undefined) {
|
|
247
|
+
const authorized = input.allowedPaths?.some(pattern => pathMatchesPattern(file, pattern)) && !input.forbiddenPaths?.some(pattern => pathMatchesPattern(file, pattern));
|
|
248
|
+
if (!authorized)
|
|
249
|
+
throw Error(`missing npm script ${invocation.name}; ${file} is not an authorized delivery`);
|
|
250
|
+
return { observation: { ...base, status: "deferred", reason: `Awaiting authorized ${file} script ${invocation.name} with a supported test report capability` }, effectiveCwd: cwd };
|
|
251
|
+
}
|
|
252
|
+
if (typeof script !== "string")
|
|
253
|
+
throw Error("npm script must be a string");
|
|
254
|
+
const scriptWords = words(script);
|
|
255
|
+
if (scriptWords.some(word => word.operator))
|
|
256
|
+
throw Error("npm script must be a single literal command");
|
|
257
|
+
const scriptArgv = scriptWords.map(w => w.value);
|
|
258
|
+
if (hasReporter(scriptArgv))
|
|
259
|
+
throw Error("npm script already owns reporter/outputFile flags");
|
|
260
|
+
const scriptRunner = resolveRunnerArgv(scriptArgv);
|
|
261
|
+
const adapter = resolveFrontendTestAdapter({ argv: parsed.argv, runner: scriptRunner, scriptName: invocation.name, scriptArgv });
|
|
262
|
+
if (!adapter)
|
|
263
|
+
throw capabilityGapError(scriptRunner);
|
|
264
|
+
const suffix = adapter.reportSuffix({ argv: scriptArgv, runner: scriptRunner });
|
|
265
|
+
if (suffix === undefined)
|
|
266
|
+
throw Error(`${adapter.runner} project configuration owns report output; declare an explicit regression command instead of overriding it`);
|
|
267
|
+
// `npm test -- --json` forwards the flags to the script; a bare script
|
|
268
|
+
// uses the same `--` separator so npm never eats the report flags.
|
|
269
|
+
// Space-prefixed because `append` concatenates onto the trimmed
|
|
270
|
+
// command; a bare script needs `--` so npm forwards the report flags.
|
|
271
|
+
const separator = invocation.rest.length ? "" : " -- ";
|
|
272
|
+
return { observation: { ...base, scheme: adapter.scheme, status: "supported" }, effectiveCwd: cwd,
|
|
273
|
+
withReporter: reportPath => reportInvocation(adapter, inner => parsed.append(invocation.rest.length ? ` ${inner}` : `${separator}${inner}`), suffix, reportPath) };
|
|
192
274
|
}
|
|
193
275
|
catch (error) {
|
|
194
|
-
|
|
276
|
+
const gap = error.capabilityGap;
|
|
277
|
+
return { observation: { ...base, status: "unsupported", reason: error instanceof Error ? error.message : String(error), ...(gap ? { capabilityGap: gap } : {}) }, effectiveCwd: cwd };
|
|
195
278
|
}
|
|
196
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* An unadapted runner is a capability gap in this tool, not a user mistake: the
|
|
282
|
+
* message must never suggest switching frameworks.
|
|
283
|
+
*/
|
|
284
|
+
function capabilityGapError(runner) {
|
|
285
|
+
const known = detectUnadaptedRunner(runner);
|
|
286
|
+
const binary = known ?? (runner[0] ?? "unknown");
|
|
287
|
+
const error = new Error(`${FRONTEND_TEST_OBSERVATION_FAILURES.capabilityGap}: no test report adapter for ${binary}; ` +
|
|
288
|
+
`frontend behavior verification currently adapts vitest, jest and playwright test, ` +
|
|
289
|
+
`so this command cannot produce execution evidence`);
|
|
290
|
+
error.capabilityGap = binary;
|
|
291
|
+
return error;
|
|
292
|
+
}
|
|
197
293
|
export function compareFrontendTestObservation(frozen, observed, phase, writeSet = []) {
|
|
198
294
|
if (frozen.status === "unsupported")
|
|
199
295
|
return frozen.reason ?? "unsupported test observation scheme";
|
|
200
296
|
if (observed.status === "unsupported")
|
|
201
297
|
return observed.reason ?? "unsupported test observation scheme";
|
|
202
|
-
|
|
203
|
-
|
|
298
|
+
// A `deferred` observation has no resolved adapter, so its `scheme` is the
|
|
299
|
+
// placeholder rather than a binding. Comparing it would reject every new
|
|
300
|
+
// Jest/Playwright project the moment the writer adds the very script the
|
|
301
|
+
// contract asked for. The scheme only becomes a binding once the frozen
|
|
302
|
+
// observation is `supported`; script identity is still checked below.
|
|
303
|
+
const schemeBound = frozen.status !== "deferred";
|
|
304
|
+
if ((schemeBound && frozen.scheme !== observed.scheme) || frozen.commandSha256 !== observed.commandSha256 || frozen.cwd !== observed.cwd || frozen.packageScript?.file !== observed.packageScript?.file || frozen.packageScript?.name !== observed.packageScript?.name)
|
|
305
|
+
return `${FRONTEND_TEST_OBSERVATION_FAILURES.bindingDrift}: frozen test command/cwd/script binding changed`;
|
|
204
306
|
if (frozen.status === "supported" && (observed.status !== "supported" || frozen.packageScript?.sha256 !== observed.packageScript?.sha256))
|
|
205
|
-
return
|
|
307
|
+
return `${FRONTEND_TEST_OBSERVATION_FAILURES.bindingDrift}: frozen npm test script changed`;
|
|
206
308
|
if (frozen.status === "deferred" && phase === "admission" && (!frozen.packageScript || !writeSet.includes(frozen.packageScript.file)))
|
|
207
309
|
return "future test script is outside the admitted concrete writeSet";
|
|
208
310
|
if (phase === "execution" && observed.status !== "supported")
|
|
209
311
|
return "future test reporter capability is still unavailable";
|
|
210
312
|
return undefined;
|
|
211
313
|
}
|
|
212
|
-
/** Observe supported frozen
|
|
314
|
+
/** Observe supported frozen commands without changing their selection. */
|
|
213
315
|
export async function withFrontendTestReporter(input) {
|
|
214
|
-
return (await inspectFrontendTestCommand(input)).withReporter?.(input.reportPath);
|
|
316
|
+
return (await inspectFrontendTestCommand(input)).withReporter?.(input.reportPath)?.command;
|
|
215
317
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
export function parseFrontendTestExecutionReport(raw, cwd, workspaceRoot = cwd) {
|
|
227
|
-
return resultSchema.parse(raw).testResults.flatMap((suite) => suite.assertionResults.map((test) => ({
|
|
228
|
-
file: path.relative(workspaceRoot, path.resolve(cwd, suite.name)).replace(/\\/g, "/"),
|
|
229
|
-
title: test.fullName ?? [...(test.ancestorTitles ?? []), test.title].join(" "),
|
|
230
|
-
status: test.status,
|
|
231
|
-
})));
|
|
318
|
+
/**
|
|
319
|
+
* Parse a runner report into uniform execution records using the adapter for
|
|
320
|
+
* `scheme`. A malformed report throws; callers classify that as a report
|
|
321
|
+
* protocol failure, never as a product defect.
|
|
322
|
+
*/
|
|
323
|
+
export function parseFrontendTestExecutionReport(raw, cwd, workspaceRoot = cwd, scheme = "vitest-json-v1") {
|
|
324
|
+
const adapter = FRONTEND_TEST_ADAPTERS_BY_SCHEME[scheme];
|
|
325
|
+
if (!adapter)
|
|
326
|
+
throw Error(`no test report adapter for scheme ${scheme}`);
|
|
327
|
+
return adapter.parse(raw, cwd, workspaceRoot);
|
|
232
328
|
}
|