@tea-agent/loop-agent 0.33.4 → 0.33.6-beta.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.
- package/CHANGELOG.md +36 -0
- package/dist/executors/dag-pi-executor.js +34 -12
- package/dist/executors/shell-executor.js +26 -66
- package/dist/task/config-types.js +2 -2
- package/dist/worker/console/chat/chat-event-store.js +57 -18
- package/dist/worker/console/chat/routes.js +850 -170
- package/dist/worker/console/static/assets/{index-PzYzcuFG.js → index-CteJFFL2.js} +17 -17
- package/dist/worker/console/static/index.html +1 -1
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +33 -8
- package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +3 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +80 -2
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +27 -2
- package/dist/workflows/dag/backend-test-markdown-workflow.js +17 -0
- package/dist/workflows/dag/backend-test-module-stem.js +26 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +44 -0
- package/dist/workflows/dag/backend-test-writer-completeness.js +165 -49
- package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
- package/dist/workflows/dag/init-hybrid.js +44 -167
- package/dist/workflows/dag/types.js +7 -0
- package/docs/templates/backend-test-dag.json +10 -10
- package/docs/templates/frontend-test-dag.generate-cases.prompt.md +2 -2
- package/docs/templates/frontend-test-dag.json +9 -9
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +7 -7
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/playwright-cli/SKILL.md +2 -3
|
@@ -3508,20 +3508,29 @@ const readme=fs.existsSync('testcase/md/README.md')?fs.readFileSync('testcase/md
|
|
|
3508
3508
|
const norm=s=>String(s).toLowerCase().replace(/[^a-z0-9]+/g,'_').replace(/^_+|_+$/g,'').replace(/_+/g,'_');
|
|
3509
3509
|
const bt=String.fromCharCode(96);
|
|
3510
3510
|
const stripBackticks=s=>s.split(bt).join('');
|
|
3511
|
-
const
|
|
3511
|
+
const invalidReason=raw=>{const st=norm(raw);if(/^p[0-2]$/.test(st))return 'priority-only-module-stem';if(/^[a-f][a-f0-9]{6,63}$/.test(st))return 'opaque-hash-module-stem';if(st==='readme')return 'reserved-module-stem';if(!/^[a-z][a-z0-9_]*$/.test(st))return 'invalid-syntax';if(/^(?:be|tp|ac|req|br)[_-]/i.test(st))return 'case-like-module-stem';return null;};
|
|
3512
|
+
const valid=raw=>invalidReason(raw)===null;
|
|
3512
3513
|
const rxMdPath=/testcase\\/md\\/([A-Za-z0-9_.-]+)\\.md/g;
|
|
3513
3514
|
const rxTableRow=/\\|\\s*([A-Za-z0-9_.-]+)\\s*\\|\\s*testcase\\/test_/g;
|
|
3514
3515
|
const rxRelLink=/\\[[^\\]]+\\]\\(\\.\\/([A-Za-z0-9_.-]+)\\.md\\)/g;
|
|
3516
|
+
const allLines=readme.replace(/\\r\\n/g,'\\n').replace(/\\r/g,'\\n').split('\\n');
|
|
3517
|
+
const headings=[];for(let i=0;i<allLines.length;i++){if(allLines[i].trim()==='## Module Index')headings.push(i);}
|
|
3518
|
+
if(headings.length!==1){process.stderr.write((headings.length===0?'missing-module-index':'duplicate-module-index')+'; require exactly one exact ## Module Index section\\n');process.exit(2);}
|
|
3519
|
+
const start=headings[0]+1;let end=allLines.length;for(let i=start;i<allLines.length;i++){if(/^##\\s+\\S/.test(allLines[i].trim())){end=i;break;}}
|
|
3520
|
+
const section=allLines.slice(start,end).join('\\n');
|
|
3515
3521
|
const raw=[];
|
|
3516
|
-
const lines=
|
|
3522
|
+
const lines=section.split('\\n').filter(l=>l.includes('|'));
|
|
3517
3523
|
for(const line of lines){
|
|
3518
3524
|
const bare=stripBackticks(line);
|
|
3519
|
-
for(const m of bare.matchAll(rxMdPath)){
|
|
3520
|
-
for(const m of bare.matchAll(rxTableRow)){if(valid(m[1]))raw.push(m[1]);}
|
|
3525
|
+
for(const m of bare.matchAll(rxMdPath)){raw.push(m[1]);}
|
|
3526
|
+
for(const m of bare.matchAll(rxTableRow)){if(valid(m[1])||invalidReason(m[1])!=='invalid-syntax')raw.push(m[1]);}
|
|
3521
3527
|
}
|
|
3522
|
-
for(const m of
|
|
3528
|
+
for(const m of section.matchAll(rxRelLink)){raw.push(m[1]);}
|
|
3529
|
+
const invalid=[];for(const r of raw){const reason=invalidReason(r);if(reason)invalid.push({stem:norm(r),reason});}
|
|
3530
|
+
if(invalid.length){for(const item of invalid)process.stderr.write(item.reason+': '+item.stem+'; use a stable business resource/domain stem\\n');process.exit(2);}
|
|
3523
3531
|
const seen=new Set();const modules=[];
|
|
3524
|
-
for(const r of raw){const st=norm(r);if(
|
|
3532
|
+
for(const r of raw){const st=norm(r);if(valid(r)&&!seen.has(st)){seen.add(st);modules.push({stem:st});}}
|
|
3533
|
+
if(modules.length>8){process.stderr.write('excessive-module-count: '+modules.length+' > 8; merge by the smallest stable business resource/domain set\\n');process.exit(2);}
|
|
3525
3534
|
process.stdout.write(JSON.stringify({modules}));
|
|
3526
3535
|
`;
|
|
3527
3536
|
const encoded = Buffer.from(script, "utf8").toString("base64");
|
|
@@ -3599,7 +3608,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3599
3608
|
"Each Rule Key must appear in exactly one Matrix row. Preserve each AC/REQ/BR Rule Key as one row; if one product rule spans multiple dimensions, use a concise composite Dimension in that single row instead of duplicating the key. Derive OpenAPI Rule Keys exactly as the deterministic analyzer does: operation token is `<HTTP-METHOD>-<PATH>` with braces removed and every non-alphanumeric run replaced by a hyphen, uppercase (for example POST `/api/resource-notes` → `POST-API-RESOURCE-NOTES`); response statuses use `API-<OPERATION>-RESPONSE-STATUS`; body/parameter fields use `API-<OPERATION>-<FIELD>-REQUIRED|ENUM|MIN-LENGTH|MAX-LENGTH|MINIMUM|MAXIMUM|PATTERN|FORMAT`. Do not invent aliases such as API-CREATE-FIELDS when a deterministic key applies.",
|
|
3600
3609
|
"Coverage priority is strict inside the declared scope: P0 product requirements/task hard constraints always remain in scope; P1 exhaustively supplements documented operations, fields, business rules, statuses and errors only for Affected Operations; P2 adds bounded protocol robustness only when it is relevant to the change and does not invent product behavior. Coverage percentages describe the declared affected scope, never whole-API completeness unless every operation is explicitly listed. Conflicts or undefined expectations must stay visible as GAP/CONFLICT with precise source pointers, never guessed.",
|
|
3601
3610
|
"For uniqueness/lifecycle rules cover absent, active-existing, deleted-existing, create-delete-recreate, restore-then-recreate and documented scope/case-normalization states. For every enum cover every valid value plus bounded invalid equivalence classes (unknown, case variant, whitespace, empty, null/missing and wrong types as applicable). For every length/number rule cover min-1, min, nominal, max and max+1. For format rules cover each allowed class separately plus a valid mixed value, and representative forbidden classes including uppercase, internal/leading/trailing whitespace, tab/newline, unsupported punctuation, slash, emoji or control characters when the source contract supports that expectation.",
|
|
3602
|
-
"Mandatory module index: include a `## Module Index` table in README that lists every planned module as a canonical relative link of the exact form `[label](./<stem>.md)` plus a `testcase/md/<stem>.md` path cell, so a downstream deterministic manifest can parse the module list. Group by stable business resource/domain, not by CRUD operation: one resource's list/detail/create/update/delete cases belong in one module such as `resource_notes`; split only when a single module would exceed the per-child 16K output
|
|
3611
|
+
"Mandatory module index: include a `## Module Index` table in README that lists every planned module as a canonical relative link of the exact form `[label](./<stem>.md)` plus a `testcase/md/<stem>.md` path cell, so a downstream deterministic manifest can parse the module list. Group by stable business resource/domain, not by CRUD operation: one resource's list/detail/create/update/delete cases belong in one module such as `resource_notes`; split only when a single module would exceed the per-child 16K output protocol, keep the total module count at the smallest safe value, and never exceed 8 modules. Name each module file with a stable lowercase business stem such as `health` or `resource_notes`. Pure hexadecimal/hash-like opaque stems such as `a401606` or `deadbeef` are forbidden. Do not use priority-only stems `p0`, `p1` or `p2`; Priority belongs only in the Coverage Matrix and never defines module files. Do not use Case-ID-like module filenames such as `BE-HEALTH.md` or `BE-NOTES.md`. The relative link target MUST equal the on-disk filename stem the sharded writer will create. For every automatable case, `自动化映射` must name exactly `testcase/test_<module>.py`, where <module> is that Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `testcase/md/health.md` → `testcase/test_health.py`; `testcase/md/resource_notes.md` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.",
|
|
3603
3612
|
"Before finalizing README, calculate the predicted collected-item count as `sum(max(1, number of variant Test Points in each Case))`. If the task declares an item budget, the prediction must not exceed it. Reduce excess only by removing duplicate execution and converting same-request checkpoints to assertions; never drop required rules, boundaries, enums, operation-specific inputs, or business states. Record the prediction in README. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.",
|
|
3604
3613
|
intake.boundedSourceContext,
|
|
3605
3614
|
"## Authoritative reference index",
|
|
@@ -3644,10 +3653,11 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3644
3653
|
workflowNodeId: "generate-backend-md-cases-map",
|
|
3645
3654
|
itemsFrom: "$.nodes['materialize-backend-md-module-manifest-shell'].output.modules",
|
|
3646
3655
|
itemName: "item",
|
|
3647
|
-
maxItems:
|
|
3648
|
-
maxExpandedNodes:
|
|
3656
|
+
maxItems: 8,
|
|
3657
|
+
maxExpandedNodes: 8,
|
|
3649
3658
|
childIdPrefix: "generate-backend-md-case",
|
|
3650
|
-
tokenBudget: {
|
|
3659
|
+
tokenBudget: { maxTotalTokens: 600000 },
|
|
3660
|
+
failOnTokenBudgetExhaustion: true,
|
|
3651
3661
|
childTask: {
|
|
3652
3662
|
executor: "pi",
|
|
3653
3663
|
role: "implementer",
|
|
@@ -3670,7 +3680,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3670
3680
|
"The first non-empty response line must be exactly IMPLEMENTATION_OUTCOME: changed after the module file has been written, or IMPLEMENTATION_OUTCOME: blocked when precise missing evidence prevents safe generation. already-satisfied is not valid for this node.",
|
|
3671
3681
|
"Write human-readable content in Simplified Chinese by default. Keep English only for machine-readable IDs and technical literals such as Case/AC/REQ/BR IDs, HTTP methods, paths, field names, enum values, commands, filenames, code symbols and exact source citations.",
|
|
3672
3682
|
'Write the module {{item.stem}} as readable case cards covering every in-scope rule/Test Point the README Coverage Matrix assigns to this module. Every case starts with `## BE-<MODULE>-<NNN>|<中文用例名称>`. `<NNN>` is exactly three zero-padded digits (`001`, `002`, ...), never two digits (`01`), a bare number, or an alphabetic suffix such as `011A`. Every case must include `### 覆盖规则`, `### 测试点`, `### 场景类型`, `### 前置条件`, `### 操作步骤`, `### 预期结果`, and `### 自动化映射` Do not group cases under "## 测试类 ..." (or any h2 grouping) headings that force Cases down to h3; each Case must be a direct h2 (`##`), and its seven sections must be h3 (`###`) children of that Case. If you need to convey a pytest class, state it inside the Case\'s `### 自动化映射` instead. Forbidden: `## 测试类 X` then `### BE-PD-001` and `### 覆盖规则` at the same h3 level. Required: `## BE-PD-001` then `### 覆盖规则`.; `覆盖规则` and `测试点` must reference exact Matrix Rule Keys/Test Points. Add `测试目的`, `验收标准`, `需求依据`, and `测试数据` for readable evidence. The `验收标准` section must list the exact applicable `AC-...` IDs, and every explicit task AC must appear in at least one Case. Every automatable case explicitly names its target pytest script and exactly one primary symbol so traceability scans only that script/symbol.',
|
|
3673
|
-
"Name this module file with the stable lowercase business stem `{{item.stem}}` (filename `testcase/md/{{item.stem}}.md`). Do not use Case-ID-like module filenames. For every automatable case, `自动化映射` must name exactly `testcase/test_{{item.stem}}.py`, where the module stem is this Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `health` → `testcase/test_health.py`; `resource_notes` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.",
|
|
3683
|
+
"Name this module file with the stable lowercase business stem `{{item.stem}}` (filename `testcase/md/{{item.stem}}.md`). Priority-only stems `p0`, `p1` and `p2` are forbidden and must never produce `p0.md` or `test_p0.py`. Pure hexadecimal/hash-like opaque stems such as `a401606` and `deadbeef` are also forbidden. Do not use Case-ID-like module filenames. For every automatable case, `自动化映射` must name exactly `testcase/test_{{item.stem}}.py`, where the module stem is this Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `health` → `testcase/test_health.py`; `resource_notes` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.",
|
|
3674
3684
|
"Every Case must keep at least one numbered executable line under `### 操作步骤`; a compact variant/result table may follow but must not replace the numbered action anchor. Keep numbered/bulleted independently assertable results under `### 预期结果`. The exact `### 操作步骤` and `### 预期结果` headings must remain present for every Case, including compact/table-based Cases; never compress later Cases by dropping required headings. Every result must name the observable HTTP status, response field/value, state transition or membership condition, never vague wording such as ‘符合预期’.",
|
|
3675
3685
|
"In every `自动化映射`, use exactly these machine-readable list labels: `脚本`, `primary symbol`, `变体测试点`, `场景断言测试点`, `横切证据测试点`. Each Test Point from `### 测试点` must appear in exactly one binding list, and every Test Point named in any binding list must also be declared in that Case's `### 测试点`; write `无` for an empty list. A variant Test Point is atomic: one exact endpoint/input/precondition/outcome row equals one exact pytest item and one exact TP ID. If a parameter table has five rows, declare five distinct variant TP IDs in Markdown; never declare one family TP and append row suffixes only in pytest. Classify as `variant` only when endpoint, request input, precondition business state, or expected outcome genuinely changes and therefore needs an independent pytest parameter item. Classify CRUD checkpoints, status/body/header/schema assertions and multiple checks over the same response/journey as `assertion`; classify shared HTTP logging/redaction/truncation evidence as `cross-cutting`. Never create a Test Point merely to parameterize a checkpoint. Every non-cross-cutting TP ID is owned by exactly one Case; when the same response/schema/error assertion is needed in different Cases, use distinct Case-specific TP IDs instead of reusing one assertion TP across Cases. Keep the script path identical to the module one-to-one path and declare exactly one primary symbol named with the canonical Case prefix, for example `BE-RN-003` → `test_BE_RN_003_<description>`; non-Case-prefixed primary symbols are forbidden because parameterized item association must remain deterministic. For redaction scenarios, list sensitive header/field key names only. Never write any header-name-and-value pair, credential placeholder, fake token, anti-example, or other secret-shaped literal in Markdown; state only that a test-only value is supplied at runtime and omitted. Put implementation-only restrictions in a concise `<details>` block rather than dominating the main case flow. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.",
|
|
3676
3686
|
intake.boundedSourceContext,
|
|
@@ -3774,10 +3784,11 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3774
3784
|
workflowNodeId: "generate-backend-pytest-cases-map",
|
|
3775
3785
|
itemsFrom: "$.nodes['materialize-backend-pytest-module-manifest-shell'].output.modules",
|
|
3776
3786
|
itemName: "item",
|
|
3777
|
-
maxItems:
|
|
3778
|
-
maxExpandedNodes:
|
|
3787
|
+
maxItems: 8,
|
|
3788
|
+
maxExpandedNodes: 8,
|
|
3779
3789
|
childIdPrefix: "generate-backend-pytest-case",
|
|
3780
|
-
tokenBudget: {
|
|
3790
|
+
tokenBudget: { maxTotalTokens: 600000 },
|
|
3791
|
+
failOnTokenBudgetExhaustion: true,
|
|
3781
3792
|
childTask: {
|
|
3782
3793
|
executor: "pi",
|
|
3783
3794
|
role: "implementer",
|
|
@@ -3939,143 +3950,9 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3939
3950
|
assertValidDagSpec(spec);
|
|
3940
3951
|
return spec;
|
|
3941
3952
|
}
|
|
3942
|
-
//
|
|
3943
|
-
//
|
|
3944
|
-
//
|
|
3945
|
-
const FRONTEND_BASE_URL_DOCUMENT_ALLOWLIST = [
|
|
3946
|
-
/(?:^|[\\/])config\.md$/i,
|
|
3947
|
-
/(?:^|[\\/])environment\.md$/i,
|
|
3948
|
-
/(?:^|[\\/])env\.md$/i,
|
|
3949
|
-
/(?:^|[\\/])urls?\.md$/i,
|
|
3950
|
-
/(?:^|[\\/])references\.md$/i,
|
|
3951
|
-
/(?:^|[\\/])task-reference\.md$/i,
|
|
3952
|
-
/(?:^|[\\/])task-source\.md$/i,
|
|
3953
|
-
];
|
|
3954
|
-
// Explicit allowlist of URL keys. The semantic name drives a deterministic
|
|
3955
|
-
// priority so two conflicting same-priority URLs fail closed instead of being
|
|
3956
|
-
// silently picked.
|
|
3957
|
-
const FRONTEND_BASE_URL_KEY_ALLOWLIST = [
|
|
3958
|
-
// The primary frontend browser entry point is the strongest signal.
|
|
3959
|
-
{ name: "baseurl", priority: 0 },
|
|
3960
|
-
{ name: "base url", priority: 0 },
|
|
3961
|
-
{ name: "targeturl", priority: 0 },
|
|
3962
|
-
{ name: "target url", priority: 0 },
|
|
3963
|
-
{ name: "frontendbaseurl", priority: 1 },
|
|
3964
|
-
{ name: "frontend base url", priority: 1 },
|
|
3965
|
-
{ name: "frontendurl", priority: 1 },
|
|
3966
|
-
{ name: "frontend url", priority: 1 },
|
|
3967
|
-
{ name: "loginurl", priority: 2 },
|
|
3968
|
-
{ name: "login url", priority: 2 },
|
|
3969
|
-
];
|
|
3970
|
-
const DEFAULT_FRONTEND_BASE_URL = "http://localhost:5173/";
|
|
3971
|
-
const DEFAULT_FRONTEND_BASE_URL_SOURCE = "default-localhost-5173";
|
|
3972
|
-
function isAllowedFrontendBaseUrlDocument(documentPath) {
|
|
3973
|
-
return FRONTEND_BASE_URL_DOCUMENT_ALLOWLIST.some((pattern) => pattern.test(documentPath));
|
|
3974
|
-
}
|
|
3975
|
-
function normalizeFrontendBaseUrlKey(raw) {
|
|
3976
|
-
// Normalize separators: kebab/snake/space variants collapse to a comparable
|
|
3977
|
-
// token. Lowercase so casing differences do not bypass the allowlist.
|
|
3978
|
-
return raw
|
|
3979
|
-
.toLowerCase()
|
|
3980
|
-
.replace(/[_\-]+/g, " ")
|
|
3981
|
-
.replace(/\s+/g, " ")
|
|
3982
|
-
.trim();
|
|
3983
|
-
}
|
|
3984
|
-
/**
|
|
3985
|
-
* Resolve the browser origin only from controller-owned task source bytes.
|
|
3986
|
-
* Model-authored RAG/case/evidence files are intentionally excluded.
|
|
3987
|
-
*
|
|
3988
|
-
* Safety contract:
|
|
3989
|
-
* - Only hash-bound controller reference documents on the filename allowlist
|
|
3990
|
-
* may declare a URL.
|
|
3991
|
-
* - Only the explicit key allowlist (baseUrl / targetUrl / loginUrl + common
|
|
3992
|
-
* case/separator variants) is recognized.
|
|
3993
|
-
* - Candidates are ranked deterministically by key priority then by document
|
|
3994
|
-
* order; equal-priority conflicting URLs fail closed.
|
|
3995
|
-
* - The resolved URL must be non-production http(s) without credentials, query,
|
|
3996
|
-
* or fragment. Only when zero candidates exist does the localhost default
|
|
3997
|
-
* apply.
|
|
3998
|
-
*/
|
|
3999
|
-
export function resolveControllerFrontendBaseUrl(sources) {
|
|
4000
|
-
const allowedKeyByNormalized = new Map(FRONTEND_BASE_URL_KEY_ALLOWLIST.map((variant) => [
|
|
4001
|
-
variant.name,
|
|
4002
|
-
variant.priority,
|
|
4003
|
-
]));
|
|
4004
|
-
const candidates = [];
|
|
4005
|
-
const documents = sources.referenceDocuments ?? [];
|
|
4006
|
-
documents.forEach((document, documentIndex) => {
|
|
4007
|
-
if (!isAllowedFrontendBaseUrlDocument(document.path))
|
|
4008
|
-
return;
|
|
4009
|
-
const source = toDagSourcePath(sources, document.path);
|
|
4010
|
-
// Only accept `key: value` / `key = value` lines. A colon or equals
|
|
4011
|
-
// sign after a known key bounds the URL; free-form prose is ignored.
|
|
4012
|
-
const keyPattern = /^[ ]*(?:#>*)?[ ]*([A-Za-z][A-Za-z0-9 _\-]*?)[ ]*[:=][ ]*["'`]?((?:https?:)?\/\/[\S]+?)(?:["'`])?[ ]*(?:#.*)?$/gm;
|
|
4013
|
-
let match;
|
|
4014
|
-
while ((match = keyPattern.exec(document.markdown)) !== null) {
|
|
4015
|
-
const rawKey = match[1].trim();
|
|
4016
|
-
const normalizedKey = normalizeFrontendBaseUrlKey(rawKey);
|
|
4017
|
-
const priority = allowedKeyByNormalized.get(normalizedKey);
|
|
4018
|
-
if (priority === undefined)
|
|
4019
|
-
continue;
|
|
4020
|
-
let rawUrl = match[2].replace(/[)\]},.;]+$/, "");
|
|
4021
|
-
if (!/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(rawUrl)) {
|
|
4022
|
-
rawUrl = `https://${rawUrl.replace(/^\/\/+/, "")}`;
|
|
4023
|
-
}
|
|
4024
|
-
candidates.push({
|
|
4025
|
-
url: rawUrl,
|
|
4026
|
-
source,
|
|
4027
|
-
keyName: normalizedKey,
|
|
4028
|
-
priority,
|
|
4029
|
-
documentIndex,
|
|
4030
|
-
});
|
|
4031
|
-
}
|
|
4032
|
-
});
|
|
4033
|
-
if (candidates.length === 0) {
|
|
4034
|
-
return {
|
|
4035
|
-
baseUrl: DEFAULT_FRONTEND_BASE_URL,
|
|
4036
|
-
baseUrlSource: DEFAULT_FRONTEND_BASE_URL_SOURCE,
|
|
4037
|
-
};
|
|
4038
|
-
}
|
|
4039
|
-
// Deterministic ranking: lowest priority number first, then earliest
|
|
4040
|
-
// document, then earliest occurrence in that document.
|
|
4041
|
-
candidates.sort((left, right) => {
|
|
4042
|
-
if (left.priority !== right.priority) {
|
|
4043
|
-
return left.priority - right.priority;
|
|
4044
|
-
}
|
|
4045
|
-
if (left.documentIndex !== right.documentIndex) {
|
|
4046
|
-
return left.documentIndex - right.documentIndex;
|
|
4047
|
-
}
|
|
4048
|
-
return 0;
|
|
4049
|
-
});
|
|
4050
|
-
const bestPriority = candidates[0].priority;
|
|
4051
|
-
const bestPriorityCandidates = candidates.filter((candidate) => candidate.priority === bestPriority);
|
|
4052
|
-
// Fail closed: two distinct URLs at the same priority level are ambiguous.
|
|
4053
|
-
const distinctUrls = new Set(bestPriorityCandidates.map((candidate) => candidate.url));
|
|
4054
|
-
if (distinctUrls.size > 1) {
|
|
4055
|
-
const conflictingSources = bestPriorityCandidates
|
|
4056
|
-
.map((candidate) => `${candidate.source}:${candidate.keyName}`)
|
|
4057
|
-
.join(", ");
|
|
4058
|
-
throw new Error(`frontend-test controller baseUrl is ambiguous (${conflictingSources})`);
|
|
4059
|
-
}
|
|
4060
|
-
const selected = bestPriorityCandidates[0];
|
|
4061
|
-
const baseUrlSource = selected.source;
|
|
4062
|
-
let parsed;
|
|
4063
|
-
try {
|
|
4064
|
-
parsed = new URL(selected.url);
|
|
4065
|
-
}
|
|
4066
|
-
catch {
|
|
4067
|
-
throw new Error(`frontend-test controller baseUrl is invalid (${baseUrlSource})`);
|
|
4068
|
-
}
|
|
4069
|
-
if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
|
|
4070
|
-
parsed.username ||
|
|
4071
|
-
parsed.password ||
|
|
4072
|
-
parsed.search ||
|
|
4073
|
-
parsed.hash ||
|
|
4074
|
-
/(?:^|\.)(?:www\.)?[^.]*(?:prod|production)/i.test(parsed.hostname)) {
|
|
4075
|
-
throw new Error(`frontend-test controller baseUrl is unsafe (${baseUrlSource})`);
|
|
4076
|
-
}
|
|
4077
|
-
return { baseUrl: parsed.toString(), baseUrlSource };
|
|
4078
|
-
}
|
|
3953
|
+
// ---------------------------------------------------------------------------
|
|
3954
|
+
// Frontend browser-test RAG DAG template
|
|
3955
|
+
// ---------------------------------------------------------------------------
|
|
4079
3956
|
function buildFrontendTestHybridDag(sources) {
|
|
4080
3957
|
const rawFrontendTest = sources.taskConfig.frontendTest;
|
|
4081
3958
|
const config = {
|
|
@@ -4087,8 +3964,9 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4087
3964
|
strictOutcomeGate: rawFrontendTest?.strictOutcomeGate === true,
|
|
4088
3965
|
maxRerunAttempts: (() => {
|
|
4089
3966
|
const raw = rawFrontendTest?.maxRerunAttempts;
|
|
4090
|
-
if (raw === undefined || raw === null || Number.isNaN(Number(raw)))
|
|
4091
|
-
return 2;
|
|
3967
|
+
if (raw === undefined || raw === null || Number.isNaN(Number(raw))) {
|
|
3968
|
+
return rawFrontendTest?.reviewMode === "blocking" ? 2 : 1;
|
|
3969
|
+
}
|
|
4092
3970
|
return Math.min(4, Math.max(0, Math.trunc(Number(raw))));
|
|
4093
3971
|
})(),
|
|
4094
3972
|
reports: {
|
|
@@ -4101,7 +3979,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4101
3979
|
const reviewMode = config.reviewMode;
|
|
4102
3980
|
const blockingReview = reviewMode === "blocking";
|
|
4103
3981
|
const strictOutcomeGate = config.strictOutcomeGate;
|
|
4104
|
-
const maxRerunAttempts = config.maxRerunAttempts
|
|
3982
|
+
const maxRerunAttempts = config.maxRerunAttempts;
|
|
4105
3983
|
const enableRetrospect = config.reports?.retrospect === true;
|
|
4106
3984
|
const enableL5Report = config.reports?.l5 !== false;
|
|
4107
3985
|
const hasFrontendTestWriteScope = sources.taskConfig.allowedPaths.some((pattern) => pattern === "testcase/frontend/**" ||
|
|
@@ -4111,7 +3989,6 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4111
3989
|
throw new Error('frontend-test requires task.json allowedPaths to include "testcase/frontend/**" (or an explicit containing glob).');
|
|
4112
3990
|
}
|
|
4113
3991
|
const forbidden = commonForbiddenPaths(sources);
|
|
4114
|
-
const controllerFrontend = resolveControllerFrontendBaseUrl(sources);
|
|
4115
3992
|
const ragWriteSet = ["testcase/frontend/rag/**"];
|
|
4116
3993
|
const caseDraftWriteSet = [
|
|
4117
3994
|
"testcase/frontend/cases/FE-*.md",
|
|
@@ -4237,7 +4114,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4237
4114
|
allowedPaths: [],
|
|
4238
4115
|
forbiddenPaths: forbidden,
|
|
4239
4116
|
outputContract: "Deterministic browser-tool preflight: SDK-only custom-tool capability + controller verified playwright-cli launcher + --help contract + frozen controller origin. Fail closed with playwright-cli-unavailable | playwright-cli-contract-incompatible | browser-command-capability-unavailable before any frontend-test Pi node.",
|
|
4240
|
-
subtask_prompt:
|
|
4117
|
+
subtask_prompt: "Verify CODE_AGENT_PI_BACKEND permits SDK, the Pi SDK structured custom-tool surface is available, and the controller-resolved playwright-cli launcher and --help list open/close/find/snapshot/click. Do not resolve or freeze the target URL in this node. Do not install packages. Do not start a browser session. On failure exit non-zero so retrieve/generate/map never run (zero frontend-test Pi calls).",
|
|
4241
4118
|
shell: {
|
|
4242
4119
|
commands: [],
|
|
4243
4120
|
frontendBrowserToolPreflight: {},
|
|
@@ -4280,12 +4157,12 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4280
4157
|
writeSet: ragWriteSet,
|
|
4281
4158
|
allowedPaths: [...commonReadOnlyPaths(sources), ...ragWriteSet],
|
|
4282
4159
|
forbiddenPaths: forbidden,
|
|
4283
|
-
outputContract: "Write short testcase/frontend/rag/context.md and coverage-map.md
|
|
4160
|
+
outputContract: "Write short testcase/frontend/rag/context.md and coverage-map.md with a model-resolved non-production baseUrl/baseUrlSource from task source config, environmentProbe=pending, and capability notes.",
|
|
4284
4161
|
subtask_prompt: [
|
|
4285
4162
|
"Build the frontend test RAG package (keep it short).",
|
|
4286
4163
|
"Read task source, routes/components/API or Mock facts, and execution contract. Write only testcase/frontend/rag/context.md and coverage-map.md.",
|
|
4287
4164
|
"Prefer fixed fields: baseUrl, baseUrlSource, environmentProbe, AC table, capability matrix (backend real/mock, pagination data, HTTP observation, error injection), risks, forbidden hosts. Do not paste large implementation dumps.",
|
|
4288
|
-
|
|
4165
|
+
"Base URL resolution (required): read the bound task source and config/environment references. Prefer TARGET_URL/targetUrl as the concrete tested entry, then BASE_URL/baseUrl or FRONTEND_BASE_URL/frontendUrl, then LOGIN_URL/loginUrl; accept common casing and underscore/kebab/space variants. Choose one absolute non-production http(s) URL, never API-only or production hosts. If no usable URL exists, use http://localhost:5173. Write `baseUrl: <url>`, `baseUrlSource: <bound source path>|default-localhost-5173`, and `environmentProbe: pending`. Include exact start prefix: playwright-cli open --browser=chrome <resolved-base-url> with the concrete selected URL.",
|
|
4289
4166
|
buildSourceContextBlock(sources),
|
|
4290
4167
|
].join("\n\n"),
|
|
4291
4168
|
},
|
|
@@ -4300,12 +4177,12 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4300
4177
|
allowedPaths: [...ragWriteSet],
|
|
4301
4178
|
forbiddenPaths: forbidden,
|
|
4302
4179
|
outputContract: "Fail-closed environment preflight: absolute non-production baseUrl + curl HTTP reachability; writes environmentProbe facts; unreachable => blockedReason frontend-base-url-unreachable (node ERROR so generate/map do not run).",
|
|
4303
|
-
subtask_prompt:
|
|
4180
|
+
subtask_prompt: "Parse the resolved absolute baseUrl from testcase/frontend/rag/context.md, reject production/non-http(s)/credential/query/fragment URLs, then probe it with curl HEAD and GET fallback (connect/max-time; no auth/cookie). 2xx/3xx => reachable and continue. 4xx/5xx/DNS/timeout/connection refused/TLS => blockedReason frontend-base-url-unreachable. Missing curl => blockedReason curl-unavailable. Do not start the app. Fixture/reset remain soft guidance.",
|
|
4304
4181
|
shell: {
|
|
4305
4182
|
commands: [
|
|
4306
4183
|
[
|
|
4307
4184
|
"node -e",
|
|
4308
|
-
JSON.stringify(`const fs=require('fs');const {spawnSync}=require('child_process');const p='testcase/frontend/rag/context.md';const probePath='testcase/frontend/rag/environment-probe.json';function writeProbe(obj){try{fs.mkdirSync('testcase/frontend/rag',{recursive:true});fs.writeFileSync(probePath,JSON.stringify(obj,null,2)+'\\n');let ctx=fs.existsSync(p)?fs.readFileSync(p,'utf8'):'';const line='environmentProbe: '+obj.status+(obj.blockedReason?(' ('+obj.blockedReason+')'):'');if(/environmentProbe\\s*[:=]/i.test(ctx)){ctx=ctx.replace(/environmentProbe\\s*[:=]\\s*.*/i,line);}else{ctx=ctx.trimEnd()+'\\n\\n'+line+'\\n';}fs.writeFileSync(p,ctx);}catch(e){console.error('probe-write-failed',e&&e.message||e);}}function redactUrl(u){try{const x=new URL(u);x.username='';x.password='';if(x.search){x.search='';}return x.toString();}catch(_){return String(u).replace(/\\/\\/[^@\\s]+@/g,'//');}}function failBlocked(reason,extra){const payload=Object.assign({status:'unreachable',blockedReason:reason,baseUrlRedacted:extra&&extra.baseUrlRedacted||null,httpStatus:extra&&extra.httpStatus||null,method:extra&&extra.method||null,curlExit:extra&&extra.curlExit||null,errorClass:extra&&extra.errorClass||null},extra||{});writeProbe(payload);console.error('frontend-test preflight blocked: '+JSON.stringify({blockedReason:reason,baseUrl:payload.baseUrlRedacted,httpStatus:payload.httpStatus,errorClass:payload.errorClass}));throw new Error('frontend-test preflight blocked: '+reason);}if(!fs.existsSync(p))throw new Error('missing '+p);const baseUrl
|
|
4185
|
+
JSON.stringify(`const fs=require('fs');const {spawnSync}=require('child_process');const p='testcase/frontend/rag/context.md';const probePath='testcase/frontend/rag/environment-probe.json';function writeProbe(obj){try{fs.mkdirSync('testcase/frontend/rag',{recursive:true});fs.writeFileSync(probePath,JSON.stringify(obj,null,2)+'\\n');let ctx=fs.existsSync(p)?fs.readFileSync(p,'utf8'):'';const line='environmentProbe: '+obj.status+(obj.blockedReason?(' ('+obj.blockedReason+')'):'');if(/environmentProbe\\s*[:=]/i.test(ctx)){ctx=ctx.replace(/environmentProbe\\s*[:=]\\s*.*/i,line);}else{ctx=ctx.trimEnd()+'\\n\\n'+line+'\\n';}fs.writeFileSync(p,ctx);}catch(e){console.error('probe-write-failed',e&&e.message||e);}}function redactUrl(u){try{const x=new URL(u);x.username='';x.password='';if(x.search){x.search='';}return x.toString();}catch(_){return String(u).replace(/\\/\\/[^@\\s]+@/g,'//');}}function failBlocked(reason,extra){const payload=Object.assign({status:'unreachable',blockedReason:reason,baseUrlRedacted:extra&&extra.baseUrlRedacted||null,httpStatus:extra&&extra.httpStatus||null,method:extra&&extra.method||null,curlExit:extra&&extra.curlExit||null,errorClass:extra&&extra.errorClass||null},extra||{});writeProbe(payload);console.error('frontend-test preflight blocked: '+JSON.stringify({blockedReason:reason,baseUrl:payload.baseUrlRedacted,httpStatus:payload.httpStatus,errorClass:payload.errorClass}));throw new Error('frontend-test preflight blocked: '+reason);}if(!fs.existsSync(p))throw new Error('missing '+p);const s=fs.readFileSync(p,'utf8');const patterns=[/baseUrl\\s*[:=]\\s*[\"'\\x60]?((?:https?):\\/\\/[^\\s\"'\\x60<>]+)/i,/base[-_ ]url\\s*[:=]\\s*[\"'\\x60]?((?:https?):\\/\\/[^\\s\"'\\x60<>]+)/i,/playwright-cli open --browser=chrome\\s+((?:https?):\\/\\/[^\\s\"'\\x60<>]+)/i,/(https?:\\/\\/(?:localhost|127\\.0\\.0\\.1)[^\\s)\\}\\],\"']*)/i];let baseUrl=null;for(const re of patterns){const m=s.match(re);if(m){baseUrl=m[1];break;}}if(!baseUrl)throw new Error('frontend-test preflight missing absolute baseUrl from context.md');baseUrl=baseUrl.replace(/[)\\}\\],.\"'\\x60]+$/,'');const sourceMatch=s.match(/baseUrlSource\\s*[:=]\\s*([^\\r\\n]+)/i);const baseUrlSource=sourceMatch?sourceMatch[1].trim():'context.md';let parsed;try{parsed=new URL(baseUrl);}catch(_){throw new Error('baseUrl must be absolute http(s): '+baseUrl);}if((parsed.protocol!=='http:'&&parsed.protocol!=='https:')||parsed.username||parsed.password||parsed.search||parsed.hash)throw new Error('unsafe baseUrl from context.md: '+redactUrl(baseUrl));if(/(?:^|\\.)(?:www\\.)?[^.]*(?:prod|production)/i.test(parsed.hostname))throw new Error('production URL forbidden: '+redactUrl(baseUrl));baseUrl=parsed.toString();const safe=redactUrl(baseUrl);const curlCheck=spawnSync('curl',['--version'],{encoding:'utf8'});if(curlCheck.error||curlCheck.status!==0){failBlocked('curl-unavailable',{baseUrlRedacted:safe,errorClass:'curl-missing'});}function probe(method){const args=['-sS','-o','/dev/null','-w','%{http_code}','--connect-timeout','3','--max-time','8','-X',method,'-L','--max-redirs','3','--http1.1','--proto-redir','=http,https',safe];const r=spawnSync('curl',args,{encoding:'utf8'});return r;}let used='HEAD';let r=probe('HEAD');let code=String(r.stdout||'').trim();let statusNum=parseInt(code,10);const headRejected=r.status!==0||!statusNum||statusNum===405||statusNum===501;if(headRejected){used='GET';r=probe('GET');code=String(r.stdout||'').trim();statusNum=parseInt(code,10);}const ok=statusNum>=200&&statusNum<400;if(!ok){const errClass=r.error?'spawn-error':(r.status!==0?'curl-exit-'+r.status:('http-'+statusNum));failBlocked('frontend-base-url-unreachable',{baseUrlRedacted:safe,httpStatus:statusNum||null,method:used,curlExit:r.status,errorClass:errClass});}writeProbe({status:'reachable',blockedReason:null,baseUrl:safe,baseUrlRedacted:safe,baseUrlSource,httpStatus:statusNum,method:used,curlExit:r.status});console.log('frontend-test-execution-v1 validated contextBaseUrl='+safe+' source='+baseUrlSource+' probe=reachable method='+used+' httpStatus='+statusNum);`),
|
|
4309
4186
|
].join(" "),
|
|
4310
4187
|
],
|
|
4311
4188
|
cwd: ".",
|
|
@@ -4338,7 +4215,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4338
4215
|
"dimensions: core|boundary|flow|backend only.",
|
|
4339
4216
|
"Prefer a small smoke suite (default max roughly 4-8 cases unless task frontendTest.maxCasesPerBatch is higher). Never invent unavailable API fields or credentials. Do not create pytest or Playwright source.",
|
|
4340
4217
|
"HARD playwright-cli-only: every browser step must use repo skill playwright-cli declared commands only. Forbidden: bare `playwright`, `npx playwright`, `playwright test`, `@playwright/test`, Node Playwright API, or generating Playwright/Pytest source. No fallback when playwright-cli is unavailable - case must instruct blocked evidence playwright-cli-unavailable.",
|
|
4341
|
-
|
|
4218
|
+
"Copy the resolved absolute baseUrl from testcase/frontend/rag/context.md after the environment probe has marked it reachable. Every browser start command must be exactly: playwright-cli open --browser=chrome <resolved-base-url-from-context.md> with that concrete URL. Never leave a base-url placeholder. Use default browser session only; never write -s=<case-id>.",
|
|
4342
4219
|
"HARD dynamic refs: executable playwright-cli lines must never contain an angle-bracket token such as <fresh-ref> or descriptive <...> placeholder. Use only shell-safe documentation placeholders `eX`, `eY`, ...; each means the real `eNN` ref parsed from the immediately preceding latest `snapshot`. Write a fresh snapshot before every element reference. eX/eY are never literal structured-tool arguments; a later snapshot invalidates prior refs, so never reuse stale refs.",
|
|
4343
4220
|
"HARD file-output argv: use canonical `--filename` only. Screenshot uses `playwright-cli screenshot --filename final.png` (a real ref may precede the flag); PDF uses `playwright-cli pdf --filename final.pdf`; snapshot without filename is response-only and a snapshot file uses `playwright-cli snapshot --filename snapshot.txt`. Never generate `playwright-cli screenshot <path>`, use `--path`, `--output`, or `--file`, or pass an output path as a positional target.",
|
|
4344
4221
|
"Each case must be independently reproducible with fixture/reset, UI reset, snapshot-before-ref, evidence write point under testcase/frontend/evidence/<case-id>/. If the isolated environment is unavailable, require writing blocked evidence before any browser command.",
|
|
@@ -4487,7 +4364,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4487
4364
|
subtaskPromptTemplate: [
|
|
4488
4365
|
"Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session; do not use /new). playwright-cli-only: never bare Playwright CLI/API/test runner; no fallback.",
|
|
4489
4366
|
"Use the structured playwright_cli tool for every browser action. Do not request or search for bash. Do not execute raw shell commands. Translate each playwright-cli line in the case Markdown into one playwright_cli tool call ({command, args?, timeoutSeconds?}).",
|
|
4490
|
-
|
|
4367
|
+
"1) Read the concrete baseUrl from testcase/frontend/rag/context.md; it has already passed the environment shell safety/reachability gate. Start browser ONLY via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>] (default session only; no -s=). 2) Dynamic refs: eX/eY in case Markdown are documentation placeholders, never tool args. Immediately before every structured playwright_cli call that references an element, parse the current real eNN from the immediately preceding latest snapshot and pass only that real eNN; never send literal `eX`/`eY`. A new snapshot invalidates prior refs, so never reuse stale refs. File outputs are canonical: screenshot args [--filename, final.png] (or [e5, --filename, final.png] for a real target), PDF args [--filename, final.pdf], and snapshot writes a file only with [--filename, snapshot.txt]; snapshot without filename is response-only. Never use --path, --output, --file, or any output path as a positional target. Follow case steps with snapshot before element refs using only playwright_cli. A passed case requires this same child receipt order: successful open → successful find → controller post-execution cleanup. Only successful find is a meaningful assertion; snapshot, goto, screenshot, request/console, click/fill and other ordinary interactions cannot establish passed authority. 3) Only when preflight or playwright_cli tool explicitly fails may you write blocked evidence (blockedReason playwright-cli-unavailable | frontend-base-url-unreachable); never invent CLI-unavailable solely because bash is absent. 4) For U/D: enforce current-user ownership / create-or-mock-or-blocked; never mutate other users' data.",
|
|
4491
4368
|
"Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json with caseId={{case.caseId}}, status passed|failed|blocked, evidencePaths (relative under evidenceDir). blocked needs non-empty blockedReason. After writing, self-check the same contract; if self-check fails, rewrite both files as status=blocked blockedReason=invalid-evidence-shape (never leave missing/malformed evidence).",
|
|
4492
4369
|
"Business failed/blocked is a recorded result, not a node failure. Close browser via playwright_cli command=close. Return compact JSON (<=1200 chars): {caseId,status,evidencePaths,errorSummary,tokens}.",
|
|
4493
4370
|
].join("\n\n"),
|
|
@@ -4510,8 +4387,8 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4510
4387
|
"const manifestPath='testcase/frontend/cases/manifest.json';",
|
|
4511
4388
|
"if(!fs.existsSync(manifestPath)){process.stdout.write(JSON.stringify({cases:[]}));process.exit(0);}",
|
|
4512
4389
|
"const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));const cases=[];",
|
|
4513
|
-
"for(const c of (manifest.cases||[])){const evidenceDir=(c.evidenceDir||('testcase/frontend/evidence/'+c.caseId+'/')).replace(
|
|
4514
|
-
`fs.mkdirSync('testcase/frontend/evidence',{recursive:true});fs.writeFileSync('testcase/frontend/evidence/${candidateArtifact}',JSON.stringify({schemaVersion:1,cases},null,2)+'
|
|
4390
|
+
"for(const c of (manifest.cases||[])){const evidenceDir=(c.evidenceDir||('testcase/frontend/evidence/'+c.caseId+'/')).replace(/\\/+$/,'')+'/';const resultPath=path.join(evidenceDir,'case-result.json');const execPath=path.join(evidenceDir,'execution.md');let reason=null;let attempt=0;let missing=false;if(!fs.existsSync(resultPath)){missing=true;reason='missing-result-files';}else{try{const r=JSON.parse(fs.readFileSync(resultPath,'utf8'));attempt=Number(r.rerunAttempt||0)||0;if(r.status==='blocked')reason='blocked';if(!r.status){missing=true;reason='missing-result-files';}}catch(_){missing=true;reason='missing-result-files';}}if(!fs.existsSync(execPath)&&reason!=='blocked'){missing=true;reason=reason||'missing-result-files';}const should=(reason==='blocked'||missing)&&attempt<" + String(round) + ";if(should){cases.push({caseId:c.caseId,casePath:c.casePath||('testcase/frontend/cases/'+c.caseId+'.md'),evidenceDir,dimension:c.dimension||'core',acIds:c.acIds||[],rerunAttempt:attempt+1,reason:reason||'blocked'});}}",
|
|
4391
|
+
`fs.mkdirSync('testcase/frontend/evidence',{recursive:true});fs.writeFileSync('testcase/frontend/evidence/${candidateArtifact}',JSON.stringify({schemaVersion:1,cases},null,2)+'\\n');process.stdout.write(JSON.stringify({cases}));`,
|
|
4515
4392
|
].join("");
|
|
4516
4393
|
tasks.push({
|
|
4517
4394
|
id: selectorId,
|
|
@@ -4577,7 +4454,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4577
4454
|
subtaskPromptTemplate: [
|
|
4578
4455
|
"RERUN attempt {{case.rerunAttempt}} for {{case.caseId}} (reason={{case.reason}}). Rewrite the authoritative execution.md and case-result.json; its final status replaces the earlier case result.",
|
|
4579
4456
|
"Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session). Use structured playwright_cli only; headless open.",
|
|
4580
|
-
|
|
4457
|
+
"Read the concrete baseUrl from testcase/frontend/rag/context.md and start via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>]. Passed requires open → find → cleanup receipts.",
|
|
4581
4458
|
"Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json with caseId, status, evidencePaths, rerunAttempt={{case.rerunAttempt}}. Write fixed sections `### 执行摘要` and `### 实际执行步骤` to execution.md when available.",
|
|
4582
4459
|
].join("\n\n"),
|
|
4583
4460
|
},
|
|
@@ -4673,12 +4550,12 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4673
4550
|
"frontend-test-dag generates Markdown cases and browser evidence only; it must not generate pytest or Playwright test source code.",
|
|
4674
4551
|
"Each browser case runs serially in a fresh Pi execution boundary. Persist its evidence before starting the next case.",
|
|
4675
4552
|
"Use only the declared isolated test environment. Production URLs, real credentials, and unauthorized data are blocked.",
|
|
4676
|
-
|
|
4553
|
+
"Browser startup for generated cases must use the concrete non-production baseUrl written by retrieve-frontend-test-context-pi and validated reachable by materialize-frontend-test-execution-shell; generated operations stay in the default browser session and must not use unverified named-session flags.",
|
|
4677
4554
|
"Browser-tool preflight runs before any frontend-test Pi node; cli-only rollback, missing/incompatible Pi SDK custom-tool capability, missing verified playwright-cli launcher, or incompatible --help fails with zero Pi calls.",
|
|
4678
4555
|
"Browser case children use commandPolicy capability-allowlist playwright-cli and structured playwright_cli tool; playwright-cli stays capability-gated while ordinary writers have bash.",
|
|
4679
4556
|
"Passed cases require same-child ordered controller receipts: successful open → successful find → successful post-execution cleanup. Pre-start cleanup, snapshot/goto/screenshot/request/console and ordinary interactions are insufficient; missing or unordered receipts convert to blocked (browser-command-evidence-missing).",
|
|
4680
4557
|
"playwright-cli-only: generators and executors may call only skill-declared playwright-cli commands; bare playwright / npx playwright / @playwright/test / Playwright source are forbidden with no native Playwright fallback.",
|
|
4681
|
-
"Environment preflight must
|
|
4558
|
+
"Environment preflight must parse the retrieve-authored context.md baseUrl, reject unsafe/non-production violations, and curl-probe it before generate; unreachable or curl-unavailable ends preflight as blocked (frontend-base-url-unreachable|curl-unavailable) so generate/map do not run.",
|
|
4682
4559
|
"U/D cases must prove current-user data ownership or create cleanable current-user data or authorized Mock; otherwise blocked (current-user-data-unavailable|data-ownership-unverifiable|safe-test-data-setup-unavailable) without cross-user mutation.",
|
|
4683
4560
|
"Token settings are post-case stop thresholds, never a hard provider token cap. Unstarted cases after a threshold are blocked: token-budget-exhausted.",
|
|
4684
4561
|
"Pipeline acceptance for frontend-test is frontend-test-result-v1 plus the main report under testcase/frontend/reports/frontend-test-report.html; case pass rate and outcome=passed are quality signals; retrospect is opt-in (frontendTest.reports.retrospect).",
|
|
@@ -555,6 +555,13 @@ export const dagDynamicExpansionSchema = z.object({
|
|
|
555
555
|
maxTotalTokens: z.number().int().positive().optional(),
|
|
556
556
|
})
|
|
557
557
|
.optional(),
|
|
558
|
+
/**
|
|
559
|
+
* When true, any child blocked by the aggregate token budget fails the map
|
|
560
|
+
* barrier instead of being retained as a case-level blocked outcome.
|
|
561
|
+
* Backend-test enables this because partial Markdown/pytest module sets are
|
|
562
|
+
* invalid generation assets; frontend-test keeps the default false.
|
|
563
|
+
*/
|
|
564
|
+
failOnTokenBudgetExhaustion: z.boolean().optional(),
|
|
558
565
|
/**
|
|
559
566
|
* When true, map child ERROR/auth/timeout is recorded as case-level
|
|
560
567
|
* failed/blocked evidence and the map barrier still succeeds (frontend-test).
|
|
@@ -140,7 +140,7 @@
|
|
|
140
140
|
]
|
|
141
141
|
},
|
|
142
142
|
"outputContract": "Write a Chinese, human-readable testcase/md/README.md as the single Markdown-first entry page with Coverage Scope, Coverage Matrix and a machine-parseable module index. Do not write module case cards here; do not execute pytest or modify production code/config.",
|
|
143
|
-
"subtask_prompt": "This is a required file-generation node. After reading the bounded inputs, immediately use write tools to create testcase/md/README.md. Do not end after analysis or planning, and do not return before a non-empty bounded diff exists. Write ONLY testcase/md/README.md in this node; module case cards are written by downstream sharded nodes.\n\nOutput budget protocol (hard, max output <=16K per turn): Never paste full Matrix, case bodies, or source text into assistant chat. README holds only Scope+Matrix+module index; never inline full case bodies. If a Completeness Gate / OUTPUT_LIMIT_RECOVERY retry is injected, continue only listed target paths.\n\nThe first non-empty response line must be exactly IMPLEMENTATION_OUTCOME: changed after the README has been written, or IMPLEMENTATION_OUTCOME: blocked when precise missing evidence prevents safe generation. already-satisfied is not valid for this node.\n\nRead the upstream environment report. Generate the Markdown-first backend test README under testcase/md/README.md.\n\nWrite human-readable content in Simplified Chinese by default. Keep English only for machine-readable IDs and technical literals such as Case/AC/REQ/BR IDs, HTTP methods, paths, field names, enum values, commands, filenames, code symbols and exact source citations.\n\nCreate testcase/md/README.md as the concise entry page: test objective, target/environment, isolation/cleanup, module summary and a linked case index table with Case ID, Chinese case name, scenario type, endpoint and expected status/result. Avoid repeating every case body in README.\n\nBefore the Coverage Matrix, write a mandatory machine-readable `## Coverage Scope` section in README using exactly `| Field | Value |`, immediately followed by the separator row `|---|---|`, and these six unique rows: `Change Classification`, `Coverage Policy`, `Affected Operations`, `Affected Rule Keys`, `Regression Floor`, `Scope Evidence`. Always set `Change Classification` to `new-operation` and `Coverage Policy` to `full-contract`; do NOT reason about whether operations are new or existing. Cover all in-scope rules from the requirement document at full depth; treat the product requirement as the coverage baseline and use API contract evidence (fields/status/enum/boundary/format) to supplement scenario dimensions. Scope is limited to operations/rules the requirement document (or its referenced API contract) explicitly describes; do not expand to unrelated operations that the requirement does not mention. List affected operations exactly as `METHOD /path`, stable rule keys separated by semicolons, and precise source pointers as Scope Evidence.\n\nCoverage depth is full over the in-scope rules: fully cover every documented status, request/response field rule, requiredness, enum, boundary, format, auth and business state of each affected operation the requirement describes, but do not re-test unrelated operations the requirement does not mention. Inspect shared validator/helper/DTO/query builder evidence and expand Affected Operations when the same affected path can affect them; unresolved impact stays visible as GAP/CONFLICT.\n\nBefore writing cases, build the mandatory machine-readable Coverage Matrix inside `testcase/md/README.md` itself. Its section heading line must be exactly `## Coverage Matrix` with no numeric prefix/suffix; never place the canonical Matrix only in a module file. Use this exact header: `| Rule Key | Priority | Source | Endpoint/Field | Dimension | Rule | Required Test Points | Case IDs | Status |`. Every data row must contain exactly 9 pipe-delimited cells and must never omit `Dimension`; use concise dimensions such as requirement, operation, response-status, requiredness, enum, boundary, format, business-state or error. Use only P0/P1/P2 and COVERED/PARTIAL/GAP/CONFLICT. Use stable `TP-<UPPERCASE-HYPHENATED-ID>` test points separated by semicolons.\n\nEach Rule Key must appear in exactly one Matrix row. Preserve each AC/REQ/BR Rule Key as one row; if one product rule spans multiple dimensions, use a concise composite Dimension in that single row instead of duplicating the key. Derive OpenAPI Rule Keys exactly as the deterministic analyzer does: operation token is `<HTTP-METHOD>-<PATH>` with braces removed and every non-alphanumeric run replaced by a hyphen, uppercase (for example POST `/api/resource-notes` → `POST-API-RESOURCE-NOTES`); response statuses use `API-<OPERATION>-RESPONSE-STATUS`; body/parameter fields use `API-<OPERATION>-<FIELD>-REQUIRED|ENUM|MIN-LENGTH|MAX-LENGTH|MINIMUM|MAXIMUM|PATTERN|FORMAT`. Do not invent aliases such as API-CREATE-FIELDS when a deterministic key applies.\n\nCoverage priority is strict inside the declared scope: P0 product requirements/task hard constraints always remain in scope; P1 exhaustively supplements documented operations, fields, business rules, statuses and errors only for Affected Operations; P2 adds bounded protocol robustness only when it is relevant to the change and does not invent product behavior. Coverage percentages describe the declared affected scope, never whole-API completeness unless every operation is explicitly listed. Conflicts or undefined expectations must stay visible as GAP/CONFLICT with precise source pointers, never guessed.\n\nFor uniqueness/lifecycle rules cover absent, active-existing, deleted-existing, create-delete-recreate, restore-then-recreate and documented scope/case-normalization states. For every enum cover every valid value plus bounded invalid equivalence classes (unknown, case variant, whitespace, empty, null/missing and wrong types as applicable). For every length/number rule cover min-1, min, nominal, max and max+1. For format rules cover each allowed class separately plus a valid mixed value, and representative forbidden classes including uppercase, internal/leading/trailing whitespace, tab/newline, unsupported punctuation, slash, emoji or control characters when the source contract supports that expectation.\n\nMandatory module index: include a `## Module Index` table in README that lists every planned module as a canonical relative link of the exact form `[label](./<stem>.md)` plus a `testcase/md/<stem>.md` path cell, so a downstream deterministic manifest can parse the module list. Group by stable business resource/domain, not by CRUD operation: one resource's list/detail/create/update/delete cases belong in one module such as `resource_notes`; split only when a single module would exceed the per-child 16K output
|
|
143
|
+
"subtask_prompt": "This is a required file-generation node. After reading the bounded inputs, immediately use write tools to create testcase/md/README.md. Do not end after analysis or planning, and do not return before a non-empty bounded diff exists. Write ONLY testcase/md/README.md in this node; module case cards are written by downstream sharded nodes.\n\nOutput budget protocol (hard, max output <=16K per turn): Never paste full Matrix, case bodies, or source text into assistant chat. README holds only Scope+Matrix+module index; never inline full case bodies. If a Completeness Gate / OUTPUT_LIMIT_RECOVERY retry is injected, continue only listed target paths.\n\nThe first non-empty response line must be exactly IMPLEMENTATION_OUTCOME: changed after the README has been written, or IMPLEMENTATION_OUTCOME: blocked when precise missing evidence prevents safe generation. already-satisfied is not valid for this node.\n\nRead the upstream environment report. Generate the Markdown-first backend test README under testcase/md/README.md.\n\nWrite human-readable content in Simplified Chinese by default. Keep English only for machine-readable IDs and technical literals such as Case/AC/REQ/BR IDs, HTTP methods, paths, field names, enum values, commands, filenames, code symbols and exact source citations.\n\nCreate testcase/md/README.md as the concise entry page: test objective, target/environment, isolation/cleanup, module summary and a linked case index table with Case ID, Chinese case name, scenario type, endpoint and expected status/result. Avoid repeating every case body in README.\n\nBefore the Coverage Matrix, write a mandatory machine-readable `## Coverage Scope` section in README using exactly `| Field | Value |`, immediately followed by the separator row `|---|---|`, and these six unique rows: `Change Classification`, `Coverage Policy`, `Affected Operations`, `Affected Rule Keys`, `Regression Floor`, `Scope Evidence`. Always set `Change Classification` to `new-operation` and `Coverage Policy` to `full-contract`; do NOT reason about whether operations are new or existing. Cover all in-scope rules from the requirement document at full depth; treat the product requirement as the coverage baseline and use API contract evidence (fields/status/enum/boundary/format) to supplement scenario dimensions. Scope is limited to operations/rules the requirement document (or its referenced API contract) explicitly describes; do not expand to unrelated operations that the requirement does not mention. List affected operations exactly as `METHOD /path`, stable rule keys separated by semicolons, and precise source pointers as Scope Evidence.\n\nCoverage depth is full over the in-scope rules: fully cover every documented status, request/response field rule, requiredness, enum, boundary, format, auth and business state of each affected operation the requirement describes, but do not re-test unrelated operations the requirement does not mention. Inspect shared validator/helper/DTO/query builder evidence and expand Affected Operations when the same affected path can affect them; unresolved impact stays visible as GAP/CONFLICT.\n\nBefore writing cases, build the mandatory machine-readable Coverage Matrix inside `testcase/md/README.md` itself. Its section heading line must be exactly `## Coverage Matrix` with no numeric prefix/suffix; never place the canonical Matrix only in a module file. Use this exact header: `| Rule Key | Priority | Source | Endpoint/Field | Dimension | Rule | Required Test Points | Case IDs | Status |`. Every data row must contain exactly 9 pipe-delimited cells and must never omit `Dimension`; use concise dimensions such as requirement, operation, response-status, requiredness, enum, boundary, format, business-state or error. Use only P0/P1/P2 and COVERED/PARTIAL/GAP/CONFLICT. Use stable `TP-<UPPERCASE-HYPHENATED-ID>` test points separated by semicolons.\n\nEach Rule Key must appear in exactly one Matrix row. Preserve each AC/REQ/BR Rule Key as one row; if one product rule spans multiple dimensions, use a concise composite Dimension in that single row instead of duplicating the key. Derive OpenAPI Rule Keys exactly as the deterministic analyzer does: operation token is `<HTTP-METHOD>-<PATH>` with braces removed and every non-alphanumeric run replaced by a hyphen, uppercase (for example POST `/api/resource-notes` → `POST-API-RESOURCE-NOTES`); response statuses use `API-<OPERATION>-RESPONSE-STATUS`; body/parameter fields use `API-<OPERATION>-<FIELD>-REQUIRED|ENUM|MIN-LENGTH|MAX-LENGTH|MINIMUM|MAXIMUM|PATTERN|FORMAT`. Do not invent aliases such as API-CREATE-FIELDS when a deterministic key applies.\n\nCoverage priority is strict inside the declared scope: P0 product requirements/task hard constraints always remain in scope; P1 exhaustively supplements documented operations, fields, business rules, statuses and errors only for Affected Operations; P2 adds bounded protocol robustness only when it is relevant to the change and does not invent product behavior. Coverage percentages describe the declared affected scope, never whole-API completeness unless every operation is explicitly listed. Conflicts or undefined expectations must stay visible as GAP/CONFLICT with precise source pointers, never guessed.\n\nFor uniqueness/lifecycle rules cover absent, active-existing, deleted-existing, create-delete-recreate, restore-then-recreate and documented scope/case-normalization states. For every enum cover every valid value plus bounded invalid equivalence classes (unknown, case variant, whitespace, empty, null/missing and wrong types as applicable). For every length/number rule cover min-1, min, nominal, max and max+1. For format rules cover each allowed class separately plus a valid mixed value, and representative forbidden classes including uppercase, internal/leading/trailing whitespace, tab/newline, unsupported punctuation, slash, emoji or control characters when the source contract supports that expectation.\n\nMandatory module index: include a `## Module Index` table in README that lists every planned module as a canonical relative link of the exact form `[label](./<stem>.md)` plus a `testcase/md/<stem>.md` path cell, so a downstream deterministic manifest can parse the module list. Group by stable business resource/domain, not by CRUD operation: one resource's list/detail/create/update/delete cases belong in one module such as `resource_notes`; split only when a single module would exceed the per-child 16K output protocol, keep the total module count at the smallest safe value, and never exceed 8 modules. Name each module file with a stable lowercase business stem such as `health` or `resource_notes`. Pure hexadecimal/hash-like opaque stems such as `a401606` or `deadbeef` are forbidden. Do not use priority-only stems `p0`, `p1` or `p2`; Priority belongs only in the Coverage Matrix and never defines module files. Do not use Case-ID-like module filenames such as `BE-HEALTH.md` or `BE-NOTES.md`. The relative link target MUST equal the on-disk filename stem the sharded writer will create. For every automatable case, `自动化映射` must name exactly `testcase/test_<module>.py`, where <module> is that Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `testcase/md/health.md` → `testcase/test_health.py`; `testcase/md/resource_notes.md` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.\n\nBefore finalizing README, calculate the predicted collected-item count as `sum(max(1, number of variant Test Points in each Case))`. If the task declares an item budget, the prediction must not exceed it. Reduce excess only by removing duplicate execution and converting same-request checkpoints to assertions; never drop required rules, boundaries, enums, operation-specific inputs, or business states. Record the prediction in README. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.\n\n## Derived task contract: 需求.md\n\n# Backend test\n- AC-001 proof\n\n## Authoritative reference index\n\n[]\n\nFor each index entry, use `readPath` for Pi read-tool calls and copy `path` exactly into Markdown Source References. Bound files under .harness/tasks/<taskId>/source/** are read-only inputs: reading them is allowed even though writing .harness/** is forbidden. Never resolve `path` relative to the repository root, search for substitutes, or fall back to docs/** when a bound read fails.\n\nRead only precise indexed references needed for AC/API/field/rule evidence; references remain authoritative over derived text."
|
|
144
144
|
},
|
|
145
145
|
{
|
|
146
146
|
"id": "materialize-backend-md-module-manifest-shell",
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
"subtask_prompt": "Parse testcase/md/README.md and emit exactly one trailing JSON line {modules:[{stem}]} listing every trusted module stem (table-row testcase/md/<stem>.md mentions and canonical [label](./<stem>.md) relative links only). No file writes.",
|
|
165
165
|
"shell": {
|
|
166
166
|
"commands": [
|
|
167
|
-
"node -e \"eval(Buffer.from('Y29uc3QgZnM9cmVxdWlyZSgnZnMnKTsKY29uc3QgcmVhZG1lPWZzLmV4aXN0c1N5bmMoJ3Rlc3RjYXNlL21kL1JFQURNRS5tZCcpP2ZzLnJlYWRGaWxlU3luYygndGVzdGNhc2UvbWQvUkVBRE1FLm1kJywndXRmOCcpOicnOwpjb25zdCBub3JtPXM9PlN0cmluZyhzKS50b0xvd2VyQ2FzZSgpLnJlcGxhY2UoL1teYS16MC05XSsvZywnXycpLnJlcGxhY2UoL15fK3xfKyQvZywnJykucmVwbGFjZSgvXysvZywnXycpOwpjb25zdCBidD1TdHJpbmcuZnJvbUNoYXJDb2RlKDk2KTsKY29uc3Qgc3RyaXBCYWNrdGlja3M9cz0+
|
|
167
|
+
"node -e \"eval(Buffer.from('Y29uc3QgZnM9cmVxdWlyZSgnZnMnKTsKY29uc3QgcmVhZG1lPWZzLmV4aXN0c1N5bmMoJ3Rlc3RjYXNlL21kL1JFQURNRS5tZCcpP2ZzLnJlYWRGaWxlU3luYygndGVzdGNhc2UvbWQvUkVBRE1FLm1kJywndXRmOCcpOicnOwpjb25zdCBub3JtPXM9PlN0cmluZyhzKS50b0xvd2VyQ2FzZSgpLnJlcGxhY2UoL1teYS16MC05XSsvZywnXycpLnJlcGxhY2UoL15fK3xfKyQvZywnJykucmVwbGFjZSgvXysvZywnXycpOwpjb25zdCBidD1TdHJpbmcuZnJvbUNoYXJDb2RlKDk2KTsKY29uc3Qgc3RyaXBCYWNrdGlja3M9cz0+cy5zcGxpdChidCkuam9pbignJyk7CmNvbnN0IGludmFsaWRSZWFzb249cmF3PT57Y29uc3Qgc3Q9bm9ybShyYXcpO2lmKC9ecFswLTJdJC8udGVzdChzdCkpcmV0dXJuICdwcmlvcml0eS1vbmx5LW1vZHVsZS1zdGVtJztpZigvXlthLWZdW2EtZjAtOV17Niw2M30kLy50ZXN0KHN0KSlyZXR1cm4gJ29wYXF1ZS1oYXNoLW1vZHVsZS1zdGVtJztpZihzdD09PSdyZWFkbWUnKXJldHVybiAncmVzZXJ2ZWQtbW9kdWxlLXN0ZW0nO2lmKCEvXlthLXpdW2EtejAtOV9dKiQvLnRlc3Qoc3QpKXJldHVybiAnaW52YWxpZC1zeW50YXgnO2lmKC9eKD86YmV8dHB8YWN8cmVxfGJyKVtfLV0vaS50ZXN0KHN0KSlyZXR1cm4gJ2Nhc2UtbGlrZS1tb2R1bGUtc3RlbSc7cmV0dXJuIG51bGw7fTsKY29uc3QgdmFsaWQ9cmF3PT5pbnZhbGlkUmVhc29uKHJhdyk9PT1udWxsOwpjb25zdCByeE1kUGF0aD0vdGVzdGNhc2VcL21kXC8oW0EtWmEtejAtOV8uLV0rKVwubWQvZzsKY29uc3QgcnhUYWJsZVJvdz0vXHxccyooW0EtWmEtejAtOV8uLV0rKVxzKlx8XHMqdGVzdGNhc2VcL3Rlc3RfL2c7CmNvbnN0IHJ4UmVsTGluaz0vXFtbXlxdXStcXVwoXC5cLyhbQS1aYS16MC05Xy4tXSspXC5tZFwpL2c7CmNvbnN0IGFsbExpbmVzPXJlYWRtZS5yZXBsYWNlKC9cclxuL2csJ1xuJykucmVwbGFjZSgvXHIvZywnXG4nKS5zcGxpdCgnXG4nKTsKY29uc3QgaGVhZGluZ3M9W107Zm9yKGxldCBpPTA7aTxhbGxMaW5lcy5sZW5ndGg7aSsrKXtpZihhbGxMaW5lc1tpXS50cmltKCk9PT0nIyMgTW9kdWxlIEluZGV4JyloZWFkaW5ncy5wdXNoKGkpO30KaWYoaGVhZGluZ3MubGVuZ3RoIT09MSl7cHJvY2Vzcy5zdGRlcnIud3JpdGUoKGhlYWRpbmdzLmxlbmd0aD09PTA/J21pc3NpbmctbW9kdWxlLWluZGV4JzonZHVwbGljYXRlLW1vZHVsZS1pbmRleCcpKyc7IHJlcXVpcmUgZXhhY3RseSBvbmUgZXhhY3QgIyMgTW9kdWxlIEluZGV4IHNlY3Rpb25cbicpO3Byb2Nlc3MuZXhpdCgyKTt9CmNvbnN0IHN0YXJ0PWhlYWRpbmdzWzBdKzE7bGV0IGVuZD1hbGxMaW5lcy5sZW5ndGg7Zm9yKGxldCBpPXN0YXJ0O2k8YWxsTGluZXMubGVuZ3RoO2krKyl7aWYoL14jI1xzK1xTLy50ZXN0KGFsbExpbmVzW2ldLnRyaW0oKSkpe2VuZD1pO2JyZWFrO319CmNvbnN0IHNlY3Rpb249YWxsTGluZXMuc2xpY2Uoc3RhcnQsZW5kKS5qb2luKCdcbicpOwpjb25zdCByYXc9W107CmNvbnN0IGxpbmVzPXNlY3Rpb24uc3BsaXQoJ1xuJykuZmlsdGVyKGw9PmwuaW5jbHVkZXMoJ3wnKSk7CmZvcihjb25zdCBsaW5lIG9mIGxpbmVzKXsKICBjb25zdCBiYXJlPXN0cmlwQmFja3RpY2tzKGxpbmUpOwogIGZvcihjb25zdCBtIG9mIGJhcmUubWF0Y2hBbGwocnhNZFBhdGgpKXtyYXcucHVzaChtWzFdKTt9CiAgZm9yKGNvbnN0IG0gb2YgYmFyZS5tYXRjaEFsbChyeFRhYmxlUm93KSl7aWYodmFsaWQobVsxXSl8fGludmFsaWRSZWFzb24obVsxXSkhPT0naW52YWxpZC1zeW50YXgnKXJhdy5wdXNoKG1bMV0pO30KfQpmb3IoY29uc3QgbSBvZiBzZWN0aW9uLm1hdGNoQWxsKHJ4UmVsTGluaykpe3Jhdy5wdXNoKG1bMV0pO30KY29uc3QgaW52YWxpZD1bXTtmb3IoY29uc3QgciBvZiByYXcpe2NvbnN0IHJlYXNvbj1pbnZhbGlkUmVhc29uKHIpO2lmKHJlYXNvbilpbnZhbGlkLnB1c2goe3N0ZW06bm9ybShyKSxyZWFzb259KTt9CmlmKGludmFsaWQubGVuZ3RoKXtmb3IoY29uc3QgaXRlbSBvZiBpbnZhbGlkKXByb2Nlc3Muc3RkZXJyLndyaXRlKGl0ZW0ucmVhc29uKyc6ICcraXRlbS5zdGVtKyc7IHVzZSBhIHN0YWJsZSBidXNpbmVzcyByZXNvdXJjZS9kb21haW4gc3RlbVxuJyk7cHJvY2Vzcy5leGl0KDIpO30KY29uc3Qgc2Vlbj1uZXcgU2V0KCk7Y29uc3QgbW9kdWxlcz1bXTsKZm9yKGNvbnN0IHIgb2YgcmF3KXtjb25zdCBzdD1ub3JtKHIpO2lmKHZhbGlkKHIpJiYhc2Vlbi5oYXMoc3QpKXtzZWVuLmFkZChzdCk7bW9kdWxlcy5wdXNoKHtzdGVtOnN0fSk7fX0KaWYobW9kdWxlcy5sZW5ndGg+OCl7cHJvY2Vzcy5zdGRlcnIud3JpdGUoJ2V4Y2Vzc2l2ZS1tb2R1bGUtY291bnQ6ICcrbW9kdWxlcy5sZW5ndGgrJyA+IDg7IG1lcmdlIGJ5IHRoZSBzbWFsbGVzdCBzdGFibGUgYnVzaW5lc3MgcmVzb3VyY2UvZG9tYWluIHNldFxuJyk7cHJvY2Vzcy5leGl0KDIpO30KcHJvY2Vzcy5zdGRvdXQud3JpdGUoSlNPTi5zdHJpbmdpZnkoe21vZHVsZXN9KSk7Cg==','base64').toString('utf8'))\""
|
|
168
168
|
],
|
|
169
169
|
"cwd": ".",
|
|
170
170
|
"timeoutMs": 60000
|
|
@@ -195,13 +195,13 @@
|
|
|
195
195
|
"workflowNodeId": "generate-backend-md-cases-map",
|
|
196
196
|
"itemsFrom": "$.nodes['materialize-backend-md-module-manifest-shell'].output.modules",
|
|
197
197
|
"itemName": "item",
|
|
198
|
-
"maxItems":
|
|
199
|
-
"maxExpandedNodes":
|
|
198
|
+
"maxItems": 8,
|
|
199
|
+
"maxExpandedNodes": 8,
|
|
200
200
|
"childIdPrefix": "generate-backend-md-case",
|
|
201
201
|
"tokenBudget": {
|
|
202
|
-
"maxTokensPerCase": 16384,
|
|
203
202
|
"maxTotalTokens": 600000
|
|
204
203
|
},
|
|
204
|
+
"failOnTokenBudgetExhaustion": true,
|
|
205
205
|
"childTask": {
|
|
206
206
|
"executor": "pi",
|
|
207
207
|
"role": "implementer",
|
|
@@ -238,7 +238,7 @@
|
|
|
238
238
|
]
|
|
239
239
|
},
|
|
240
240
|
"outputContract": "Write exactly one Chinese module Markdown case-card file testcase/md/<stem>.md with BE-<MODULE>-<NNN> cases and the seven required h3 sections; keep machine IDs/literals exact and do not execute pytest or modify production code/config or the README.",
|
|
241
|
-
"subtaskPromptTemplate": "This is a required file-generation node for exactly one Markdown module. After reading testcase/md/README.md (Coverage Scope + Coverage Matrix + module index) and the bounded references, immediately use write tools to create the single file testcase/md/{{item.stem}}.md. Do not end after analysis or planning, and do not return before a non-empty bounded diff exists. Do not modify README.md or any other module file.\n\nOutput budget protocol (hard, max output <=16K per turn): Never paste full Matrix, other modules' case bodies, or source text into assistant chat. Each write/edit tool call touches at most one file (this module). Compact tables/lists are required; omitting required sections or in-scope variants is forbidden. If a Completeness Gate / OUTPUT_LIMIT_RECOVERY retry is injected, continue only listed target paths.\n\nThe first non-empty response line must be exactly IMPLEMENTATION_OUTCOME: changed after the module file has been written, or IMPLEMENTATION_OUTCOME: blocked when precise missing evidence prevents safe generation. already-satisfied is not valid for this node.\n\nWrite human-readable content in Simplified Chinese by default. Keep English only for machine-readable IDs and technical literals such as Case/AC/REQ/BR IDs, HTTP methods, paths, field names, enum values, commands, filenames, code symbols and exact source citations.\n\nWrite the module {{item.stem}} as readable case cards covering every in-scope rule/Test Point the README Coverage Matrix assigns to this module. Every case starts with `## BE-<MODULE>-<NNN>|<中文用例名称>`. `<NNN>` is exactly three zero-padded digits (`001`, `002`, ...), never two digits (`01`), a bare number, or an alphabetic suffix such as `011A`. Every case must include `### 覆盖规则`, `### 测试点`, `### 场景类型`, `### 前置条件`, `### 操作步骤`, `### 预期结果`, and `### 自动化映射` Do not group cases under \"## 测试类 ...\" (or any h2 grouping) headings that force Cases down to h3; each Case must be a direct h2 (`##`), and its seven sections must be h3 (`###`) children of that Case. If you need to convey a pytest class, state it inside the Case's `### 自动化映射` instead. Forbidden: `## 测试类 X` then `### BE-PD-001` and `### 覆盖规则` at the same h3 level. Required: `## BE-PD-001` then `### 覆盖规则`.; `覆盖规则` and `测试点` must reference exact Matrix Rule Keys/Test Points. Add `测试目的`, `验收标准`, `需求依据`, and `测试数据` for readable evidence. The `验收标准` section must list the exact applicable `AC-...` IDs, and every explicit task AC must appear in at least one Case. Every automatable case explicitly names its target pytest script and exactly one primary symbol so traceability scans only that script/symbol.\n\nName this module file with the stable lowercase business stem `{{item.stem}}` (filename `testcase/md/{{item.stem}}.md`). Do not use Case-ID-like module filenames. For every automatable case, `自动化映射` must name exactly `testcase/test_{{item.stem}}.py`, where the module stem is this Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `health` → `testcase/test_health.py`; `resource_notes` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.\n\nEvery Case must keep at least one numbered executable line under `### 操作步骤`; a compact variant/result table may follow but must not replace the numbered action anchor. Keep numbered/bulleted independently assertable results under `### 预期结果`. The exact `### 操作步骤` and `### 预期结果` headings must remain present for every Case, including compact/table-based Cases; never compress later Cases by dropping required headings. Every result must name the observable HTTP status, response field/value, state transition or membership condition, never vague wording such as ‘符合预期’.\n\nIn every `自动化映射`, use exactly these machine-readable list labels: `脚本`, `primary symbol`, `变体测试点`, `场景断言测试点`, `横切证据测试点`. Each Test Point from `### 测试点` must appear in exactly one binding list, and every Test Point named in any binding list must also be declared in that Case's `### 测试点`; write `无` for an empty list. A variant Test Point is atomic: one exact endpoint/input/precondition/outcome row equals one exact pytest item and one exact TP ID. If a parameter table has five rows, declare five distinct variant TP IDs in Markdown; never declare one family TP and append row suffixes only in pytest. Classify as `variant` only when endpoint, request input, precondition business state, or expected outcome genuinely changes and therefore needs an independent pytest parameter item. Classify CRUD checkpoints, status/body/header/schema assertions and multiple checks over the same response/journey as `assertion`; classify shared HTTP logging/redaction/truncation evidence as `cross-cutting`. Never create a Test Point merely to parameterize a checkpoint. Every non-cross-cutting TP ID is owned by exactly one Case; when the same response/schema/error assertion is needed in different Cases, use distinct Case-specific TP IDs instead of reusing one assertion TP across Cases. Keep the script path identical to the module one-to-one path and declare exactly one primary symbol named with the canonical Case prefix, for example `BE-RN-003` → `test_BE_RN_003_<description>`; non-Case-prefixed primary symbols are forbidden because parameterized item association must remain deterministic. For redaction scenarios, list sensitive header/field key names only. Never write any header-name-and-value pair, credential placeholder, fake token, anti-example, or other secret-shaped literal in Markdown; state only that a test-only value is supplied at runtime and omitted. Put implementation-only restrictions in a concise `<details>` block rather than dominating the main case flow. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.\n\n## Derived task contract: 需求.md\n\n# Backend test\n- AC-001 proof\n\n## Authoritative reference index\n\n[]\n\nFor each index entry, use `readPath` for Pi read-tool calls and copy `path` exactly into Markdown Source References. Bound files under .harness/tasks/<taskId>/source/** are read-only inputs: reading them is allowed even though writing .harness/** is forbidden. Never resolve `path` relative to the repository root, search for substitutes, or fall back to docs/** when a bound read fails.\n\nRead only precise indexed references needed for AC/API/field/rule evidence; references remain authoritative over derived text."
|
|
241
|
+
"subtaskPromptTemplate": "This is a required file-generation node for exactly one Markdown module. After reading testcase/md/README.md (Coverage Scope + Coverage Matrix + module index) and the bounded references, immediately use write tools to create the single file testcase/md/{{item.stem}}.md. Do not end after analysis or planning, and do not return before a non-empty bounded diff exists. Do not modify README.md or any other module file.\n\nOutput budget protocol (hard, max output <=16K per turn): Never paste full Matrix, other modules' case bodies, or source text into assistant chat. Each write/edit tool call touches at most one file (this module). Compact tables/lists are required; omitting required sections or in-scope variants is forbidden. If a Completeness Gate / OUTPUT_LIMIT_RECOVERY retry is injected, continue only listed target paths.\n\nThe first non-empty response line must be exactly IMPLEMENTATION_OUTCOME: changed after the module file has been written, or IMPLEMENTATION_OUTCOME: blocked when precise missing evidence prevents safe generation. already-satisfied is not valid for this node.\n\nWrite human-readable content in Simplified Chinese by default. Keep English only for machine-readable IDs and technical literals such as Case/AC/REQ/BR IDs, HTTP methods, paths, field names, enum values, commands, filenames, code symbols and exact source citations.\n\nWrite the module {{item.stem}} as readable case cards covering every in-scope rule/Test Point the README Coverage Matrix assigns to this module. Every case starts with `## BE-<MODULE>-<NNN>|<中文用例名称>`. `<NNN>` is exactly three zero-padded digits (`001`, `002`, ...), never two digits (`01`), a bare number, or an alphabetic suffix such as `011A`. Every case must include `### 覆盖规则`, `### 测试点`, `### 场景类型`, `### 前置条件`, `### 操作步骤`, `### 预期结果`, and `### 自动化映射` Do not group cases under \"## 测试类 ...\" (or any h2 grouping) headings that force Cases down to h3; each Case must be a direct h2 (`##`), and its seven sections must be h3 (`###`) children of that Case. If you need to convey a pytest class, state it inside the Case's `### 自动化映射` instead. Forbidden: `## 测试类 X` then `### BE-PD-001` and `### 覆盖规则` at the same h3 level. Required: `## BE-PD-001` then `### 覆盖规则`.; `覆盖规则` and `测试点` must reference exact Matrix Rule Keys/Test Points. Add `测试目的`, `验收标准`, `需求依据`, and `测试数据` for readable evidence. The `验收标准` section must list the exact applicable `AC-...` IDs, and every explicit task AC must appear in at least one Case. Every automatable case explicitly names its target pytest script and exactly one primary symbol so traceability scans only that script/symbol.\n\nName this module file with the stable lowercase business stem `{{item.stem}}` (filename `testcase/md/{{item.stem}}.md`). Priority-only stems `p0`, `p1` and `p2` are forbidden and must never produce `p0.md` or `test_p0.py`. Pure hexadecimal/hash-like opaque stems such as `a401606` and `deadbeef` are also forbidden. Do not use Case-ID-like module filenames. For every automatable case, `自动化映射` must name exactly `testcase/test_{{item.stem}}.py`, where the module stem is this Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `health` → `testcase/test_health.py`; `resource_notes` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.\n\nEvery Case must keep at least one numbered executable line under `### 操作步骤`; a compact variant/result table may follow but must not replace the numbered action anchor. Keep numbered/bulleted independently assertable results under `### 预期结果`. The exact `### 操作步骤` and `### 预期结果` headings must remain present for every Case, including compact/table-based Cases; never compress later Cases by dropping required headings. Every result must name the observable HTTP status, response field/value, state transition or membership condition, never vague wording such as ‘符合预期’.\n\nIn every `自动化映射`, use exactly these machine-readable list labels: `脚本`, `primary symbol`, `变体测试点`, `场景断言测试点`, `横切证据测试点`. Each Test Point from `### 测试点` must appear in exactly one binding list, and every Test Point named in any binding list must also be declared in that Case's `### 测试点`; write `无` for an empty list. A variant Test Point is atomic: one exact endpoint/input/precondition/outcome row equals one exact pytest item and one exact TP ID. If a parameter table has five rows, declare five distinct variant TP IDs in Markdown; never declare one family TP and append row suffixes only in pytest. Classify as `variant` only when endpoint, request input, precondition business state, or expected outcome genuinely changes and therefore needs an independent pytest parameter item. Classify CRUD checkpoints, status/body/header/schema assertions and multiple checks over the same response/journey as `assertion`; classify shared HTTP logging/redaction/truncation evidence as `cross-cutting`. Never create a Test Point merely to parameterize a checkpoint. Every non-cross-cutting TP ID is owned by exactly one Case; when the same response/schema/error assertion is needed in different Cases, use distinct Case-specific TP IDs instead of reusing one assertion TP across Cases. Keep the script path identical to the module one-to-one path and declare exactly one primary symbol named with the canonical Case prefix, for example `BE-RN-003` → `test_BE_RN_003_<description>`; non-Case-prefixed primary symbols are forbidden because parameterized item association must remain deterministic. For redaction scenarios, list sensitive header/field key names only. Never write any header-name-and-value pair, credential placeholder, fake token, anti-example, or other secret-shaped literal in Markdown; state only that a test-only value is supplied at runtime and omitted. Put implementation-only restrictions in a concise `<details>` block rather than dominating the main case flow. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.\n\n## Derived task contract: 需求.md\n\n# Backend test\n- AC-001 proof\n\n## Authoritative reference index\n\n[]\n\nFor each index entry, use `readPath` for Pi read-tool calls and copy `path` exactly into Markdown Source References. Bound files under .harness/tasks/<taskId>/source/** are read-only inputs: reading them is allowed even though writing .harness/** is forbidden. Never resolve `path` relative to the repository root, search for substitutes, or fall back to docs/** when a bound read fails.\n\nRead only precise indexed references needed for AC/API/field/rule evidence; references remain authoritative over derived text."
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
244
|
},
|
|
@@ -338,7 +338,7 @@
|
|
|
338
338
|
"subtask_prompt": "Parse testcase/md/README.md and emit exactly one trailing JSON line {modules:[{stem}]} listing every trusted module stem (table-row testcase/md/<stem>.md mentions and canonical [label](./<stem>.md) relative links only). No file writes.",
|
|
339
339
|
"shell": {
|
|
340
340
|
"commands": [
|
|
341
|
-
"node -e \"eval(Buffer.from('Y29uc3QgZnM9cmVxdWlyZSgnZnMnKTsKY29uc3QgcmVhZG1lPWZzLmV4aXN0c1N5bmMoJ3Rlc3RjYXNlL21kL1JFQURNRS5tZCcpP2ZzLnJlYWRGaWxlU3luYygndGVzdGNhc2UvbWQvUkVBRE1FLm1kJywndXRmOCcpOicnOwpjb25zdCBub3JtPXM9PlN0cmluZyhzKS50b0xvd2VyQ2FzZSgpLnJlcGxhY2UoL1teYS16MC05XSsvZywnXycpLnJlcGxhY2UoL15fK3xfKyQvZywnJykucmVwbGFjZSgvXysvZywnXycpOwpjb25zdCBidD1TdHJpbmcuZnJvbUNoYXJDb2RlKDk2KTsKY29uc3Qgc3RyaXBCYWNrdGlja3M9cz0+
|
|
341
|
+
"node -e \"eval(Buffer.from('Y29uc3QgZnM9cmVxdWlyZSgnZnMnKTsKY29uc3QgcmVhZG1lPWZzLmV4aXN0c1N5bmMoJ3Rlc3RjYXNlL21kL1JFQURNRS5tZCcpP2ZzLnJlYWRGaWxlU3luYygndGVzdGNhc2UvbWQvUkVBRE1FLm1kJywndXRmOCcpOicnOwpjb25zdCBub3JtPXM9PlN0cmluZyhzKS50b0xvd2VyQ2FzZSgpLnJlcGxhY2UoL1teYS16MC05XSsvZywnXycpLnJlcGxhY2UoL15fK3xfKyQvZywnJykucmVwbGFjZSgvXysvZywnXycpOwpjb25zdCBidD1TdHJpbmcuZnJvbUNoYXJDb2RlKDk2KTsKY29uc3Qgc3RyaXBCYWNrdGlja3M9cz0+cy5zcGxpdChidCkuam9pbignJyk7CmNvbnN0IGludmFsaWRSZWFzb249cmF3PT57Y29uc3Qgc3Q9bm9ybShyYXcpO2lmKC9ecFswLTJdJC8udGVzdChzdCkpcmV0dXJuICdwcmlvcml0eS1vbmx5LW1vZHVsZS1zdGVtJztpZigvXlthLWZdW2EtZjAtOV17Niw2M30kLy50ZXN0KHN0KSlyZXR1cm4gJ29wYXF1ZS1oYXNoLW1vZHVsZS1zdGVtJztpZihzdD09PSdyZWFkbWUnKXJldHVybiAncmVzZXJ2ZWQtbW9kdWxlLXN0ZW0nO2lmKCEvXlthLXpdW2EtejAtOV9dKiQvLnRlc3Qoc3QpKXJldHVybiAnaW52YWxpZC1zeW50YXgnO2lmKC9eKD86YmV8dHB8YWN8cmVxfGJyKVtfLV0vaS50ZXN0KHN0KSlyZXR1cm4gJ2Nhc2UtbGlrZS1tb2R1bGUtc3RlbSc7cmV0dXJuIG51bGw7fTsKY29uc3QgdmFsaWQ9cmF3PT5pbnZhbGlkUmVhc29uKHJhdyk9PT1udWxsOwpjb25zdCByeE1kUGF0aD0vdGVzdGNhc2VcL21kXC8oW0EtWmEtejAtOV8uLV0rKVwubWQvZzsKY29uc3QgcnhUYWJsZVJvdz0vXHxccyooW0EtWmEtejAtOV8uLV0rKVxzKlx8XHMqdGVzdGNhc2VcL3Rlc3RfL2c7CmNvbnN0IHJ4UmVsTGluaz0vXFtbXlxdXStcXVwoXC5cLyhbQS1aYS16MC05Xy4tXSspXC5tZFwpL2c7CmNvbnN0IGFsbExpbmVzPXJlYWRtZS5yZXBsYWNlKC9cclxuL2csJ1xuJykucmVwbGFjZSgvXHIvZywnXG4nKS5zcGxpdCgnXG4nKTsKY29uc3QgaGVhZGluZ3M9W107Zm9yKGxldCBpPTA7aTxhbGxMaW5lcy5sZW5ndGg7aSsrKXtpZihhbGxMaW5lc1tpXS50cmltKCk9PT0nIyMgTW9kdWxlIEluZGV4JyloZWFkaW5ncy5wdXNoKGkpO30KaWYoaGVhZGluZ3MubGVuZ3RoIT09MSl7cHJvY2Vzcy5zdGRlcnIud3JpdGUoKGhlYWRpbmdzLmxlbmd0aD09PTA/J21pc3NpbmctbW9kdWxlLWluZGV4JzonZHVwbGljYXRlLW1vZHVsZS1pbmRleCcpKyc7IHJlcXVpcmUgZXhhY3RseSBvbmUgZXhhY3QgIyMgTW9kdWxlIEluZGV4IHNlY3Rpb25cbicpO3Byb2Nlc3MuZXhpdCgyKTt9CmNvbnN0IHN0YXJ0PWhlYWRpbmdzWzBdKzE7bGV0IGVuZD1hbGxMaW5lcy5sZW5ndGg7Zm9yKGxldCBpPXN0YXJ0O2k8YWxsTGluZXMubGVuZ3RoO2krKyl7aWYoL14jI1xzK1xTLy50ZXN0KGFsbExpbmVzW2ldLnRyaW0oKSkpe2VuZD1pO2JyZWFrO319CmNvbnN0IHNlY3Rpb249YWxsTGluZXMuc2xpY2Uoc3RhcnQsZW5kKS5qb2luKCdcbicpOwpjb25zdCByYXc9W107CmNvbnN0IGxpbmVzPXNlY3Rpb24uc3BsaXQoJ1xuJykuZmlsdGVyKGw9PmwuaW5jbHVkZXMoJ3wnKSk7CmZvcihjb25zdCBsaW5lIG9mIGxpbmVzKXsKICBjb25zdCBiYXJlPXN0cmlwQmFja3RpY2tzKGxpbmUpOwogIGZvcihjb25zdCBtIG9mIGJhcmUubWF0Y2hBbGwocnhNZFBhdGgpKXtyYXcucHVzaChtWzFdKTt9CiAgZm9yKGNvbnN0IG0gb2YgYmFyZS5tYXRjaEFsbChyeFRhYmxlUm93KSl7aWYodmFsaWQobVsxXSl8fGludmFsaWRSZWFzb24obVsxXSkhPT0naW52YWxpZC1zeW50YXgnKXJhdy5wdXNoKG1bMV0pO30KfQpmb3IoY29uc3QgbSBvZiBzZWN0aW9uLm1hdGNoQWxsKHJ4UmVsTGluaykpe3Jhdy5wdXNoKG1bMV0pO30KY29uc3QgaW52YWxpZD1bXTtmb3IoY29uc3QgciBvZiByYXcpe2NvbnN0IHJlYXNvbj1pbnZhbGlkUmVhc29uKHIpO2lmKHJlYXNvbilpbnZhbGlkLnB1c2goe3N0ZW06bm9ybShyKSxyZWFzb259KTt9CmlmKGludmFsaWQubGVuZ3RoKXtmb3IoY29uc3QgaXRlbSBvZiBpbnZhbGlkKXByb2Nlc3Muc3RkZXJyLndyaXRlKGl0ZW0ucmVhc29uKyc6ICcraXRlbS5zdGVtKyc7IHVzZSBhIHN0YWJsZSBidXNpbmVzcyByZXNvdXJjZS9kb21haW4gc3RlbVxuJyk7cHJvY2Vzcy5leGl0KDIpO30KY29uc3Qgc2Vlbj1uZXcgU2V0KCk7Y29uc3QgbW9kdWxlcz1bXTsKZm9yKGNvbnN0IHIgb2YgcmF3KXtjb25zdCBzdD1ub3JtKHIpO2lmKHZhbGlkKHIpJiYhc2Vlbi5oYXMoc3QpKXtzZWVuLmFkZChzdCk7bW9kdWxlcy5wdXNoKHtzdGVtOnN0fSk7fX0KaWYobW9kdWxlcy5sZW5ndGg+OCl7cHJvY2Vzcy5zdGRlcnIud3JpdGUoJ2V4Y2Vzc2l2ZS1tb2R1bGUtY291bnQ6ICcrbW9kdWxlcy5sZW5ndGgrJyA+IDg7IG1lcmdlIGJ5IHRoZSBzbWFsbGVzdCBzdGFibGUgYnVzaW5lc3MgcmVzb3VyY2UvZG9tYWluIHNldFxuJyk7cHJvY2Vzcy5leGl0KDIpO30KcHJvY2Vzcy5zdGRvdXQud3JpdGUoSlNPTi5zdHJpbmdpZnkoe21vZHVsZXN9KSk7Cg==','base64').toString('utf8'))\""
|
|
342
342
|
],
|
|
343
343
|
"cwd": ".",
|
|
344
344
|
"timeoutMs": 60000
|
|
@@ -369,13 +369,13 @@
|
|
|
369
369
|
"workflowNodeId": "generate-backend-pytest-cases-map",
|
|
370
370
|
"itemsFrom": "$.nodes['materialize-backend-pytest-module-manifest-shell'].output.modules",
|
|
371
371
|
"itemName": "item",
|
|
372
|
-
"maxItems":
|
|
373
|
-
"maxExpandedNodes":
|
|
372
|
+
"maxItems": 8,
|
|
373
|
+
"maxExpandedNodes": 8,
|
|
374
374
|
"childIdPrefix": "generate-backend-pytest-case",
|
|
375
375
|
"tokenBudget": {
|
|
376
|
-
"maxTokensPerCase": 16384,
|
|
377
376
|
"maxTotalTokens": 600000
|
|
378
377
|
},
|
|
378
|
+
"failOnTokenBudgetExhaustion": true,
|
|
379
379
|
"childTask": {
|
|
380
380
|
"executor": "pi",
|
|
381
381
|
"role": "implementer",
|
|
@@ -16,9 +16,9 @@ Use `playwright-cli-case-generator`. Read only `testcase/frontend/rag/context.md
|
|
|
16
16
|
|
|
17
17
|
Each case is independently executable and includes AC mapping (`acIds`), preconditions, cleanup, a semantic assertion, and its isolated evidence path. Use dimensions `core`, `boundary`, `flow`, or `backend`. Never guess API fields, constraints, SLA, credentials, or unrecorded data. For passed authority, include a successful `playwright-cli find ...` after `open` and before controller post-execution cleanup; `snapshot`, `goto`, `screenshot`, `request`/`console`, and ordinary interactions are not assertions.
|
|
18
18
|
|
|
19
|
-
Every case must copy the
|
|
19
|
+
Every case must copy the concrete absolute base URL from `testcase/frontend/rag/context.md` only after the environment shell has marked `environmentProbe: reachable`. Do not derive a different origin and do not leave a base-url placeholder.
|
|
20
20
|
|
|
21
|
-
Every case must start with `playwright-cli open --browser=chrome` followed by
|
|
21
|
+
Every case must start with `playwright-cli open --browser=chrome` followed by that concrete context URL. Do not copy an angle-bracket URL placeholder into a generated executable case line.
|
|
22
22
|
|
|
23
23
|
### Dynamic element refs (hard)
|
|
24
24
|
|