@qatonic_innovations/qaios 0.4.1 → 0.5.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/dist/index.js +261 -14
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -905,7 +905,19 @@ var QaiosConfig = z.object({
|
|
|
905
905
|
// Playwright defaults to `data-testid`; apps using `data-test`,
|
|
906
906
|
// `data-qa`, etc. must declare it here so generated specs emit a
|
|
907
907
|
// `test.use({ testIdAttribute })` and actually find their elements.
|
|
908
|
-
testIdAttribute: z.string().default("data-testid")
|
|
908
|
+
testIdAttribute: z.string().default("data-testid"),
|
|
909
|
+
// 0.5 DOM grounding: when true (default), `qaios test` captures the
|
|
910
|
+
// live page at `app.baseUrl` before writing and feeds the real DOM
|
|
911
|
+
// to write.web so it generates locators against actual elements
|
|
912
|
+
// instead of guessing from the DesignSpec. Needs the app reachable
|
|
913
|
+
// at generation time; when it isn't, capture fails fast (see
|
|
914
|
+
// groundCaptureTimeoutMs) and falls back to blind generation. Set
|
|
915
|
+
// false to skip capture entirely (also: `qaios test --no-ground`).
|
|
916
|
+
groundLocators: z.boolean().default(true),
|
|
917
|
+
// Hard cap on the headless page-capture before grounding gives up
|
|
918
|
+
// and falls back. Kept tight so a not-yet-running dev server doesn't
|
|
919
|
+
// stall generation — a real capture against a live page is fast.
|
|
920
|
+
groundCaptureTimeoutMs: z.number().int().positive().default(15e3)
|
|
909
921
|
}).default({}),
|
|
910
922
|
visual: z.object({
|
|
911
923
|
viewports: z.array(
|
|
@@ -1103,6 +1115,97 @@ var ulidImpl = monotonicFactory();
|
|
|
1103
1115
|
function ulid() {
|
|
1104
1116
|
return ulidImpl();
|
|
1105
1117
|
}
|
|
1118
|
+
var GET_BY_TESTID_RE = /getByTestId\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
|
|
1119
|
+
var GET_BY_ROLE_RE = /getByRole\(\s*['"`]([^'"`]+)['"`]\s*(?:,\s*\{[^}]*?name\s*:\s*['"`]([^'"`]+)['"`][^}]*\})?\s*\)/g;
|
|
1120
|
+
var GET_BY_TEXT_RE = /getByText\(\s*['"`]([^'"`]+)['"`]/g;
|
|
1121
|
+
function countOccurrences(hay, needle) {
|
|
1122
|
+
if (needle.length === 0) return 0;
|
|
1123
|
+
let count = 0;
|
|
1124
|
+
let from = 0;
|
|
1125
|
+
for (; ; ) {
|
|
1126
|
+
const idx = hay.indexOf(needle, from);
|
|
1127
|
+
if (idx < 0) break;
|
|
1128
|
+
count += 1;
|
|
1129
|
+
from = idx + needle.length;
|
|
1130
|
+
}
|
|
1131
|
+
return count;
|
|
1132
|
+
}
|
|
1133
|
+
function collectA11yNodes(tree, out) {
|
|
1134
|
+
if (tree === null || typeof tree !== "object") return;
|
|
1135
|
+
const node = tree;
|
|
1136
|
+
if (typeof node.role === "string") {
|
|
1137
|
+
out.push({ role: node.role, name: typeof node.name === "string" ? node.name : "" });
|
|
1138
|
+
}
|
|
1139
|
+
if (Array.isArray(node.children)) {
|
|
1140
|
+
for (const child of node.children) collectA11yNodes(child, out);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
function verifyLocatorsAgainstSnapshot(specContents, snapshot) {
|
|
1144
|
+
if (snapshot === void 0) {
|
|
1145
|
+
return { okCount: 0, problems: [], uncheckedCount: 0 };
|
|
1146
|
+
}
|
|
1147
|
+
const html = snapshot.html ?? "";
|
|
1148
|
+
const truncated = html.includes("<!-- truncated -->");
|
|
1149
|
+
const a11yNodes = [];
|
|
1150
|
+
collectA11yNodes(snapshot.accessibilityTree, a11yNodes);
|
|
1151
|
+
const problems = [];
|
|
1152
|
+
let okCount = 0;
|
|
1153
|
+
let uncheckedCount = 0;
|
|
1154
|
+
for (const m of specContents.matchAll(GET_BY_TESTID_RE)) {
|
|
1155
|
+
const id = m[1];
|
|
1156
|
+
const valueHits = countOccurrences(html, `"${id}"`) + countOccurrences(html, `'${id}'`);
|
|
1157
|
+
if (valueHits === 0) {
|
|
1158
|
+
if (truncated) uncheckedCount += 1;
|
|
1159
|
+
else problems.push({ expression: m[0], problem: "missing", count: 0, kind: "testid" });
|
|
1160
|
+
} else if (valueHits > 1) {
|
|
1161
|
+
problems.push({ expression: m[0], problem: "ambiguous", count: valueHits, kind: "testid" });
|
|
1162
|
+
} else {
|
|
1163
|
+
okCount += 1;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
if (a11yNodes.length > 0) {
|
|
1167
|
+
for (const m of specContents.matchAll(GET_BY_ROLE_RE)) {
|
|
1168
|
+
const role = m[1];
|
|
1169
|
+
const name = m[2];
|
|
1170
|
+
const roleMatches = a11yNodes.filter((n) => n.role === role);
|
|
1171
|
+
if (roleMatches.length === 0) {
|
|
1172
|
+
problems.push({ expression: m[0], problem: "missing", count: 0, kind: "role" });
|
|
1173
|
+
} else if (name !== void 0) {
|
|
1174
|
+
const exact = roleMatches.filter((n) => n.name === name);
|
|
1175
|
+
if (exact.length > 1) {
|
|
1176
|
+
problems.push({
|
|
1177
|
+
expression: m[0],
|
|
1178
|
+
problem: "ambiguous",
|
|
1179
|
+
count: exact.length,
|
|
1180
|
+
kind: "role"
|
|
1181
|
+
});
|
|
1182
|
+
} else {
|
|
1183
|
+
okCount += 1;
|
|
1184
|
+
}
|
|
1185
|
+
} else {
|
|
1186
|
+
uncheckedCount += 1;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
} else {
|
|
1190
|
+
for (const _m of specContents.matchAll(GET_BY_ROLE_RE)) uncheckedCount += 1;
|
|
1191
|
+
}
|
|
1192
|
+
for (const m of specContents.matchAll(GET_BY_TEXT_RE)) {
|
|
1193
|
+
const text = m[1];
|
|
1194
|
+
const hits = countOccurrences(html, text);
|
|
1195
|
+
if (hits === 0) {
|
|
1196
|
+
if (truncated) uncheckedCount += 1;
|
|
1197
|
+
else problems.push({ expression: m[0], problem: "missing", count: 0, kind: "text" });
|
|
1198
|
+
} else {
|
|
1199
|
+
okCount += 1;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
return { okCount, problems, uncheckedCount };
|
|
1203
|
+
}
|
|
1204
|
+
function summarizeLocatorProblems(v) {
|
|
1205
|
+
return v.problems.map(
|
|
1206
|
+
(p) => p.problem === "missing" ? `${p.expression} \u2014 matched no element on the captured page` : `${p.expression} \u2014 matched ${p.count} elements (ambiguous; narrow it instead of using .nth/.first)`
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1106
1209
|
var QaiosError = class extends Error {
|
|
1107
1210
|
code;
|
|
1108
1211
|
detail;
|
|
@@ -5237,7 +5340,7 @@ function extractLocatorToken(expr) {
|
|
|
5237
5340
|
}
|
|
5238
5341
|
return null;
|
|
5239
5342
|
}
|
|
5240
|
-
function
|
|
5343
|
+
function countOccurrences2(haystack, needle) {
|
|
5241
5344
|
if (needle.length === 0) return 0;
|
|
5242
5345
|
let n = 0;
|
|
5243
5346
|
let i = haystack.indexOf(needle);
|
|
@@ -5281,7 +5384,7 @@ function applyLocatorFix(opts) {
|
|
|
5281
5384
|
const cwd = opts.cwd ?? process.cwd();
|
|
5282
5385
|
const absPath = path15.isAbsolute(opts.testFile) ? opts.testFile : path15.resolve(cwd, opts.testFile);
|
|
5283
5386
|
const oldContents = readFileSync(absPath, "utf-8");
|
|
5284
|
-
const occurrences =
|
|
5387
|
+
const occurrences = countOccurrences2(oldContents, opts.fix.oldLocator);
|
|
5285
5388
|
if (occurrences === 0) {
|
|
5286
5389
|
return {
|
|
5287
5390
|
applied: false,
|
|
@@ -6059,12 +6162,65 @@ var writing = async (deps) => {
|
|
|
6059
6162
|
const appContext = buildAppContext(deps.args);
|
|
6060
6163
|
appContext["pomMode"] = typeof deps.args["pomMode"] === "string" && (deps.args["pomMode"] === "off" || deps.args["pomMode"] === "auto" || deps.args["pomMode"] === "on") ? deps.args["pomMode"] : "auto";
|
|
6061
6164
|
appContext["framework"] = "playwright";
|
|
6165
|
+
const groundUrl = baseUrlFromArgs(deps.args);
|
|
6166
|
+
const groundDisabled = deps.args["noGround"] === true;
|
|
6167
|
+
const groundTimeoutMs = typeof deps.args["groundCaptureTimeoutMs"] === "number" ? deps.args["groundCaptureTimeoutMs"] : 15e3;
|
|
6168
|
+
if (!groundDisabled && groundUrl.length > 0 && deps.capturePageSnapshot !== void 0) {
|
|
6169
|
+
const cwd = typeof deps.args["cwd"] === "string" ? deps.args["cwd"] : process.cwd();
|
|
6170
|
+
try {
|
|
6171
|
+
const snapshot2 = await deps.capturePageSnapshot({
|
|
6172
|
+
url: groundUrl,
|
|
6173
|
+
cwd,
|
|
6174
|
+
timeoutMs: groundTimeoutMs
|
|
6175
|
+
});
|
|
6176
|
+
if (snapshot2 !== null) {
|
|
6177
|
+
appContext["pageSnapshot"] = snapshot2;
|
|
6178
|
+
auditGrounding(deps, "grounding_captured", { url: groundUrl });
|
|
6179
|
+
} else {
|
|
6180
|
+
auditGrounding(deps, "grounding_skipped", {
|
|
6181
|
+
reason: "page capture returned no snapshot",
|
|
6182
|
+
url: groundUrl
|
|
6183
|
+
});
|
|
6184
|
+
}
|
|
6185
|
+
} catch (err) {
|
|
6186
|
+
auditGrounding(deps, "grounding_skipped", {
|
|
6187
|
+
reason: `page capture failed: ${err.message}`,
|
|
6188
|
+
url: groundUrl
|
|
6189
|
+
});
|
|
6190
|
+
}
|
|
6191
|
+
}
|
|
6062
6192
|
const input = {
|
|
6063
6193
|
designSpec: designOutput?.["designSpec"] ?? null,
|
|
6064
6194
|
appContext,
|
|
6065
6195
|
testDir: typeof deps.args["testDir"] === "string" ? deps.args["testDir"] : "tests"
|
|
6066
6196
|
};
|
|
6067
|
-
|
|
6197
|
+
const result = await runSkill(deps, skillId, input, "writing_gate");
|
|
6198
|
+
const snapshot = appContext["pageSnapshot"];
|
|
6199
|
+
if (snapshot !== void 0) {
|
|
6200
|
+
try {
|
|
6201
|
+
const out = result.skillExecution?.output;
|
|
6202
|
+
const files = Array.isArray(out?.files) ? out.files : [];
|
|
6203
|
+
const specText = files.filter((f) => f.kind === "spec" && typeof f.contents === "string").map((f) => f.contents).join("\n");
|
|
6204
|
+
const verification = verifyLocatorsAgainstSnapshot(
|
|
6205
|
+
specText,
|
|
6206
|
+
snapshot
|
|
6207
|
+
);
|
|
6208
|
+
if (verification.problems.length > 0) {
|
|
6209
|
+
const summary = summarizeLocatorProblems(verification);
|
|
6210
|
+
auditGrounding(deps, "grounding_warning", {
|
|
6211
|
+
problems: summary,
|
|
6212
|
+
okCount: verification.okCount,
|
|
6213
|
+
uncheckedCount: verification.uncheckedCount
|
|
6214
|
+
});
|
|
6215
|
+
result.warnings = [
|
|
6216
|
+
`${summary.length} generated locator(s) don't cleanly match the live page:`,
|
|
6217
|
+
...summary.map((s) => ` \u2022 ${s}`)
|
|
6218
|
+
];
|
|
6219
|
+
}
|
|
6220
|
+
} catch {
|
|
6221
|
+
}
|
|
6222
|
+
}
|
|
6223
|
+
return result;
|
|
6068
6224
|
};
|
|
6069
6225
|
var writingGate = async (deps) => {
|
|
6070
6226
|
const writeKey2 = Object.keys(deps.prior).find((k) => k.startsWith("write."));
|
|
@@ -6441,6 +6597,16 @@ function auditHeal(deps, kind, payload) {
|
|
|
6441
6597
|
actor: "system"
|
|
6442
6598
|
});
|
|
6443
6599
|
}
|
|
6600
|
+
function auditGrounding(deps, kind, payload) {
|
|
6601
|
+
deps.auditLogger.append({
|
|
6602
|
+
workflowId: deps.workflow.id,
|
|
6603
|
+
phase: "WRITING",
|
|
6604
|
+
skillId: "write.web",
|
|
6605
|
+
event: "skill.completed",
|
|
6606
|
+
payload: { kind: `write.${kind}`, ...payload },
|
|
6607
|
+
actor: "system"
|
|
6608
|
+
});
|
|
6609
|
+
}
|
|
6444
6610
|
function auditDefect(deps, kind, payload) {
|
|
6445
6611
|
deps.auditLogger.append({
|
|
6446
6612
|
workflowId: deps.workflow.id,
|
|
@@ -6583,7 +6749,11 @@ var Orchestrator = class {
|
|
|
6583
6749
|
this.costTracker = new CostTracker(deps.storage.db);
|
|
6584
6750
|
this.gatesRepo = new GatesRepository(deps.storage.db);
|
|
6585
6751
|
this.costCap = deps.costCap === null ? null : deps.costCap ?? DEFAULT_COST_CAP;
|
|
6586
|
-
this.capturePageSnapshot = deps.capturePageSnapshot ?? ((o) => capturePageSnapshot({
|
|
6752
|
+
this.capturePageSnapshot = deps.capturePageSnapshot ?? ((o) => capturePageSnapshot({
|
|
6753
|
+
url: o.url,
|
|
6754
|
+
cwd: o.cwd,
|
|
6755
|
+
...o.timeoutMs !== void 0 ? { timeoutMs: o.timeoutMs } : {}
|
|
6756
|
+
}));
|
|
6587
6757
|
}
|
|
6588
6758
|
deps;
|
|
6589
6759
|
workflowsRepo;
|
|
@@ -6977,7 +7147,8 @@ var Orchestrator = class {
|
|
|
6977
7147
|
workflowId,
|
|
6978
7148
|
skillId: result.skillExecution.skillId,
|
|
6979
7149
|
durationMs: Date.now() - skillStart,
|
|
6980
|
-
output: result.skillExecution.output
|
|
7150
|
+
output: result.skillExecution.output,
|
|
7151
|
+
...result.warnings !== void 0 && result.warnings.length > 0 ? { warnings: result.warnings } : {}
|
|
6981
7152
|
});
|
|
6982
7153
|
}
|
|
6983
7154
|
if (this.costCap !== null) {
|
|
@@ -10045,10 +10216,23 @@ var WriteWebInput = z.object({
|
|
|
10045
10216
|
// Defaults to Playwright's own default (`data-testid`); when the app
|
|
10046
10217
|
// uses something else (e.g. `data-test`), the generated spec must set
|
|
10047
10218
|
// it via `test.use({ testIdAttribute })` or getByTestId finds nothing.
|
|
10048
|
-
testIdAttribute: z.string().default("data-testid")
|
|
10219
|
+
testIdAttribute: z.string().default("data-testid"),
|
|
10220
|
+
// 0.5: DOM grounding. When the orchestrator has a baseUrl, it
|
|
10221
|
+
// captures the live page (same headless snapshot heal.locator uses)
|
|
10222
|
+
// and threads it in here so the writer generates locators against
|
|
10223
|
+
// REAL elements instead of guessing from the DesignSpec's text hints.
|
|
10224
|
+
// Optional: API tests and no-baseUrl runs omit it and the writer
|
|
10225
|
+
// behaves byte-for-byte as before. Same shape as HealLocatorInput's
|
|
10226
|
+
// pageSnapshot so both paths share the capture infrastructure.
|
|
10227
|
+
pageSnapshot: z.object({
|
|
10228
|
+
url: z.string(),
|
|
10229
|
+
html: z.string().optional(),
|
|
10230
|
+
accessibilityTree: z.unknown().optional()
|
|
10231
|
+
}).optional()
|
|
10049
10232
|
}),
|
|
10050
10233
|
testDir: z.string().default("tests")
|
|
10051
10234
|
});
|
|
10235
|
+
var WRITE_WEB_HTML_MAX_CHARS = 5e4;
|
|
10052
10236
|
var HEADER_PLACEHOLDERS = {
|
|
10053
10237
|
workflowId: "<ulid will be filled by runtime>",
|
|
10054
10238
|
generatedAt: "<iso-8601 will be filled by runtime>",
|
|
@@ -10090,7 +10274,39 @@ function detectDisallowedImports(contents) {
|
|
|
10090
10274
|
}
|
|
10091
10275
|
return [...specifiers].sort();
|
|
10092
10276
|
}
|
|
10093
|
-
function
|
|
10277
|
+
function truncateHtmlForWriting(html, maxChars = WRITE_WEB_HTML_MAX_CHARS) {
|
|
10278
|
+
if (html.length <= maxChars) return html;
|
|
10279
|
+
return html.slice(0, maxChars) + "\n<!-- truncated -->";
|
|
10280
|
+
}
|
|
10281
|
+
function buildPageGroundingBlock(snapshot) {
|
|
10282
|
+
if (snapshot === void 0) return "";
|
|
10283
|
+
const html = snapshot.html ? truncateHtmlForWriting(snapshot.html) : "(no HTML captured)";
|
|
10284
|
+
const a11y = snapshot.accessibilityTree !== void 0 ? JSON.stringify(snapshot.accessibilityTree, null, 2) : "(no accessibility tree captured)";
|
|
10285
|
+
return `
|
|
10286
|
+
|
|
10287
|
+
REAL PAGE ELEMENTS (captured live from ${snapshot.url}):
|
|
10288
|
+
The HTML and accessibility tree below are the ACTUAL target page. Treat them
|
|
10289
|
+
as ground truth, overriding any locator hint in the DesignSpec that conflicts.
|
|
10290
|
+
- Generate every locator against an element you can SEE here. Do NOT invent
|
|
10291
|
+
test-ids, roles, or text that do not appear below.
|
|
10292
|
+
- Prefer a locator that matches EXACTLY ONE element. If the obvious locator
|
|
10293
|
+
matches several (e.g. several elements share the same role+name), narrow it
|
|
10294
|
+
with a containing region (\`getByRole('navigation').getByRole('link', \u2026)\`),
|
|
10295
|
+
a more specific accessible name, or \`.filter({ hasText })\` \u2014 do NOT reach
|
|
10296
|
+
for \`.nth(n)\` or \`.first()\` to paper over an ambiguous match.
|
|
10297
|
+
- If a DesignSpec step needs an element that genuinely is NOT on this page,
|
|
10298
|
+
emit \`test.fixme(...)\` with a comment and add the gap to \`qaiosMdSuggestions\`
|
|
10299
|
+
\u2014 do not guess a locator.
|
|
10300
|
+
|
|
10301
|
+
Page HTML (truncated):
|
|
10302
|
+
\`\`\`html
|
|
10303
|
+
${html}
|
|
10304
|
+
\`\`\`
|
|
10305
|
+
|
|
10306
|
+
Accessibility tree:
|
|
10307
|
+
${a11y}`;
|
|
10308
|
+
}
|
|
10309
|
+
function buildSystemPrompt12(projectMemory, locatorPreference, knownSelectors, testIdAttribute, pageSnapshot) {
|
|
10094
10310
|
const knownSelectorsRendered = knownSelectors.length === 0 ? "(none)" : knownSelectors.join(", ");
|
|
10095
10311
|
const testIdBlock = !testIdAttribute || testIdAttribute === "data-testid" ? "" : `
|
|
10096
10312
|
- TEST-ID ATTRIBUTE: this project's test ids use the \`${testIdAttribute}\` attribute,
|
|
@@ -10147,7 +10363,7 @@ Project context:
|
|
|
10147
10363
|
${projectMemory}
|
|
10148
10364
|
|
|
10149
10365
|
Locator preference order: ${locatorPreference.join(", ")}
|
|
10150
|
-
Known selectors (use these where applicable): ${knownSelectorsRendered}
|
|
10366
|
+
Known selectors (use these where applicable): ${knownSelectorsRendered}${buildPageGroundingBlock(pageSnapshot)}
|
|
10151
10367
|
|
|
10152
10368
|
Output via the \`submit\` tool.`;
|
|
10153
10369
|
}
|
|
@@ -10175,7 +10391,8 @@ var writeWebSkill = {
|
|
|
10175
10391
|
ctx.projectMemory,
|
|
10176
10392
|
input.appContext.locatorPreference,
|
|
10177
10393
|
input.appContext.knownSelectors,
|
|
10178
|
-
input.appContext.testIdAttribute
|
|
10394
|
+
input.appContext.testIdAttribute,
|
|
10395
|
+
input.appContext.pageSnapshot
|
|
10179
10396
|
),
|
|
10180
10397
|
userPrompt: (_ctx, input) => buildUserPrompt12(input),
|
|
10181
10398
|
confidenceFn: (output, ctx) => {
|
|
@@ -10187,7 +10404,21 @@ var writeWebSkill = {
|
|
|
10187
10404
|
const scan = scanGeneratedTests(output.files, designedIds);
|
|
10188
10405
|
const assertionsPresent = scan.specFileCount === 0 || scan.specFilesWithoutAssertions.length === 0 ? 1 : 0.6;
|
|
10189
10406
|
const idsInContents = scan.unreferencedScenarioIds.length === 0 ? 1 : 0.7;
|
|
10190
|
-
|
|
10407
|
+
const snapshot = ctx.input.appContext.pageSnapshot;
|
|
10408
|
+
let locatorsVerified = 1;
|
|
10409
|
+
if (snapshot !== void 0) {
|
|
10410
|
+
const allSpecText = output.files.filter((f) => f.kind === "spec").map((f) => f.contents).join("\n");
|
|
10411
|
+
const v = verifyLocatorsAgainstSnapshot(allSpecText, snapshot);
|
|
10412
|
+
const checked = v.okCount + v.problems.length;
|
|
10413
|
+
if (checked > 0 && v.problems.length > 0) {
|
|
10414
|
+
const badRatio = v.problems.length / checked;
|
|
10415
|
+
locatorsVerified = Math.max(0.5, 1 - badRatio);
|
|
10416
|
+
}
|
|
10417
|
+
}
|
|
10418
|
+
return Math.max(
|
|
10419
|
+
0,
|
|
10420
|
+
Math.min(1, reported * noSilentDrops * assertionsPresent * idsInContents * locatorsVerified)
|
|
10421
|
+
);
|
|
10191
10422
|
}
|
|
10192
10423
|
};
|
|
10193
10424
|
var WriteApiInput = z.object({
|
|
@@ -14014,7 +14245,9 @@ function riskHotspotsFromMemory2(mem) {
|
|
|
14014
14245
|
var GLYPH6 = {
|
|
14015
14246
|
ok: "\u2713",
|
|
14016
14247
|
failed: "\u2717",
|
|
14017
|
-
blocked: "\u23F8"
|
|
14248
|
+
blocked: "\u23F8",
|
|
14249
|
+
warn: "\u26A0"
|
|
14250
|
+
};
|
|
14018
14251
|
function renderTextLine4(event) {
|
|
14019
14252
|
switch (event.kind) {
|
|
14020
14253
|
case "workflow_started":
|
|
@@ -14024,8 +14257,13 @@ function renderTextLine4(event) {
|
|
|
14024
14257
|
const seconds = (event.durationMs / 1e3).toFixed(1);
|
|
14025
14258
|
const conf = readConfidence2(event.output);
|
|
14026
14259
|
const confSuffix = conf !== null ? ` (confidence ${conf.toFixed(2)})` : "";
|
|
14027
|
-
|
|
14260
|
+
let line = `\u25B8 ${event.skillId.padEnd(22)} ${GLYPH6.ok} ${seconds}s${confSuffix}
|
|
14028
14261
|
`;
|
|
14262
|
+
if (Array.isArray(event.warnings) && event.warnings.length > 0) {
|
|
14263
|
+
line += ` ${GLYPH6.warn} ${event.warnings.join("\n ")}
|
|
14264
|
+
`;
|
|
14265
|
+
}
|
|
14266
|
+
return line;
|
|
14029
14267
|
}
|
|
14030
14268
|
case "gate_evaluated":
|
|
14031
14269
|
return event.decision === "pass" ? `\u25B8 gate.${event.gateType.padEnd(16)} ${GLYPH6.ok} passed
|
|
@@ -14172,6 +14410,11 @@ ${epSummary}`;
|
|
|
14172
14410
|
riskHotspots: riskHotspotsFromMemory2(memory),
|
|
14173
14411
|
knownSelectors: []
|
|
14174
14412
|
};
|
|
14413
|
+
if (testType === "web") {
|
|
14414
|
+
const groundEnabled = opts.noGround === true ? false : testCfg?.groundLocators ?? true;
|
|
14415
|
+
if (!groundEnabled) args["noGround"] = true;
|
|
14416
|
+
args["groundCaptureTimeoutMs"] = testCfg?.groundCaptureTimeoutMs ?? 15e3;
|
|
14417
|
+
}
|
|
14175
14418
|
if (testType === "api" && parsedSpec !== null) {
|
|
14176
14419
|
args["apiContext"] = {
|
|
14177
14420
|
baseUrl: parsedSpec.baseUrl,
|
|
@@ -14465,7 +14708,10 @@ function buildProgram() {
|
|
|
14465
14708
|
"--scope <scope>",
|
|
14466
14709
|
"design scope: minimal (~3 scenarios) | standard (default) | exhaustive (~15-25 scenarios)",
|
|
14467
14710
|
"standard"
|
|
14468
|
-
).option("--run", "execute tests immediately after generating").option(
|
|
14711
|
+
).option("--run", "execute tests immediately after generating").option(
|
|
14712
|
+
"--no-ground",
|
|
14713
|
+
"skip DOM grounding (do not capture the live page before writing locators)"
|
|
14714
|
+
).option("--out <path>", "override default test output directory").option("--feature-name <name>", "override auto-detected feature name").option("--yes", "auto-approve any blocking gate for this run (non-interactive)").option(
|
|
14469
14715
|
"--workflow <id>",
|
|
14470
14716
|
"resume a prior workflow interrupted mid-run instead of starting fresh"
|
|
14471
14717
|
).action(async (description, cmdOpts, command) => {
|
|
@@ -14481,6 +14727,7 @@ function buildProgram() {
|
|
|
14481
14727
|
};
|
|
14482
14728
|
if (typeof cmdOpts["apiSpec"] === "string") opts.apiSpec = cmdOpts["apiSpec"];
|
|
14483
14729
|
if (typeof cmdOpts["baseUrl"] === "string") opts.baseUrl = cmdOpts["baseUrl"];
|
|
14730
|
+
if (cmdOpts["ground"] === false) opts.noGround = true;
|
|
14484
14731
|
if (typeof cmdOpts["out"] === "string") opts.out = cmdOpts["out"];
|
|
14485
14732
|
if (typeof cmdOpts["featureName"] === "string") opts.featureName = cmdOpts["featureName"];
|
|
14486
14733
|
if (cmdOpts["yes"] === true) opts.yes = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qatonic_innovations/qaios",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AI QA engineer in your terminal — designs, writes, runs, heals, and explores tests for web UI and APIs with audit-grade traceability.",
|
|
6
6
|
"license": "MIT",
|