@qa-gentic/stlc-agents 1.0.25 → 1.0.27
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/package.json +1 -1
- package/skills/generate-test-cases/SKILL.md +5 -0
- package/src/cli/cmd-cost.js +61 -30
- package/src/cli/cmd-init.js +88 -8
- package/src/stlc_agents/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/__pycache__/server.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/ado_gherkin.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/__pycache__/server.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/server.py +41 -6
- package/src/stlc_agents/agent_helix_writer/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/tools/__pycache__/boilerplate.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/tools/__pycache__/helix_write.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/__pycache__/server.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/server.py +419 -213
- package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/ado_attach.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/__pycache__/server.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/server.py +12 -0
- package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/ado_workitem.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/tools/ado_workitem.py +65 -1
- package/src/stlc_agents/shared/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/auth.cpython-314.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/cost_tracker.cpython-314.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/pricing.cpython-314.pyc +0 -0
- package/src/stlc_agents/shared/cost_tracker.py +378 -70
- package/src/stlc_agents/shared/pricing.py +115 -24
- package/src/stlc_agents/webhook_orchestrator/__init__.py +0 -0
- package/src/stlc_agents/webhook_orchestrator/agent_runner.py +599 -0
- package/src/stlc_agents/webhook_orchestrator/main.py +43 -0
- package/src/stlc_agents/webhook_orchestrator/models.py +63 -0
- package/src/stlc_agents/webhook_orchestrator/orchestrator.py +103 -0
- package/src/stlc_agents/webhook_orchestrator/pipelines/__init__.py +0 -0
- package/src/stlc_agents/webhook_orchestrator/pipelines/_base.py +57 -0
- package/src/stlc_agents/webhook_orchestrator/pipelines/ado_test_cases.py +55 -0
- package/src/stlc_agents/webhook_orchestrator/pipelines/full_pipeline.py +202 -0
- package/src/stlc_agents/webhook_orchestrator/pipelines/gherkin_playwright.py +156 -0
- package/src/stlc_agents/webhook_orchestrator/pipelines/jira_test_cases.py +48 -0
- package/src/stlc_agents/webhook_orchestrator/webhook_bridge.py +368 -0
- package/src/stlc_agents/agent_gherkin_generator/__pycache__/server.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/__pycache__/server.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_jira_manager/__pycache__/server.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/__pycache__/server.cpython-310.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/cost_tracker.cpython-310.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/pricing.cpython-310.pyc +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qa-gentic/stlc-agents",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.27",
|
|
4
4
|
"description": "QA STLC Agents — five MCP servers + skills for AI-powered test case, Gherkin, Playwright generation, and Helix-QA file writing against Azure DevOps and Jira Cloud. Full pipeline for both: fetch → test cases → Gherkin → Playwright → Helix-QA. Works with Claude Code, GitHub Copilot, Cursor, Windsurf.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"playwright",
|
|
@@ -152,6 +152,11 @@ Each test case:
|
|
|
152
152
|
- `steps` — array of `{ action, expected_result }` — at least 2 steps per TC
|
|
153
153
|
- `priority` — 1=Critical, 2=High (default), 3=Medium, 4=Low
|
|
154
154
|
|
|
155
|
+
**Automatic post-creation actions (server-side — no skill action required):**
|
|
156
|
+
- Each `TestedBy-Forward` relation is created with `"attributes": {"comment": "STLC-Agent generated test case"}` — this comment appears in the Links tab Comments column in ADO.
|
|
157
|
+
- The tag `STLCAgentTestCases` is appended to the parent work item (PBI/Bug/Feature).
|
|
158
|
+
Both are best-effort; a failure does not roll back test case creation.
|
|
159
|
+
|
|
155
160
|
## Example tool call
|
|
156
161
|
|
|
157
162
|
```json
|
package/src/cli/cmd-cost.js
CHANGED
|
@@ -124,64 +124,88 @@ function readLogs() {
|
|
|
124
124
|
|
|
125
125
|
// ── Printing ───────────────────────────────────────────────────────────────
|
|
126
126
|
|
|
127
|
+
function fmtTokens(r) {
|
|
128
|
+
const n = r.estimated_tokens || 0;
|
|
129
|
+
const exact = r.token_method === "exact";
|
|
130
|
+
const prefix = exact ? "" : "~";
|
|
131
|
+
return prefix + fmtTok(n);
|
|
132
|
+
}
|
|
133
|
+
|
|
127
134
|
function printSession(sess) {
|
|
128
135
|
const { sessionId, records } = sess;
|
|
129
136
|
if (!records.length) return;
|
|
130
137
|
|
|
131
138
|
const byServer = {};
|
|
132
|
-
let totalCost = 0,
|
|
139
|
+
let totalCost = 0, totalExactTokens = 0, totalEstTokens = 0;
|
|
140
|
+
let hasExact = false, hasEst = false;
|
|
141
|
+
|
|
133
142
|
for (const r of records) {
|
|
134
143
|
const k = r.server || "unknown";
|
|
135
|
-
if (!byServer[k]) byServer[k] = { calls: 0, tokens: 0, cost: 0 };
|
|
144
|
+
if (!byServer[k]) byServer[k] = { calls: 0, tokens: 0, cost: 0, exact: false };
|
|
136
145
|
byServer[k].calls++;
|
|
137
146
|
byServer[k].tokens += r.estimated_tokens || 0;
|
|
138
147
|
byServer[k].cost += r.cost_usd || 0;
|
|
139
|
-
|
|
140
|
-
|
|
148
|
+
if (r.token_method === "exact") { byServer[k].exact = true; hasExact = true; }
|
|
149
|
+
else { hasEst = true; }
|
|
150
|
+
totalCost += r.cost_usd || 0;
|
|
151
|
+
if (r.token_method === "exact") totalExactTokens += r.estimated_tokens || 0;
|
|
152
|
+
else totalEstTokens += r.estimated_tokens || 0;
|
|
141
153
|
}
|
|
142
154
|
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
155
|
+
const methodLabel = hasExact && hasEst ? "mixed (exact + estimated)"
|
|
156
|
+
: hasExact ? "exact (from API response)"
|
|
157
|
+
: "estimated (payload chars÷4)";
|
|
158
|
+
|
|
159
|
+
const models = [...new Set(records.map((r) => r.model).filter(Boolean))].join(", ");
|
|
160
|
+
const sources = [...new Set(records.map((r) => r.model_source).filter(Boolean))].join(", ");
|
|
147
161
|
const ts = records[0]?.timestamp?.slice(0, 19).replace("T", " ") || "";
|
|
148
162
|
const tsEnd = records[records.length - 1]?.timestamp?.slice(0, 19).replace("T", " ") || "";
|
|
149
163
|
|
|
150
|
-
console.log(`\n${"═".repeat(
|
|
164
|
+
console.log(`\n${"═".repeat(72)}`);
|
|
151
165
|
console.log(b(` stlc-agents · Cost Report · ${sessionId}`));
|
|
152
|
-
console.log(`${"═".repeat(
|
|
166
|
+
console.log(`${"═".repeat(72)}`);
|
|
153
167
|
console.log(dim(` ${ts} → ${tsEnd}`));
|
|
154
|
-
console.log(dim(` Model: ${
|
|
155
|
-
console.log(dim(` Token method: ${
|
|
168
|
+
console.log(dim(` Model(s): ${models} (via: ${sources})`));
|
|
169
|
+
console.log(dim(` Token method: ${methodLabel}`));
|
|
156
170
|
|
|
157
171
|
// Per-server
|
|
158
|
-
console.log(`\n ${"Server".padEnd(30)} ${"Calls".padStart(6)} ${"
|
|
159
|
-
console.log(` ${"─".repeat(
|
|
172
|
+
console.log(`\n ${"Server".padEnd(30)} ${"Calls".padStart(6)} ${"Tokens".padStart(10)} ${"Cost (USD)".padStart(14)} Method`);
|
|
173
|
+
console.log(` ${"─".repeat(70)}`);
|
|
160
174
|
for (const [svr, d] of Object.entries(byServer)) {
|
|
175
|
+
const meth = d.exact ? "" : dim(" [est]");
|
|
176
|
+
const prefix = d.exact ? "" : "~";
|
|
161
177
|
console.log(
|
|
162
178
|
` ${cyn(svr.padEnd(30))} ${String(d.calls).padStart(6)} ` +
|
|
163
|
-
`${fmtTok(d.tokens).padStart(10)} ${grn(fmtUsd(d.cost).padStart(14))}`
|
|
179
|
+
`${(prefix + fmtTok(d.tokens)).padStart(10)} ${grn(fmtUsd(d.cost).padStart(14))}${meth}`
|
|
164
180
|
);
|
|
165
181
|
}
|
|
166
182
|
|
|
167
183
|
// Per-step
|
|
168
184
|
console.log(`\n ${"Step detail"}`);
|
|
169
|
-
console.log(` ${"─".repeat(
|
|
185
|
+
console.log(` ${"─".repeat(72)}`);
|
|
170
186
|
for (const r of records) {
|
|
187
|
+
const exact = r.token_method === "exact";
|
|
188
|
+
const methTag = exact ? "" : dim(" [est]");
|
|
189
|
+
const cacheNote = (r.cache_write_tokens || r.cache_read_tokens)
|
|
190
|
+
? dim(` cw=${r.cache_write_tokens||0} cr=${r.cache_read_tokens||0}`) : "";
|
|
191
|
+
const iters = r.iterations > 1 ? dim(` ×${r.iterations}`) : "";
|
|
171
192
|
console.log(
|
|
172
|
-
` ${(r.server || "?").padEnd(26)} ${(r.tool || "?").padEnd(
|
|
173
|
-
`${
|
|
174
|
-
`${grn(fmtUsd(r.cost_usd || 0))} ${fmtMs(r.latency_ms || 0)}
|
|
175
|
-
dim(`[${r.token_method || "?"}]`)
|
|
193
|
+
` ${(r.server || "?").padEnd(26)} ${(r.tool || "?").padEnd(34)} ` +
|
|
194
|
+
`${fmtTokens(r).padStart(8)} ` +
|
|
195
|
+
`${grn(fmtUsd(r.cost_usd || 0))} ${fmtMs(r.latency_ms || 0)}${methTag}${cacheNote}${iters}`
|
|
176
196
|
);
|
|
177
197
|
}
|
|
178
198
|
|
|
179
199
|
// Totals
|
|
180
|
-
|
|
181
|
-
|
|
200
|
+
const totalTokens = totalExactTokens + totalEstTokens;
|
|
201
|
+
const tokenNote = hasExact && hasEst
|
|
202
|
+
? ` (${fmtTok(totalExactTokens)} exact + ~${fmtTok(totalEstTokens)} est)`
|
|
203
|
+
: hasExact ? " (exact)" : " (estimated)";
|
|
204
|
+
console.log(`\n ${"─".repeat(72)}`);
|
|
205
|
+
console.log(` ${"Total tokens".padEnd(40)} ${fmtTok(totalTokens).padStart(10)}${dim(tokenNote)}`);
|
|
182
206
|
console.log(` ${b("Total cost".padEnd(40))} ${grn(fmtUsd(totalCost))}`);
|
|
183
207
|
console.log(dim(` Log: ${sess.file}`));
|
|
184
|
-
console.log(`${"═".repeat(
|
|
208
|
+
console.log(`${"═".repeat(72)}\n`);
|
|
185
209
|
}
|
|
186
210
|
|
|
187
211
|
// ── Main ───────────────────────────────────────────────────────────────────
|
|
@@ -235,16 +259,23 @@ module.exports = async function cost(opts) {
|
|
|
235
259
|
|
|
236
260
|
if (opts.all) {
|
|
237
261
|
for (const s of sessions) printSession(s);
|
|
238
|
-
const allRecords
|
|
239
|
-
const grandTotal
|
|
240
|
-
const
|
|
241
|
-
|
|
262
|
+
const allRecords = sessions.flatMap((s) => s.records);
|
|
263
|
+
const grandTotal = allRecords.reduce((a, r) => a + (r.cost_usd || 0), 0);
|
|
264
|
+
const grandExact = allRecords.filter((r) => r.token_method === "exact")
|
|
265
|
+
.reduce((a, r) => a + (r.estimated_tokens || 0), 0);
|
|
266
|
+
const grandEst = allRecords.filter((r) => r.token_method !== "exact")
|
|
267
|
+
.reduce((a, r) => a + (r.estimated_tokens || 0), 0);
|
|
268
|
+
const grandTokens = grandExact + grandEst;
|
|
269
|
+
const tokenBreakdown = grandExact && grandEst
|
|
270
|
+
? ` (${fmtTok(grandExact)} exact + ~${fmtTok(grandEst)} est)`
|
|
271
|
+
: grandExact ? " (exact)" : " (estimated)";
|
|
272
|
+
console.log(`${"═".repeat(72)}`);
|
|
242
273
|
console.log(b(` All sessions — grand total`));
|
|
243
|
-
console.log(`${"═".repeat(
|
|
274
|
+
console.log(`${"═".repeat(72)}`);
|
|
244
275
|
console.log(` Sessions : ${sessions.length}`);
|
|
245
|
-
console.log(` Total tokens: ${fmtTok(grandTokens)}`);
|
|
276
|
+
console.log(` Total tokens: ${fmtTok(grandTokens)}${dim(tokenBreakdown)}`);
|
|
246
277
|
console.log(b(` TOTAL COST : ${grn(fmtUsd(grandTotal))}`));
|
|
247
|
-
console.log(`${"═".repeat(
|
|
278
|
+
console.log(`${"═".repeat(72)}\n`);
|
|
248
279
|
return;
|
|
249
280
|
}
|
|
250
281
|
|
package/src/cli/cmd-init.js
CHANGED
|
@@ -80,14 +80,94 @@ module.exports = async function init(opts) {
|
|
|
80
80
|
|
|
81
81
|
// ── 3. pip install qa-gentic-stlc-agents ──────────────────────────────────
|
|
82
82
|
info("Installing qa-gentic-stlc-agents (pip)…");
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
|
|
84
|
+
const IS_WIN = process.platform === "win32";
|
|
85
|
+
const VENV_DIR = path.join(os.homedir(), ".qa-stlc", "venv");
|
|
86
|
+
const venvPython = IS_WIN
|
|
87
|
+
? path.join(VENV_DIR, "Scripts", "python.exe")
|
|
88
|
+
: path.join(VENV_DIR, "bin", "python3");
|
|
89
|
+
|
|
90
|
+
// Helper: check if the package is importable by a given python binary
|
|
91
|
+
function isImportable(pyBin) {
|
|
92
|
+
if (!fs.existsSync(pyBin) && pyBin !== python) return false;
|
|
93
|
+
const r = spawnSync(pyBin, ["-c", "import qa_gentic_stlc_agents"], { encoding: "utf8" });
|
|
94
|
+
return r.status === 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Find a Python 3.10–3.13 binary compatible with qa-gentic-stlc-agents.
|
|
98
|
+
// The package declares Requires-Python >=3.10,<3.14 so we must avoid 3.14+.
|
|
99
|
+
function findCompatiblePython(preferred) {
|
|
100
|
+
const candidates = IS_WIN
|
|
101
|
+
? ["py", "python", "python3"]
|
|
102
|
+
: ["python3.13", "python3.12", "python3.11", "python3.10", preferred];
|
|
103
|
+
|
|
104
|
+
for (const bin of candidates) {
|
|
105
|
+
const r = spawnSync(bin, ["--version"], { encoding: "utf8" });
|
|
106
|
+
if (r.status !== 0) continue;
|
|
107
|
+
const ver = (r.stdout || r.stderr || "").trim();
|
|
108
|
+
const m = ver.match(/Python (\d+)\.(\d+)/);
|
|
109
|
+
if (!m) continue;
|
|
110
|
+
const major = parseInt(m[1]), minor = parseInt(m[2]);
|
|
111
|
+
if (major === 3 && minor >= 10 && minor <= 13) return bin;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
89
114
|
}
|
|
90
|
-
|
|
115
|
+
|
|
116
|
+
// Determine which python to use for MCP servers — may be updated to venv python below
|
|
117
|
+
let resolvedPython = python;
|
|
118
|
+
|
|
119
|
+
if (isImportable(python)) {
|
|
120
|
+
// Already importable by the user-supplied / system python (CI, active venv, etc.)
|
|
121
|
+
ok("qa-gentic-stlc-agents already installed — skipping pip install.");
|
|
122
|
+
} else if (isImportable(venvPython)) {
|
|
123
|
+
// Package found in the persistent qa-stlc venv from a previous run
|
|
124
|
+
ok("qa-gentic-stlc-agents found in ~/.qa-stlc/venv — skipping pip install.");
|
|
125
|
+
resolvedPython = venvPython;
|
|
126
|
+
} else {
|
|
127
|
+
// Create (or reuse) a dedicated venv and install there.
|
|
128
|
+
// Bypasses PEP 668 on Mac/Linux Homebrew Python and works on Windows & CI
|
|
129
|
+
// without requiring elevated permissions or breaking the system Python.
|
|
130
|
+
|
|
131
|
+
// Find a Python 3.10–3.13 binary (package does not support 3.14+ yet)
|
|
132
|
+
const compatPython = findCompatiblePython(python);
|
|
133
|
+
if (!compatPython) {
|
|
134
|
+
die(
|
|
135
|
+
"qa-gentic-stlc-agents requires Python 3.10–3.13 but none was found.\n" +
|
|
136
|
+
" Mac: brew install python@3.13\n" +
|
|
137
|
+
" Linux: sudo apt install python3.13\n" +
|
|
138
|
+
" Windows: install Python 3.13 from python.org\n" +
|
|
139
|
+
" Then re-run: qa-stlc init --python python3.13 --vscode --integration ado"
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const compatPythonVer = (spawnSync(compatPython, ["--version"], { encoding: "utf8" }).stdout || "").trim();
|
|
144
|
+
info(`Using ${compatPython} (${compatPythonVer}) for venv…`);
|
|
145
|
+
|
|
146
|
+
if (!fs.existsSync(VENV_DIR)) {
|
|
147
|
+
info(`Creating Python venv at ${VENV_DIR}…`);
|
|
148
|
+
const mkVenv = spawnSync(compatPython, ["-m", "venv", VENV_DIR], { stdio: "inherit", encoding: "utf8" });
|
|
149
|
+
if (mkVenv.status !== 0) {
|
|
150
|
+
die(`Failed to create venv. Ensure python3-venv is installed:\n ${compatPython} -m venv ${VENV_DIR}`);
|
|
151
|
+
}
|
|
152
|
+
ok("Venv created.");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
info("Installing qa-gentic-stlc-agents into venv…");
|
|
156
|
+
const pip = spawnSync(
|
|
157
|
+
venvPython,
|
|
158
|
+
["-m", "pip", "install", "qa-gentic-stlc-agents>=1.0.1", "--quiet"],
|
|
159
|
+
{ stdio: "inherit", encoding: "utf8" }
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
if (pip.status !== 0) {
|
|
163
|
+
die(`pip install into venv failed. Try manually:\n ${venvPython} -m pip install qa-gentic-stlc-agents`);
|
|
164
|
+
}
|
|
165
|
+
ok("qa-gentic-stlc-agents installed into ~/.qa-stlc/venv.");
|
|
166
|
+
resolvedPython = venvPython;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Propagate the resolved python so MCP config points to the correct interpreter
|
|
170
|
+
opts.python = resolvedPython;
|
|
91
171
|
|
|
92
172
|
// ── 4. Copy ORCHESTRATION_RULES.md to project root ─────────────────────────
|
|
93
173
|
info("Installing ORCHESTRATION_RULES.md to project root…");
|
|
@@ -115,7 +195,7 @@ module.exports = async function init(opts) {
|
|
|
115
195
|
await cmdMcpConfig({
|
|
116
196
|
vscode: opts.vscode || false,
|
|
117
197
|
print: false,
|
|
118
|
-
python:
|
|
198
|
+
python: resolvedPython,
|
|
119
199
|
playwrightPort: "8931",
|
|
120
200
|
integration,
|
|
121
201
|
});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -23,7 +23,9 @@ import asyncio
|
|
|
23
23
|
import json
|
|
24
24
|
import re
|
|
25
25
|
import sys
|
|
26
|
+
import tempfile
|
|
26
27
|
import time
|
|
28
|
+
from pathlib import Path
|
|
27
29
|
|
|
28
30
|
from dotenv import load_dotenv
|
|
29
31
|
from mcp.server import Server
|
|
@@ -298,8 +300,9 @@ async def list_tools() -> list[types.Tool]:
|
|
|
298
300
|
description=(
|
|
299
301
|
"Write generated TypeScript/Gherkin files into the Helix-QA directory layout "
|
|
300
302
|
"with full deduplication and interface adaptation.\n\n"
|
|
301
|
-
"
|
|
302
|
-
"
|
|
303
|
+
"Preferred: pass cache_key from generate_playwright_code — "
|
|
304
|
+
"the server loads the files from disk automatically. "
|
|
305
|
+
"Alternative: pass the 'files' dict directly.\n\n"
|
|
303
306
|
"mode='tests_only' (default, safe to run repeatedly):\n"
|
|
304
307
|
" Writes locators.ts, *.page.ts, *.steps.ts, *.feature only.\n"
|
|
305
308
|
" Infrastructure files (LocatorHealer.ts etc.) are always skipped.\n"
|
|
@@ -326,11 +329,20 @@ async def list_tools() -> list[types.Tool]:
|
|
|
326
329
|
"type": "string",
|
|
327
330
|
"description": "Absolute path to the Helix-QA project root.",
|
|
328
331
|
},
|
|
332
|
+
"cache_key": {
|
|
333
|
+
"type": "string",
|
|
334
|
+
"description": (
|
|
335
|
+
"Cache key returned by generate_playwright_code or "
|
|
336
|
+
"scaffold_locator_repository. Preferred over passing 'files' directly — "
|
|
337
|
+
"the server loads the files from disk automatically."
|
|
338
|
+
),
|
|
339
|
+
},
|
|
329
340
|
"files": {
|
|
330
341
|
"type": "object",
|
|
331
342
|
"description": (
|
|
332
343
|
"Dict of { file_key: file_content } as returned by "
|
|
333
|
-
"generate_playwright_code or scaffold_locator_repository."
|
|
344
|
+
"generate_playwright_code or scaffold_locator_repository. "
|
|
345
|
+
"Use cache_key instead whenever possible."
|
|
334
346
|
),
|
|
335
347
|
"additionalProperties": {"type": "string"},
|
|
336
348
|
},
|
|
@@ -351,7 +363,7 @@ async def list_tools() -> list[types.Tool]:
|
|
|
351
363
|
),
|
|
352
364
|
},
|
|
353
365
|
},
|
|
354
|
-
"required": ["helix_root"
|
|
366
|
+
"required": ["helix_root"],
|
|
355
367
|
},
|
|
356
368
|
),
|
|
357
369
|
types.Tool(
|
|
@@ -458,8 +470,31 @@ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
|
|
|
458
470
|
result["_validation"] = _validate_inspect_result(result)
|
|
459
471
|
|
|
460
472
|
elif name == "write_helix_files":
|
|
473
|
+
# ── Resolve files: cache_key takes priority over inline dict ──
|
|
474
|
+
files = arguments.get("files") or {}
|
|
475
|
+
# Normalise: LLMs sometimes send [{file_name, content}] instead of {path: content}
|
|
476
|
+
if isinstance(files, list):
|
|
477
|
+
files = {
|
|
478
|
+
item.get("file_name") or item.get("path") or item.get("name", ""): item.get("content", "")
|
|
479
|
+
for item in files
|
|
480
|
+
if isinstance(item, dict)
|
|
481
|
+
}
|
|
482
|
+
cache_key = arguments.get("cache_key", "").strip()
|
|
483
|
+
if cache_key and not files:
|
|
484
|
+
_cache_dir = Path(tempfile.gettempdir()) / "stlc_file_cache"
|
|
485
|
+
cache_file = _cache_dir / f"{cache_key}.json"
|
|
486
|
+
if cache_file.exists():
|
|
487
|
+
files = json.loads(cache_file.read_text())
|
|
488
|
+
else:
|
|
489
|
+
result = {
|
|
490
|
+
"success": False,
|
|
491
|
+
"error": f"cache_key '{cache_key}' not found — file does not exist at {cache_file}. "
|
|
492
|
+
"Either pass the files dict directly or call get_generated_files first.",
|
|
493
|
+
"_validation": {"valid": False, "errors": [f"cache_key '{cache_key}' not found"], "warnings": []},
|
|
494
|
+
}
|
|
495
|
+
return track(result, tool_name=name, server="qa-helix-writer", t0=t0)
|
|
461
496
|
# ── Pre-write input validation ────────────────────────────────
|
|
462
|
-
input_validation = _validate_write_inputs(
|
|
497
|
+
input_validation = _validate_write_inputs(files)
|
|
463
498
|
if not input_validation["valid"]:
|
|
464
499
|
result = {
|
|
465
500
|
"success": False,
|
|
@@ -475,7 +510,7 @@ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
|
|
|
475
510
|
result = await asyncio.to_thread(
|
|
476
511
|
_write_files,
|
|
477
512
|
arguments["helix_root"],
|
|
478
|
-
|
|
513
|
+
files,
|
|
479
514
|
arguments.get("mode", "tests_only"),
|
|
480
515
|
arguments.get("force_scaffold", False),
|
|
481
516
|
)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|