@qatonic_innovations/qaios 0.4.1 → 0.5.1
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 +275 -18
- 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;
|
|
@@ -4197,6 +4300,8 @@ function tempForTier(tier) {
|
|
|
4197
4300
|
}
|
|
4198
4301
|
}
|
|
4199
4302
|
var DEFAULT_LLM_TIMEOUT_MS = 24e4;
|
|
4303
|
+
var DEFAULT_MAX_OUTPUT_TOKENS = 16384;
|
|
4304
|
+
var MAX_OUTPUT_TOKENS_CEILING = 65536;
|
|
4200
4305
|
function llmTimeoutMs() {
|
|
4201
4306
|
const raw = process.env["QAIOS_LLM_TIMEOUT_MS"];
|
|
4202
4307
|
if (raw === void 0) return DEFAULT_LLM_TIMEOUT_MS;
|
|
@@ -4257,6 +4362,7 @@ var SkillRunner = class {
|
|
|
4257
4362
|
const maxAttempts = maxRetries + 1;
|
|
4258
4363
|
let attempt = 0;
|
|
4259
4364
|
let lastValidationError = null;
|
|
4365
|
+
let currentMaxTokens = DEFAULT_MAX_OUTPUT_TOKENS;
|
|
4260
4366
|
while (attempt < maxAttempts) {
|
|
4261
4367
|
attempt++;
|
|
4262
4368
|
const userPromptBase = skill.userPrompt(effectiveCtx, input);
|
|
@@ -4274,9 +4380,9 @@ var SkillRunner = class {
|
|
|
4274
4380
|
// but Sonnet 4.6 routinely hits stopReason=max_tokens at
|
|
4275
4381
|
// that limit — surfacing as a Zod validation error from
|
|
4276
4382
|
// truncated tool input. Sonnet supports up to 128k output;
|
|
4277
|
-
// 16k is
|
|
4278
|
-
//
|
|
4279
|
-
maxTokens:
|
|
4383
|
+
// 16k is the default headroom, escalated on truncation-retry
|
|
4384
|
+
// (see currentMaxTokens) for genuinely large suites.
|
|
4385
|
+
maxTokens: currentMaxTokens,
|
|
4280
4386
|
temperature: tempForTier(skill.modelTier)
|
|
4281
4387
|
};
|
|
4282
4388
|
const signal = buildCallSignal(ctx.cancelSignal);
|
|
@@ -4329,14 +4435,21 @@ var SkillRunner = class {
|
|
|
4329
4435
|
});
|
|
4330
4436
|
if (response.stopReason === "max_tokens") {
|
|
4331
4437
|
if (attempt < maxAttempts) {
|
|
4438
|
+
const escalated = Math.min(currentMaxTokens * 2, MAX_OUTPUT_TOKENS_CEILING);
|
|
4332
4439
|
ctx.auditLogger.append({
|
|
4333
4440
|
workflowId: ctx.workflowId,
|
|
4334
4441
|
phase: skill.phase,
|
|
4335
4442
|
skillId: skill.id,
|
|
4336
4443
|
event: "skill.retry",
|
|
4337
|
-
payload: {
|
|
4444
|
+
payload: {
|
|
4445
|
+
attempt,
|
|
4446
|
+
reason: "output_truncated_max_tokens",
|
|
4447
|
+
maxTokensWas: currentMaxTokens,
|
|
4448
|
+
maxTokensNext: escalated
|
|
4449
|
+
},
|
|
4338
4450
|
actor: "agent"
|
|
4339
4451
|
});
|
|
4452
|
+
currentMaxTokens = escalated;
|
|
4340
4453
|
continue;
|
|
4341
4454
|
}
|
|
4342
4455
|
ctx.auditLogger.append({
|
|
@@ -5237,7 +5350,7 @@ function extractLocatorToken(expr) {
|
|
|
5237
5350
|
}
|
|
5238
5351
|
return null;
|
|
5239
5352
|
}
|
|
5240
|
-
function
|
|
5353
|
+
function countOccurrences2(haystack, needle) {
|
|
5241
5354
|
if (needle.length === 0) return 0;
|
|
5242
5355
|
let n = 0;
|
|
5243
5356
|
let i = haystack.indexOf(needle);
|
|
@@ -5281,7 +5394,7 @@ function applyLocatorFix(opts) {
|
|
|
5281
5394
|
const cwd = opts.cwd ?? process.cwd();
|
|
5282
5395
|
const absPath = path15.isAbsolute(opts.testFile) ? opts.testFile : path15.resolve(cwd, opts.testFile);
|
|
5283
5396
|
const oldContents = readFileSync(absPath, "utf-8");
|
|
5284
|
-
const occurrences =
|
|
5397
|
+
const occurrences = countOccurrences2(oldContents, opts.fix.oldLocator);
|
|
5285
5398
|
if (occurrences === 0) {
|
|
5286
5399
|
return {
|
|
5287
5400
|
applied: false,
|
|
@@ -6059,12 +6172,65 @@ var writing = async (deps) => {
|
|
|
6059
6172
|
const appContext = buildAppContext(deps.args);
|
|
6060
6173
|
appContext["pomMode"] = typeof deps.args["pomMode"] === "string" && (deps.args["pomMode"] === "off" || deps.args["pomMode"] === "auto" || deps.args["pomMode"] === "on") ? deps.args["pomMode"] : "auto";
|
|
6061
6174
|
appContext["framework"] = "playwright";
|
|
6175
|
+
const groundUrl = baseUrlFromArgs(deps.args);
|
|
6176
|
+
const groundDisabled = deps.args["noGround"] === true;
|
|
6177
|
+
const groundTimeoutMs = typeof deps.args["groundCaptureTimeoutMs"] === "number" ? deps.args["groundCaptureTimeoutMs"] : 15e3;
|
|
6178
|
+
if (!groundDisabled && groundUrl.length > 0 && deps.capturePageSnapshot !== void 0) {
|
|
6179
|
+
const cwd = typeof deps.args["cwd"] === "string" ? deps.args["cwd"] : process.cwd();
|
|
6180
|
+
try {
|
|
6181
|
+
const snapshot2 = await deps.capturePageSnapshot({
|
|
6182
|
+
url: groundUrl,
|
|
6183
|
+
cwd,
|
|
6184
|
+
timeoutMs: groundTimeoutMs
|
|
6185
|
+
});
|
|
6186
|
+
if (snapshot2 !== null) {
|
|
6187
|
+
appContext["pageSnapshot"] = snapshot2;
|
|
6188
|
+
auditGrounding(deps, "grounding_captured", { url: groundUrl });
|
|
6189
|
+
} else {
|
|
6190
|
+
auditGrounding(deps, "grounding_skipped", {
|
|
6191
|
+
reason: "page capture returned no snapshot",
|
|
6192
|
+
url: groundUrl
|
|
6193
|
+
});
|
|
6194
|
+
}
|
|
6195
|
+
} catch (err) {
|
|
6196
|
+
auditGrounding(deps, "grounding_skipped", {
|
|
6197
|
+
reason: `page capture failed: ${err.message}`,
|
|
6198
|
+
url: groundUrl
|
|
6199
|
+
});
|
|
6200
|
+
}
|
|
6201
|
+
}
|
|
6062
6202
|
const input = {
|
|
6063
6203
|
designSpec: designOutput?.["designSpec"] ?? null,
|
|
6064
6204
|
appContext,
|
|
6065
6205
|
testDir: typeof deps.args["testDir"] === "string" ? deps.args["testDir"] : "tests"
|
|
6066
6206
|
};
|
|
6067
|
-
|
|
6207
|
+
const result = await runSkill(deps, skillId, input, "writing_gate");
|
|
6208
|
+
const snapshot = appContext["pageSnapshot"];
|
|
6209
|
+
if (snapshot !== void 0) {
|
|
6210
|
+
try {
|
|
6211
|
+
const out = result.skillExecution?.output;
|
|
6212
|
+
const files = Array.isArray(out?.files) ? out.files : [];
|
|
6213
|
+
const specText = files.filter((f) => f.kind === "spec" && typeof f.contents === "string").map((f) => f.contents).join("\n");
|
|
6214
|
+
const verification = verifyLocatorsAgainstSnapshot(
|
|
6215
|
+
specText,
|
|
6216
|
+
snapshot
|
|
6217
|
+
);
|
|
6218
|
+
if (verification.problems.length > 0) {
|
|
6219
|
+
const summary = summarizeLocatorProblems(verification);
|
|
6220
|
+
auditGrounding(deps, "grounding_warning", {
|
|
6221
|
+
problems: summary,
|
|
6222
|
+
okCount: verification.okCount,
|
|
6223
|
+
uncheckedCount: verification.uncheckedCount
|
|
6224
|
+
});
|
|
6225
|
+
result.warnings = [
|
|
6226
|
+
`${summary.length} generated locator(s) don't cleanly match the live page:`,
|
|
6227
|
+
...summary.map((s) => ` \u2022 ${s}`)
|
|
6228
|
+
];
|
|
6229
|
+
}
|
|
6230
|
+
} catch {
|
|
6231
|
+
}
|
|
6232
|
+
}
|
|
6233
|
+
return result;
|
|
6068
6234
|
};
|
|
6069
6235
|
var writingGate = async (deps) => {
|
|
6070
6236
|
const writeKey2 = Object.keys(deps.prior).find((k) => k.startsWith("write."));
|
|
@@ -6441,6 +6607,16 @@ function auditHeal(deps, kind, payload) {
|
|
|
6441
6607
|
actor: "system"
|
|
6442
6608
|
});
|
|
6443
6609
|
}
|
|
6610
|
+
function auditGrounding(deps, kind, payload) {
|
|
6611
|
+
deps.auditLogger.append({
|
|
6612
|
+
workflowId: deps.workflow.id,
|
|
6613
|
+
phase: "WRITING",
|
|
6614
|
+
skillId: "write.web",
|
|
6615
|
+
event: "skill.completed",
|
|
6616
|
+
payload: { kind: `write.${kind}`, ...payload },
|
|
6617
|
+
actor: "system"
|
|
6618
|
+
});
|
|
6619
|
+
}
|
|
6444
6620
|
function auditDefect(deps, kind, payload) {
|
|
6445
6621
|
deps.auditLogger.append({
|
|
6446
6622
|
workflowId: deps.workflow.id,
|
|
@@ -6583,7 +6759,11 @@ var Orchestrator = class {
|
|
|
6583
6759
|
this.costTracker = new CostTracker(deps.storage.db);
|
|
6584
6760
|
this.gatesRepo = new GatesRepository(deps.storage.db);
|
|
6585
6761
|
this.costCap = deps.costCap === null ? null : deps.costCap ?? DEFAULT_COST_CAP;
|
|
6586
|
-
this.capturePageSnapshot = deps.capturePageSnapshot ?? ((o) => capturePageSnapshot({
|
|
6762
|
+
this.capturePageSnapshot = deps.capturePageSnapshot ?? ((o) => capturePageSnapshot({
|
|
6763
|
+
url: o.url,
|
|
6764
|
+
cwd: o.cwd,
|
|
6765
|
+
...o.timeoutMs !== void 0 ? { timeoutMs: o.timeoutMs } : {}
|
|
6766
|
+
}));
|
|
6587
6767
|
}
|
|
6588
6768
|
deps;
|
|
6589
6769
|
workflowsRepo;
|
|
@@ -6977,7 +7157,8 @@ var Orchestrator = class {
|
|
|
6977
7157
|
workflowId,
|
|
6978
7158
|
skillId: result.skillExecution.skillId,
|
|
6979
7159
|
durationMs: Date.now() - skillStart,
|
|
6980
|
-
output: result.skillExecution.output
|
|
7160
|
+
output: result.skillExecution.output,
|
|
7161
|
+
...result.warnings !== void 0 && result.warnings.length > 0 ? { warnings: result.warnings } : {}
|
|
6981
7162
|
});
|
|
6982
7163
|
}
|
|
6983
7164
|
if (this.costCap !== null) {
|
|
@@ -10045,10 +10226,23 @@ var WriteWebInput = z.object({
|
|
|
10045
10226
|
// Defaults to Playwright's own default (`data-testid`); when the app
|
|
10046
10227
|
// uses something else (e.g. `data-test`), the generated spec must set
|
|
10047
10228
|
// it via `test.use({ testIdAttribute })` or getByTestId finds nothing.
|
|
10048
|
-
testIdAttribute: z.string().default("data-testid")
|
|
10229
|
+
testIdAttribute: z.string().default("data-testid"),
|
|
10230
|
+
// 0.5: DOM grounding. When the orchestrator has a baseUrl, it
|
|
10231
|
+
// captures the live page (same headless snapshot heal.locator uses)
|
|
10232
|
+
// and threads it in here so the writer generates locators against
|
|
10233
|
+
// REAL elements instead of guessing from the DesignSpec's text hints.
|
|
10234
|
+
// Optional: API tests and no-baseUrl runs omit it and the writer
|
|
10235
|
+
// behaves byte-for-byte as before. Same shape as HealLocatorInput's
|
|
10236
|
+
// pageSnapshot so both paths share the capture infrastructure.
|
|
10237
|
+
pageSnapshot: z.object({
|
|
10238
|
+
url: z.string(),
|
|
10239
|
+
html: z.string().optional(),
|
|
10240
|
+
accessibilityTree: z.unknown().optional()
|
|
10241
|
+
}).optional()
|
|
10049
10242
|
}),
|
|
10050
10243
|
testDir: z.string().default("tests")
|
|
10051
10244
|
});
|
|
10245
|
+
var WRITE_WEB_HTML_MAX_CHARS = 5e4;
|
|
10052
10246
|
var HEADER_PLACEHOLDERS = {
|
|
10053
10247
|
workflowId: "<ulid will be filled by runtime>",
|
|
10054
10248
|
generatedAt: "<iso-8601 will be filled by runtime>",
|
|
@@ -10090,7 +10284,39 @@ function detectDisallowedImports(contents) {
|
|
|
10090
10284
|
}
|
|
10091
10285
|
return [...specifiers].sort();
|
|
10092
10286
|
}
|
|
10093
|
-
function
|
|
10287
|
+
function truncateHtmlForWriting(html, maxChars = WRITE_WEB_HTML_MAX_CHARS) {
|
|
10288
|
+
if (html.length <= maxChars) return html;
|
|
10289
|
+
return html.slice(0, maxChars) + "\n<!-- truncated -->";
|
|
10290
|
+
}
|
|
10291
|
+
function buildPageGroundingBlock(snapshot) {
|
|
10292
|
+
if (snapshot === void 0) return "";
|
|
10293
|
+
const html = snapshot.html ? truncateHtmlForWriting(snapshot.html) : "(no HTML captured)";
|
|
10294
|
+
const a11y = snapshot.accessibilityTree !== void 0 ? JSON.stringify(snapshot.accessibilityTree, null, 2) : "(no accessibility tree captured)";
|
|
10295
|
+
return `
|
|
10296
|
+
|
|
10297
|
+
REAL PAGE ELEMENTS (captured live from ${snapshot.url}):
|
|
10298
|
+
The HTML and accessibility tree below are the ACTUAL target page. Treat them
|
|
10299
|
+
as ground truth, overriding any locator hint in the DesignSpec that conflicts.
|
|
10300
|
+
- Generate every locator against an element you can SEE here. Do NOT invent
|
|
10301
|
+
test-ids, roles, or text that do not appear below.
|
|
10302
|
+
- Prefer a locator that matches EXACTLY ONE element. If the obvious locator
|
|
10303
|
+
matches several (e.g. several elements share the same role+name), narrow it
|
|
10304
|
+
with a containing region (\`getByRole('navigation').getByRole('link', \u2026)\`),
|
|
10305
|
+
a more specific accessible name, or \`.filter({ hasText })\` \u2014 do NOT reach
|
|
10306
|
+
for \`.nth(n)\` or \`.first()\` to paper over an ambiguous match.
|
|
10307
|
+
- If a DesignSpec step needs an element that genuinely is NOT on this page,
|
|
10308
|
+
emit \`test.fixme(...)\` with a comment and add the gap to \`qaiosMdSuggestions\`
|
|
10309
|
+
\u2014 do not guess a locator.
|
|
10310
|
+
|
|
10311
|
+
Page HTML (truncated):
|
|
10312
|
+
\`\`\`html
|
|
10313
|
+
${html}
|
|
10314
|
+
\`\`\`
|
|
10315
|
+
|
|
10316
|
+
Accessibility tree:
|
|
10317
|
+
${a11y}`;
|
|
10318
|
+
}
|
|
10319
|
+
function buildSystemPrompt12(projectMemory, locatorPreference, knownSelectors, testIdAttribute, pageSnapshot) {
|
|
10094
10320
|
const knownSelectorsRendered = knownSelectors.length === 0 ? "(none)" : knownSelectors.join(", ");
|
|
10095
10321
|
const testIdBlock = !testIdAttribute || testIdAttribute === "data-testid" ? "" : `
|
|
10096
10322
|
- TEST-ID ATTRIBUTE: this project's test ids use the \`${testIdAttribute}\` attribute,
|
|
@@ -10147,7 +10373,7 @@ Project context:
|
|
|
10147
10373
|
${projectMemory}
|
|
10148
10374
|
|
|
10149
10375
|
Locator preference order: ${locatorPreference.join(", ")}
|
|
10150
|
-
Known selectors (use these where applicable): ${knownSelectorsRendered}
|
|
10376
|
+
Known selectors (use these where applicable): ${knownSelectorsRendered}${buildPageGroundingBlock(pageSnapshot)}
|
|
10151
10377
|
|
|
10152
10378
|
Output via the \`submit\` tool.`;
|
|
10153
10379
|
}
|
|
@@ -10175,7 +10401,8 @@ var writeWebSkill = {
|
|
|
10175
10401
|
ctx.projectMemory,
|
|
10176
10402
|
input.appContext.locatorPreference,
|
|
10177
10403
|
input.appContext.knownSelectors,
|
|
10178
|
-
input.appContext.testIdAttribute
|
|
10404
|
+
input.appContext.testIdAttribute,
|
|
10405
|
+
input.appContext.pageSnapshot
|
|
10179
10406
|
),
|
|
10180
10407
|
userPrompt: (_ctx, input) => buildUserPrompt12(input),
|
|
10181
10408
|
confidenceFn: (output, ctx) => {
|
|
@@ -10187,7 +10414,21 @@ var writeWebSkill = {
|
|
|
10187
10414
|
const scan = scanGeneratedTests(output.files, designedIds);
|
|
10188
10415
|
const assertionsPresent = scan.specFileCount === 0 || scan.specFilesWithoutAssertions.length === 0 ? 1 : 0.6;
|
|
10189
10416
|
const idsInContents = scan.unreferencedScenarioIds.length === 0 ? 1 : 0.7;
|
|
10190
|
-
|
|
10417
|
+
const snapshot = ctx.input.appContext.pageSnapshot;
|
|
10418
|
+
let locatorsVerified = 1;
|
|
10419
|
+
if (snapshot !== void 0) {
|
|
10420
|
+
const allSpecText = output.files.filter((f) => f.kind === "spec").map((f) => f.contents).join("\n");
|
|
10421
|
+
const v = verifyLocatorsAgainstSnapshot(allSpecText, snapshot);
|
|
10422
|
+
const checked = v.okCount + v.problems.length;
|
|
10423
|
+
if (checked > 0 && v.problems.length > 0) {
|
|
10424
|
+
const badRatio = v.problems.length / checked;
|
|
10425
|
+
locatorsVerified = Math.max(0.5, 1 - badRatio);
|
|
10426
|
+
}
|
|
10427
|
+
}
|
|
10428
|
+
return Math.max(
|
|
10429
|
+
0,
|
|
10430
|
+
Math.min(1, reported * noSilentDrops * assertionsPresent * idsInContents * locatorsVerified)
|
|
10431
|
+
);
|
|
10191
10432
|
}
|
|
10192
10433
|
};
|
|
10193
10434
|
var WriteApiInput = z.object({
|
|
@@ -14014,7 +14255,9 @@ function riskHotspotsFromMemory2(mem) {
|
|
|
14014
14255
|
var GLYPH6 = {
|
|
14015
14256
|
ok: "\u2713",
|
|
14016
14257
|
failed: "\u2717",
|
|
14017
|
-
blocked: "\u23F8"
|
|
14258
|
+
blocked: "\u23F8",
|
|
14259
|
+
warn: "\u26A0"
|
|
14260
|
+
};
|
|
14018
14261
|
function renderTextLine4(event) {
|
|
14019
14262
|
switch (event.kind) {
|
|
14020
14263
|
case "workflow_started":
|
|
@@ -14024,8 +14267,13 @@ function renderTextLine4(event) {
|
|
|
14024
14267
|
const seconds = (event.durationMs / 1e3).toFixed(1);
|
|
14025
14268
|
const conf = readConfidence2(event.output);
|
|
14026
14269
|
const confSuffix = conf !== null ? ` (confidence ${conf.toFixed(2)})` : "";
|
|
14027
|
-
|
|
14270
|
+
let line = `\u25B8 ${event.skillId.padEnd(22)} ${GLYPH6.ok} ${seconds}s${confSuffix}
|
|
14271
|
+
`;
|
|
14272
|
+
if (Array.isArray(event.warnings) && event.warnings.length > 0) {
|
|
14273
|
+
line += ` ${GLYPH6.warn} ${event.warnings.join("\n ")}
|
|
14028
14274
|
`;
|
|
14275
|
+
}
|
|
14276
|
+
return line;
|
|
14029
14277
|
}
|
|
14030
14278
|
case "gate_evaluated":
|
|
14031
14279
|
return event.decision === "pass" ? `\u25B8 gate.${event.gateType.padEnd(16)} ${GLYPH6.ok} passed
|
|
@@ -14172,6 +14420,11 @@ ${epSummary}`;
|
|
|
14172
14420
|
riskHotspots: riskHotspotsFromMemory2(memory),
|
|
14173
14421
|
knownSelectors: []
|
|
14174
14422
|
};
|
|
14423
|
+
if (testType === "web") {
|
|
14424
|
+
const groundEnabled = opts.noGround === true ? false : testCfg?.groundLocators ?? true;
|
|
14425
|
+
if (!groundEnabled) args["noGround"] = true;
|
|
14426
|
+
args["groundCaptureTimeoutMs"] = testCfg?.groundCaptureTimeoutMs ?? 15e3;
|
|
14427
|
+
}
|
|
14175
14428
|
if (testType === "api" && parsedSpec !== null) {
|
|
14176
14429
|
args["apiContext"] = {
|
|
14177
14430
|
baseUrl: parsedSpec.baseUrl,
|
|
@@ -14465,7 +14718,10 @@ function buildProgram() {
|
|
|
14465
14718
|
"--scope <scope>",
|
|
14466
14719
|
"design scope: minimal (~3 scenarios) | standard (default) | exhaustive (~15-25 scenarios)",
|
|
14467
14720
|
"standard"
|
|
14468
|
-
).option("--run", "execute tests immediately after generating").option(
|
|
14721
|
+
).option("--run", "execute tests immediately after generating").option(
|
|
14722
|
+
"--no-ground",
|
|
14723
|
+
"skip DOM grounding (do not capture the live page before writing locators)"
|
|
14724
|
+
).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
14725
|
"--workflow <id>",
|
|
14470
14726
|
"resume a prior workflow interrupted mid-run instead of starting fresh"
|
|
14471
14727
|
).action(async (description, cmdOpts, command) => {
|
|
@@ -14481,6 +14737,7 @@ function buildProgram() {
|
|
|
14481
14737
|
};
|
|
14482
14738
|
if (typeof cmdOpts["apiSpec"] === "string") opts.apiSpec = cmdOpts["apiSpec"];
|
|
14483
14739
|
if (typeof cmdOpts["baseUrl"] === "string") opts.baseUrl = cmdOpts["baseUrl"];
|
|
14740
|
+
if (cmdOpts["ground"] === false) opts.noGround = true;
|
|
14484
14741
|
if (typeof cmdOpts["out"] === "string") opts.out = cmdOpts["out"];
|
|
14485
14742
|
if (typeof cmdOpts["featureName"] === "string") opts.featureName = cmdOpts["featureName"];
|
|
14486
14743
|
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.1",
|
|
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",
|