@tea-agent/loop-agent 0.44.0-next.11 → 0.44.0-next.13
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 +25 -1
- package/dist/build-stamp.json +3 -3
- package/dist/commands/init.js +1 -0
- package/dist/executors/dag-pi-executor.js +11 -6
- package/dist/executors/pi-writer-tool-policy.js +60 -4
- package/dist/executors/playwright-cli-launcher.js +31 -45
- package/dist/executors/shell-executor.js +39 -11
- package/dist/executors/shell-write-guard.js +24 -67
- package/dist/governance/exec-plans.js +10 -1
- package/dist/task/config-types.js +4 -2
- package/dist/task/source-prepare/build-draft.js +1 -0
- package/dist/task/source-prepare/path-policy.js +17 -7
- package/dist/worker/console/chat/browser-automation.js +343 -38
- package/dist/worker/console/chat/browser-content.js +74 -0
- package/dist/worker/console/chat/browser-diagnostics.js +250 -0
- package/dist/worker/console/chat/browser-evaluation.js +58 -0
- package/dist/worker/console/chat/browser-operation-queue.js +19 -0
- package/dist/worker/console/chat/browser-pick.js +113 -0
- package/dist/worker/console/chat/browser-routes.js +33 -5
- package/dist/worker/console/chat/browser-screenshot-result.js +47 -0
- package/dist/worker/console/chat/browser-storage.js +89 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +125 -174
- package/dist/worker/console/chat/pi-runtime.js +1 -1
- package/dist/worker/console/chat/repo-browser.js +80 -2
- package/dist/worker/console/chat/repo-preview-kind.js +155 -0
- package/dist/worker/console/chat/session-mode.js +1 -1
- package/dist/worker/console/chat/session-store.js +4 -2
- package/dist/worker/console/chat/tools.js +12 -0
- package/dist/worker/console/routes.js +5 -0
- package/dist/worker/console/static/app-icon.svg +39 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-r1NAPU_5.js → abnfDiagram-N423BO3Z-BA7qCL_r.js} +1 -1
- package/dist/worker/console/static/assets/{arc-dT5T4sVL.js → arc-Df09ajwL.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CP_cOq46.js → architectureDiagram-T3A2C74G-CpF9sw5R.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-sToEZxEf.js → blockDiagram-VBNYF7ZC-BtPys8ja.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DSxwwi2m.js → c4Diagram-5PPSVZJV-DZPnTWqT.js} +1 -1
- package/dist/worker/console/static/assets/channel-DiTNLZUm.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B9fh0IkF.js → chunk-2GRJ4B5K-BD9D4h1p.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CAUNl-A6.js → chunk-2Q5K7J3B-Svn8yBmn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BomeqbNX.js → chunk-5RXB4S5H-B8EbQj-S.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BNAuTOLE.js → chunk-5VM5RSS4-tENZpcpF.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DfzjRD5y.js → chunk-6Q2QTUOP-DBx71Lco.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C3uE-yc2.js → chunk-GF5L2VYU-1v_QamMz.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-C3sQMMYr.js → chunk-JWPE2WC7-BAMCNNBG.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D_AtEYQL.js → chunk-KBJHAD2P-BvuEury-.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DW4TwhKv.js → chunk-RYQCIY6F-YqFuHp8s.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CTQDmJcp.js → chunk-XXDRQBXY-ouI5T7Mr.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DgL8aiyZ.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DgL8aiyZ.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-iMqPuBsL.js → cose-bilkent-JH36ORCC-Bri0_Izk.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DktIuDWs.js → cynefin-VYW2F7L2-e8Vhhi4X.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BgVnjyEs.js → cynefinDiagram-MW4NZA55-tSIXGQiJ.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CR25E4bu.js → dagre-VZM6K2ZE-Rl-sF5pt.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CPM2h5HY.js → diagram-7IWD3JNH-Db6ddPz8.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DqF0x2g1.js → diagram-B4RE2ZJO-Jwd_Xw5S.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-G8BYp2eb.js → diagram-LBJQPF4R-CeC_8InC.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BbC4kT_D.js → diagram-Q27KOJAE-LKPRaNq8.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-CKZwZ-G5.js → diagram-UB23O5K3-DlVQytMW.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-kJDmheia.js → ebnfDiagram-BXEA7PRR-DnfJZwO-.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DFTVHyxB.js → erDiagram-JOGREHBK-3axVSXca.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BqdN5_os.js → flowDiagram-UKHOOZJN-xe04-_L-.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-SYDjVakh.js → ganttDiagram-PKOTCBZU-hpmRINiD.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js → gitGraphDiagram-DS77QQ5N-6HSNz9Ko.js} +1 -1
- package/dist/worker/console/static/assets/index-C_8_gWal.css +1 -0
- package/dist/worker/console/static/assets/{index-CTMZsDmh.js → index-DybcVXCu.js} +102 -100
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DIeAmJ45.js → infoDiagram-6WML65LV-BF6ksWTX.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-oikqUAlo.js → ishikawaDiagram-WSZJBQD7-DXisdeD7.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-mKEF_xYT.js → journeyDiagram-NVQOT4AX-BWu65wmu.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BdWJHPnN.js → kanban-definition-27J2QSJJ-hfNhHqAI.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DGRlieMH.js → linear-D6HTfQcp.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-4P9yPC9c.js → mermaid.core-Iv91R1WP.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-5PTSkos6.js → mindmap-definition-FAOFIHXS-CfPChwjR.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DONIFpG5.js → pegDiagram-VL7TDLO6-B7TcoqHz.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-D_Z4S70M.js → pieDiagram-7S7Q4E2Y-ouyPKMtQ.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CAsjh8iY.js → quadrantDiagram-CIZ2JOQS-B9tuyLbh.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Cx4Dd6ac.js → railroadDiagram-AXF67PYL-Ck4q9nJs.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-cQb7mwyH.js → requirementDiagram-LRYGKXZP-wXLX5Dnp.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cd-DhUdf.js → sankeyDiagram-W5VNT64P-D2XWw6_m.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-AlKYRgIk.js → sequenceDiagram-SI44F4Z6-DzxKiug4.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CUy_ET4e.js → sizeCapture-X5ZJPWSS-DWUqK5Us.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CTuVe6Sa.js → stateDiagram-OKZ733FA-D5Pa4O5g.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-C0QIbqO4.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-ChFLJb-v.js → swimlanes-SLNWSIFB-VH7cRnsJ.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BB9CzRHB.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DZZeiAPr.js → timeline-definition-Z64GVDOM-C4HM5NoZ.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-t4O91cZU.js → vennDiagram-T6HMQDX7-Bu-4hOlA.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CV5qGHZu.js → wardleyDiagram-T6FBY63Y-CasFEvvl.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DyXbytUI.js → xychartDiagram-ELKLHX3M-D-gpwPSA.js} +1 -1
- package/dist/worker/console/static/index.html +5 -2
- package/dist/worker/console/static/manifest.webmanifest +19 -0
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +57 -9
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +6 -0
- package/dist/worker/console/workspace-context.js +1 -0
- package/dist/worker/materialize/harness-task-lineage.js +4 -2
- package/dist/worker/observe/static/console-theme.css +33 -29
- package/dist/worker/observe/static/console-theme.js +38 -0
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +3 -1
- package/dist/workflows/dag/backend-test-layout.js +27 -7
- package/dist/workflows/dag/backend-test-markdown-workflow.js +3 -1
- 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-case-checklist.js +12 -3
- package/dist/workflows/dag/frontend-test-environment-probe.js +0 -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/frontend-test-layout.js +41 -12
- package/dist/workflows/dag/hybrid/sources.js +21 -2
- package/dist/workflows/dag/hybrid/templates/backend-test.js +65 -50
- package/dist/workflows/dag/hybrid/templates/frontend-test.js +58 -37
- package/dist/workflows/dag/test-artifact-namespace.js +73 -0
- package/dist/workflows/dag/validate.js +22 -3
- package/docs/architecture/dag-execution.md +3 -3
- package/docs/templates/README.md +4 -4
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +3 -3
- package/docs/templates/backend-test-dag.json +708 -708
- package/docs/templates/backend-test-dag.review-cases.prompt.md +3 -3
- package/docs/templates/frontend-test-case-checklist.md +2 -2
- package/docs/templates/frontend-test-dag.generate-cases.prompt.md +7 -7
- package/docs/templates/frontend-test-dag.json +46 -37
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +2 -2
- package/docs/templates/frontend-test-dag.retrospect.prompt.md +1 -1
- package/docs/templates/frontend-test-dag.review-cases.prompt.md +1 -1
- package/package.json +7 -2
- package/skills/fe-test-ui-scout/SKILL.md +4 -4
- package/skills/fe-test-ui-scout/references/ledger-schema.md +1 -1
- package/skills/playwright-cli/SKILL.md +1 -1
- package/skills/playwright-cli-case-generator/SKILL.md +4 -4
- package/dist/worker/console/static/assets/channel-CyBY_yGk.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +0 -1
- package/dist/worker/console/static/assets/index-B28Onmfy.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +0 -8
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
/**
|
|
5
|
+
* Framework adapters decouple the frontend behavior-verification contract from
|
|
6
|
+
* any single test runner.
|
|
7
|
+
*
|
|
8
|
+
* The contract, writer admission and execution all need three things from a
|
|
9
|
+
* frozen verify command:
|
|
10
|
+
* 1. whether the runner can emit a machine-readable execution report at all
|
|
11
|
+
* (capability), so an unsupported runner fails closed *before* implement;
|
|
12
|
+
* 2. how to ask that runner for a report without clobbering the project's
|
|
13
|
+
* own reporter configuration;
|
|
14
|
+
* 3. how to read that report back as uniform file/title/status triples.
|
|
15
|
+
*
|
|
16
|
+
* Historically all three were hardcoded to Vitest (`vitest-json-v1`), while the
|
|
17
|
+
* command classifier was already framework-agnostic. Jest and Playwright
|
|
18
|
+
* commands were therefore classified into the behavior lane correctly and then
|
|
19
|
+
* rejected as "unsupported", surfacing a capability gap to the user as if they
|
|
20
|
+
* had misconfigured their project. Adapters make the two sources of truth agree.
|
|
21
|
+
*/
|
|
22
|
+
/** Report protocols a frozen behavior command may be observed through. */
|
|
23
|
+
export const frontendTestReportSchemeSchema = z.enum([
|
|
24
|
+
"vitest-json-v1",
|
|
25
|
+
"jest-json-v1",
|
|
26
|
+
"playwright-json-v1",
|
|
27
|
+
]);
|
|
28
|
+
/** Runtime guard for a persisted scheme, e.g. read back from a trace artifact. */
|
|
29
|
+
export const isFrontendTestReportScheme = (value) => typeof value === "string" &&
|
|
30
|
+
frontendTestReportSchemeSchema.options.includes(value);
|
|
31
|
+
const asRecord = (value) => typeof value === "object" && value !== null
|
|
32
|
+
? value
|
|
33
|
+
: undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Resolve a runner-relative path into contract workspace coordinates.
|
|
36
|
+
*
|
|
37
|
+
* A runner may report an absolute path (`config.rootDir` is absolute in a
|
|
38
|
+
* Playwright report) or one relative to its own root. Either way the result is
|
|
39
|
+
* expressed against `workspaceRoot`, which is resolved physically first: a
|
|
40
|
+
* workspace reached through a symlink (macOS `/var` -> `/private/var`, i.e. the
|
|
41
|
+
* default `os.tmpdir()`) is never a lexical prefix of the runner's canonical
|
|
42
|
+
* path, so comparing raw strings would yield a `../..` escape instead of a
|
|
43
|
+
* contract-relative file.
|
|
44
|
+
*/
|
|
45
|
+
const toWorkspacePath = (raw, cwd, workspaceRoot) => {
|
|
46
|
+
const absolute = path.resolve(cwd, raw);
|
|
47
|
+
const physical = (target) => {
|
|
48
|
+
try {
|
|
49
|
+
return realpathSync(target);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return target;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const root = physical(path.resolve(workspaceRoot));
|
|
56
|
+
// The path may itself sit under the unresolved root; re-anchor it physically
|
|
57
|
+
// by resolving its root-relative tail against the physical root.
|
|
58
|
+
const anchored = absolute.startsWith(path.resolve(workspaceRoot) + path.sep)
|
|
59
|
+
? path.join(root, path.relative(path.resolve(workspaceRoot), absolute))
|
|
60
|
+
: absolute;
|
|
61
|
+
return path.relative(root, physical(anchored)).replace(/\\/g, "/");
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Strip package-manager indirection so `npx vitest run`, `pnpm exec vitest
|
|
65
|
+
* run`, `yarn exec vitest run` and a bare `vitest run` all reduce to the same
|
|
66
|
+
* runner invocation. Returns the remaining words.
|
|
67
|
+
*/
|
|
68
|
+
export function resolveRunnerArgv(argv) {
|
|
69
|
+
const words = [...argv];
|
|
70
|
+
// Skip leading env assignments (`CI=1 vitest run`).
|
|
71
|
+
while (words.length > 0 && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0])) {
|
|
72
|
+
words.shift();
|
|
73
|
+
}
|
|
74
|
+
const head = words[0];
|
|
75
|
+
if (head === "npx" || head === "pnpx" || head === "bunx") {
|
|
76
|
+
words.shift();
|
|
77
|
+
// `npx --no-install vitest run` / `npx -y vitest run`
|
|
78
|
+
while (words[0]?.startsWith("-"))
|
|
79
|
+
words.shift();
|
|
80
|
+
return words;
|
|
81
|
+
}
|
|
82
|
+
if ((head === "pnpm" || head === "yarn" || head === "npm") && words[1] === "exec") {
|
|
83
|
+
words.splice(0, 2);
|
|
84
|
+
while (words[0]?.startsWith("-"))
|
|
85
|
+
words.shift();
|
|
86
|
+
return words;
|
|
87
|
+
}
|
|
88
|
+
return words;
|
|
89
|
+
}
|
|
90
|
+
/** Runner binaries each adapter claims, keyed by adapter scheme. */
|
|
91
|
+
const VITEST_BINARIES = new Set(["vitest"]);
|
|
92
|
+
const JEST_BINARIES = new Set(["jest"]);
|
|
93
|
+
const PLAYWRIGHT_BINARIES = new Set(["playwright"]);
|
|
94
|
+
const basenameOf = (value) => value.split(/[\\/]/).pop() ?? "";
|
|
95
|
+
/** True when `runner` invokes `binary` directly (allowing an explicit path). */
|
|
96
|
+
const invokesBinary = (runner, binaries) => binaries.has(basenameOf(runner[0] ?? ""));
|
|
97
|
+
const vitestAdapter = {
|
|
98
|
+
scheme: "vitest-json-v1",
|
|
99
|
+
runner: "vitest",
|
|
100
|
+
matches: ({ runner }) => invokesBinary(runner, VITEST_BINARIES),
|
|
101
|
+
// Vitest's json reporter writes to stdout unless an output file is given,
|
|
102
|
+
// and the executor reads the report from disk, so both flags are required.
|
|
103
|
+
reportSuffix: () => `--reporter=json --outputFile=`,
|
|
104
|
+
parse: (raw, cwd, workspaceRoot) => {
|
|
105
|
+
const schema = z.object({
|
|
106
|
+
testResults: z.array(z.object({
|
|
107
|
+
name: z.string().min(1),
|
|
108
|
+
assertionResults: z.array(z.object({
|
|
109
|
+
title: z.string(),
|
|
110
|
+
fullName: z.string().optional(),
|
|
111
|
+
ancestorTitles: z.array(z.string()).optional(),
|
|
112
|
+
status: z.string(),
|
|
113
|
+
})),
|
|
114
|
+
})),
|
|
115
|
+
});
|
|
116
|
+
return schema
|
|
117
|
+
.parse(raw)
|
|
118
|
+
.testResults.flatMap((suite) => suite.assertionResults.map((test) => ({
|
|
119
|
+
file: toWorkspacePath(suite.name, cwd, workspaceRoot),
|
|
120
|
+
title: test.fullName ??
|
|
121
|
+
[...(test.ancestorTitles ?? []), test.title].join(" "),
|
|
122
|
+
status: test.status,
|
|
123
|
+
})));
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
const jestAdapter = {
|
|
127
|
+
scheme: "jest-json-v1",
|
|
128
|
+
runner: "jest",
|
|
129
|
+
matches: ({ runner }) => invokesBinary(runner, JEST_BINARIES),
|
|
130
|
+
reportSuffix: () => `--json --outputFile`,
|
|
131
|
+
parse: (raw, cwd, workspaceRoot) => {
|
|
132
|
+
// Jest's `--json` shape: testResults[] carry a per-file `name` plus
|
|
133
|
+
// assertionResults[] that mirror Vitest's field names.
|
|
134
|
+
const schema = z.object({
|
|
135
|
+
testResults: z.array(z.object({
|
|
136
|
+
name: z.string().min(1),
|
|
137
|
+
assertionResults: z.array(z.object({
|
|
138
|
+
title: z.string(),
|
|
139
|
+
fullName: z.string().optional(),
|
|
140
|
+
ancestorTitles: z.array(z.string()).optional(),
|
|
141
|
+
status: z.string(),
|
|
142
|
+
})),
|
|
143
|
+
})),
|
|
144
|
+
});
|
|
145
|
+
return schema
|
|
146
|
+
.parse(raw)
|
|
147
|
+
.testResults.flatMap((suite) => suite.assertionResults.map((test) => ({
|
|
148
|
+
file: toWorkspacePath(suite.name, cwd, workspaceRoot),
|
|
149
|
+
title: test.fullName ??
|
|
150
|
+
[...(test.ancestorTitles ?? []), test.title].join(" "),
|
|
151
|
+
status: test.status,
|
|
152
|
+
})));
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Playwright's JSON reporter emits a nested `suites` tree whose leaf `specs`
|
|
157
|
+
* carry `tests[].results[].status`. File paths live on the enclosing `file`
|
|
158
|
+
* field of each suite node, so the tree is walked while tracking it.
|
|
159
|
+
*/
|
|
160
|
+
const playwrightAdapter = {
|
|
161
|
+
scheme: "playwright-json-v1",
|
|
162
|
+
runner: "playwright",
|
|
163
|
+
matches: ({ runner }) => {
|
|
164
|
+
if (!invokesBinary(runner, PLAYWRIGHT_BINARIES))
|
|
165
|
+
return false;
|
|
166
|
+
// `playwright test` runs tests; `playwright install`/`codegen` do not.
|
|
167
|
+
return runner[1] === "test" || runner[1] === undefined;
|
|
168
|
+
},
|
|
169
|
+
reportSuffix: () => `--reporter=json`,
|
|
170
|
+
// Playwright ignores an output filename on the CLI (`--output` is its
|
|
171
|
+
// artifacts directory); its JSON reporter goes to stdout unless this
|
|
172
|
+
// variable names a file.
|
|
173
|
+
reportPathEnv: "PLAYWRIGHT_JSON_OUTPUT_NAME",
|
|
174
|
+
parse: (raw, cwd, workspaceRoot) => {
|
|
175
|
+
const suiteSchema = z.lazy(() => z.object({
|
|
176
|
+
file: z.string().optional(),
|
|
177
|
+
title: z.string().optional(),
|
|
178
|
+
specs: z
|
|
179
|
+
.array(z.object({
|
|
180
|
+
title: z.string(),
|
|
181
|
+
ok: z.boolean(),
|
|
182
|
+
tests: z.array(z.object({
|
|
183
|
+
status: z.string().optional(),
|
|
184
|
+
results: z
|
|
185
|
+
.array(z.object({
|
|
186
|
+
status: z.string(),
|
|
187
|
+
}))
|
|
188
|
+
.optional(),
|
|
189
|
+
})),
|
|
190
|
+
}))
|
|
191
|
+
.optional(),
|
|
192
|
+
suites: z.array(suiteSchema).optional(),
|
|
193
|
+
}));
|
|
194
|
+
const root = z
|
|
195
|
+
.object({
|
|
196
|
+
suites: z.array(suiteSchema).optional(),
|
|
197
|
+
// Playwright reports each suite's `file` relative to the resolved
|
|
198
|
+
// `testDir` (`config.rootDir`), not to the process cwd. A project
|
|
199
|
+
// with `testDir: "./tests"` reports `orders.spec.js` for
|
|
200
|
+
// `tests/orders.spec.js`, so resolving against cwd would produce a
|
|
201
|
+
// path that can never match a contract target.
|
|
202
|
+
config: z
|
|
203
|
+
.object({ rootDir: z.string().optional() })
|
|
204
|
+
.optional(),
|
|
205
|
+
})
|
|
206
|
+
.parse(raw);
|
|
207
|
+
// Prefer the runner's own reported root; fall back to cwd only when the
|
|
208
|
+
// report omitted it (older report shapes).
|
|
209
|
+
const reportedRoot = root.config?.rootDir;
|
|
210
|
+
const baseCwd = typeof reportedRoot === "string" && reportedRoot.length > 0
|
|
211
|
+
? reportedRoot
|
|
212
|
+
: cwd;
|
|
213
|
+
const out = [];
|
|
214
|
+
const walk = (node, inheritedFile) => {
|
|
215
|
+
const record = asRecord(node);
|
|
216
|
+
if (!record)
|
|
217
|
+
return;
|
|
218
|
+
const file = typeof record.file === "string" ? record.file : inheritedFile;
|
|
219
|
+
for (const spec of record.specs ?? []) {
|
|
220
|
+
const specRecord = asRecord(spec);
|
|
221
|
+
if (!specRecord)
|
|
222
|
+
continue;
|
|
223
|
+
const title = String(specRecord.title ?? "");
|
|
224
|
+
const tests = specRecord.tests ?? [];
|
|
225
|
+
// A spec with several projects/retries yields several tests; the
|
|
226
|
+
// spec only passes when every attempt passed.
|
|
227
|
+
const statuses = tests.flatMap((test) => {
|
|
228
|
+
const testRecord = asRecord(test);
|
|
229
|
+
const results = testRecord?.results ?? [];
|
|
230
|
+
const attempts = results
|
|
231
|
+
.map((result) => asRecord(result)?.status)
|
|
232
|
+
.filter((value) => typeof value === "string");
|
|
233
|
+
return attempts.length > 0
|
|
234
|
+
? attempts
|
|
235
|
+
: [String(testRecord?.status ?? "unknown")];
|
|
236
|
+
});
|
|
237
|
+
const status = statuses.every((value) => value === "passed")
|
|
238
|
+
? "passed"
|
|
239
|
+
: statuses.includes("passed")
|
|
240
|
+
? "failed"
|
|
241
|
+
: (statuses[0] ?? "unknown");
|
|
242
|
+
out.push({
|
|
243
|
+
file: toWorkspacePath(file ?? "", baseCwd, workspaceRoot),
|
|
244
|
+
title,
|
|
245
|
+
status,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
for (const child of record.suites ?? []) {
|
|
249
|
+
walk(child, file);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
for (const suite of root.suites ?? [])
|
|
253
|
+
walk(suite, undefined);
|
|
254
|
+
return out;
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
export const FRONTEND_TEST_ADAPTERS = [
|
|
258
|
+
vitestAdapter,
|
|
259
|
+
jestAdapter,
|
|
260
|
+
playwrightAdapter,
|
|
261
|
+
];
|
|
262
|
+
/**
|
|
263
|
+
* Runners the container classifier recognizes as behavior verification but
|
|
264
|
+
* that have no adapter yet. Naming them lets the capability gap read as
|
|
265
|
+
* "framework not adapted yet" instead of a user configuration error.
|
|
266
|
+
*/
|
|
267
|
+
const KNOWN_UNADAPTED_RUNNERS = new Set([
|
|
268
|
+
"mocha",
|
|
269
|
+
"ava",
|
|
270
|
+
"tap",
|
|
271
|
+
"cypress",
|
|
272
|
+
"testcafe",
|
|
273
|
+
"nightwatch",
|
|
274
|
+
"wdio",
|
|
275
|
+
"webdriverio",
|
|
276
|
+
]);
|
|
277
|
+
/** Resolve the adapter for a command, or `undefined` when none matches. */
|
|
278
|
+
export function resolveFrontendTestAdapter(context) {
|
|
279
|
+
// A package script owns the detail: `npm test` must resolve through the
|
|
280
|
+
// script body, not the wrapper, or `npm test` and `vitest run` would
|
|
281
|
+
// disagree about capability.
|
|
282
|
+
if (context.scriptArgv) {
|
|
283
|
+
const scriptRunner = resolveRunnerArgv(context.scriptArgv);
|
|
284
|
+
const fromScript = FRONTEND_TEST_ADAPTERS.find((adapter) => adapter.matches({ ...context, runner: scriptRunner }));
|
|
285
|
+
if (fromScript)
|
|
286
|
+
return fromScript;
|
|
287
|
+
}
|
|
288
|
+
return FRONTEND_TEST_ADAPTERS.find((adapter) => adapter.matches(context));
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Detect a recognized-but-unadapted runner so callers can report a capability
|
|
292
|
+
* gap. Returns the runner name when the command is a test runner we know of
|
|
293
|
+
* but cannot observe.
|
|
294
|
+
*/
|
|
295
|
+
export function detectUnadaptedRunner(runner) {
|
|
296
|
+
const binary = basenameOf(runner[0] ?? "").replace(/\.(?:js|cjs|mjs|cmd|exe)$/u, "");
|
|
297
|
+
if (KNOWN_UNADAPTED_RUNNERS.has(binary))
|
|
298
|
+
return binary;
|
|
299
|
+
// `node --test`, `node --test-only`, plus `deno test`/`bun test`.
|
|
300
|
+
if (binary === "node" && runner.includes("--test"))
|
|
301
|
+
return "node --test";
|
|
302
|
+
if (binary === "deno" && runner[1] === "test")
|
|
303
|
+
return "deno test";
|
|
304
|
+
if (binary === "bun" && runner[1] === "test")
|
|
305
|
+
return "bun test";
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
/** Empty `file` means the runner reported a spec outside the workspace. */
|
|
309
|
+
export const isOutsideWorkspacePath = (file) => file === ".." || file.startsWith("../") || path.isAbsolute(file);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { canonicalTestArtifactRoot, } from "./test-artifact-namespace.js";
|
|
2
3
|
/**
|
|
3
4
|
* Frontend-test artifact layout (configurable testRoot).
|
|
4
5
|
*
|
|
@@ -6,14 +7,16 @@ import path from "node:path";
|
|
|
6
7
|
* frozen on the spec for the whole run. Writers, gates, Playwright fences
|
|
7
8
|
* and TaskSpec allowedPaths all derive from this single object.
|
|
8
9
|
*
|
|
9
|
-
* Default layout
|
|
10
|
-
* testcase/frontend/rag/**
|
|
11
|
-
* testcase/frontend/cases/**
|
|
12
|
-
* testcase/frontend/evidence/**
|
|
13
|
-
* testcase/frontend/reports/**
|
|
14
|
-
* testcase/frontend/fixtures/**
|
|
10
|
+
* Default layout (since 2026-09-17) lives under the shared runtime namespace:
|
|
11
|
+
* .harness/testcase/frontend/rag/**
|
|
12
|
+
* .harness/testcase/frontend/cases/**
|
|
13
|
+
* .harness/testcase/frontend/evidence/**
|
|
14
|
+
* .harness/testcase/frontend/reports/**
|
|
15
|
+
* .harness/testcase/frontend/fixtures/**
|
|
15
16
|
*/
|
|
16
|
-
export const FRONTEND_TEST_DEFAULT_TEST_ROOT = "
|
|
17
|
+
export const FRONTEND_TEST_DEFAULT_TEST_ROOT = canonicalTestArtifactRoot("frontend-test");
|
|
18
|
+
/** Historical pre-2026-09-17 default root, kept for text rewrites only. */
|
|
19
|
+
export const FRONTEND_TEST_LEGACY_DEFAULT_TEST_ROOT = "testcase/frontend";
|
|
17
20
|
const FORBIDDEN_TEST_ROOT_PREFIXES = [
|
|
18
21
|
"src",
|
|
19
22
|
"app",
|
|
@@ -59,6 +62,10 @@ function normalizeRelPosix(value, label) {
|
|
|
59
62
|
return segments.join("/");
|
|
60
63
|
}
|
|
61
64
|
function assertTestRootAllowed(testRoot) {
|
|
65
|
+
// D3: the canonical harness test-artifact root is the ONLY hidden-root
|
|
66
|
+
// exception. Every other hidden or forbidden top segment stays rejected.
|
|
67
|
+
if (testRoot === FRONTEND_TEST_DEFAULT_TEST_ROOT)
|
|
68
|
+
return;
|
|
62
69
|
const topSegment = testRoot.split("/")[0];
|
|
63
70
|
if (FORBIDDEN_TEST_ROOT_PREFIXES.some((forbidden) => topSegment === forbidden || topSegment.startsWith(`${forbidden}.`))) {
|
|
64
71
|
throw new Error(`frontendTest.testRoot must not target production/governance trees: ${testRoot}`);
|
|
@@ -67,14 +74,25 @@ function assertTestRootAllowed(testRoot) {
|
|
|
67
74
|
throw new Error(`frontendTest.testRoot must not target hidden directories: ${testRoot}`);
|
|
68
75
|
}
|
|
69
76
|
}
|
|
77
|
+
function resolveTestRootInput(raw, canonical) {
|
|
78
|
+
// D3: only the EXACT canonical harness test-artifact root may be a hidden
|
|
79
|
+
// path. Compare after trimming + stripping trailing slashes so
|
|
80
|
+
// `.harness/testcase/frontend/` matches, but every other hidden spelling
|
|
81
|
+
// falls through to the fail-closed normalizer below.
|
|
82
|
+
const trimmed = raw.trim().replace(/\/+$/, "");
|
|
83
|
+
if (trimmed === canonical)
|
|
84
|
+
return canonical;
|
|
85
|
+
return normalizeRelPosix(raw, "testRoot");
|
|
86
|
+
}
|
|
70
87
|
/**
|
|
71
88
|
* Resolve the frozen frontend-test artifact layout from task config.
|
|
72
|
-
*
|
|
89
|
+
* Roots of the form `.harness/testcase/frontend` are accepted only as the
|
|
90
|
+
* exact canonical default; all other hidden roots fail closed (D3).
|
|
73
91
|
*/
|
|
74
92
|
export function resolveFrontendTestLayout(config) {
|
|
75
93
|
const testRoot = config?.testRoot === undefined || config?.testRoot === null
|
|
76
94
|
? FRONTEND_TEST_DEFAULT_TEST_ROOT
|
|
77
|
-
:
|
|
95
|
+
: resolveTestRootInput(config.testRoot, FRONTEND_TEST_DEFAULT_TEST_ROOT);
|
|
78
96
|
assertTestRootAllowed(testRoot);
|
|
79
97
|
const isDefault = testRoot === FRONTEND_TEST_DEFAULT_TEST_ROOT;
|
|
80
98
|
return {
|
|
@@ -98,10 +116,16 @@ export function frontendTestLayoutFromSpec(spec) {
|
|
|
98
116
|
return resolveFrontendTestLayout({ testRoot: frozen.testRoot });
|
|
99
117
|
}
|
|
100
118
|
const DEFAULT_PREFIX = `${FRONTEND_TEST_DEFAULT_TEST_ROOT}/`;
|
|
119
|
+
const LEGACY_DEFAULT_PREFIX = `${FRONTEND_TEST_LEGACY_DEFAULT_TEST_ROOT}/`;
|
|
101
120
|
/**
|
|
102
|
-
* Longest-prefix rewrite of
|
|
103
|
-
*
|
|
104
|
-
*
|
|
121
|
+
* Longest-prefix rewrite of frontend test-root paths in template text.
|
|
122
|
+
*
|
|
123
|
+
* No-op when layout.isDefault (default DAGs stay byte-identical to their
|
|
124
|
+
* templates, which already use the canonical `.harness/testcase/frontend`
|
|
125
|
+
* root). Custom roots rewrite BOTH the canonical default prefix and the
|
|
126
|
+
* historical `testcase/frontend/` prefix, so legacy templates and frozen
|
|
127
|
+
* specs converge onto the configured root. Never replaces the short token
|
|
128
|
+
* `testcase/` alone (would collide with backend-test).
|
|
105
129
|
*/
|
|
106
130
|
export function applyFrontendTestLayoutToText(text, layout) {
|
|
107
131
|
if (layout.isDefault)
|
|
@@ -119,6 +143,11 @@ export function applyFrontendTestLayoutToText(text, layout) {
|
|
|
119
143
|
i += DEFAULT_PREFIX.length;
|
|
120
144
|
continue;
|
|
121
145
|
}
|
|
146
|
+
if (text.startsWith(LEGACY_DEFAULT_PREFIX, i)) {
|
|
147
|
+
output += replacement;
|
|
148
|
+
i += LEGACY_DEFAULT_PREFIX.length;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
122
151
|
output += text[i];
|
|
123
152
|
i += 1;
|
|
124
153
|
}
|
|
@@ -7,6 +7,7 @@ import { DAG_AGENT_RUNTIME_PI_ONLY, DAG_REPAIR_WRITER_PROTOCOL_EXPLICIT_NODE_V1,
|
|
|
7
7
|
import { planMavenVerification } from "../../../verification/maven/index.js";
|
|
8
8
|
import { pathMatchesPattern } from "../../../shared/git-progress.js";
|
|
9
9
|
import { BASELINE_FORBIDDEN_PATHS } from "../governance-constants.js";
|
|
10
|
+
import { isAnyTestArtifactPath } from "../test-artifact-namespace.js";
|
|
10
11
|
import { resolveAdapter } from "../../../adapters/index.js";
|
|
11
12
|
import { loadHarnessManifest } from "../../../governance/harness.js";
|
|
12
13
|
import { mergeDocumentIndexCompanions } from "../../../governance/document-index-closure.js";
|
|
@@ -1534,20 +1535,38 @@ export function patternStaticPrefix(pattern) {
|
|
|
1534
1535
|
const wildcard = normalized.search(/[?*]/);
|
|
1535
1536
|
return (wildcard >= 0 ? normalized.slice(0, wildcard) : normalized).replace(/\/+$/, "");
|
|
1536
1537
|
}
|
|
1537
|
-
export function allowedPathHitsBaselineForbidden(allowedPath) {
|
|
1538
|
+
export function allowedPathHitsBaselineForbidden(allowedPath, options) {
|
|
1538
1539
|
const probes = probePathsForWriteSetEntry(allowedPath);
|
|
1539
1540
|
for (const forbidden of BASELINE_FORBIDDEN_PATHS) {
|
|
1540
1541
|
if (probes.some((probe) => pathMatchesPattern(probe, forbidden)) ||
|
|
1541
1542
|
pathMatchesPattern(allowedPath, forbidden) ||
|
|
1542
1543
|
pathMatchesPattern(forbidden, allowedPath)) {
|
|
1544
|
+
// D3 test-artifact exception: dedicated test tasks may own exactly the
|
|
1545
|
+
// canonical `.harness/testcase` roots in task allowedPaths; every other
|
|
1546
|
+
// `.harness` overlap stays a baseline violation.
|
|
1547
|
+
if (options?.testArtifactNamespace &&
|
|
1548
|
+
isHarnessNamespacePattern(forbidden) &&
|
|
1549
|
+
probes.every((probe) => isAnyTestArtifactPath(probe))) {
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1543
1552
|
return forbidden;
|
|
1544
1553
|
}
|
|
1545
1554
|
}
|
|
1546
1555
|
return undefined;
|
|
1547
1556
|
}
|
|
1557
|
+
function isHarnessNamespacePattern(pattern) {
|
|
1558
|
+
const normalized = pattern.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1559
|
+
return (normalized === ".harness" ||
|
|
1560
|
+
normalized === ".harness/**" ||
|
|
1561
|
+
normalized.startsWith(".harness/"));
|
|
1562
|
+
}
|
|
1548
1563
|
export function assertTaskAllowedPathsPreflight(taskConfig) {
|
|
1564
|
+
const testArtifactNamespace = taskConfig.taskKind === "backend-test" ||
|
|
1565
|
+
taskConfig.taskKind === "frontend-test";
|
|
1549
1566
|
for (const allowedPath of taskConfig.allowedPaths) {
|
|
1550
|
-
const forbidden = allowedPathHitsBaselineForbidden(allowedPath
|
|
1567
|
+
const forbidden = allowedPathHitsBaselineForbidden(allowedPath, {
|
|
1568
|
+
testArtifactNamespace,
|
|
1569
|
+
});
|
|
1551
1570
|
if (forbidden) {
|
|
1552
1571
|
throw new Error(`task allowedPaths entry "${allowedPath}" overlaps baseline forbidden path "${forbidden}"; task source paths are main-session owned and must not be executor writeSet`);
|
|
1553
1572
|
}
|