@qatonic_innovations/qaios 0.3.0 → 0.3.2
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/README.md +4 -2
- package/dist/index.js +103 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,7 +48,9 @@ The published package is `@qatonic_innovations/qaios`; the installed **command i
|
|
|
48
48
|
```
|
|
49
49
|
Prefer **OpenAI**? Set `llm.provider: openai` and export `OPENAI_API_KEY`
|
|
50
50
|
instead — see [Choose your LLM provider](#choose-your-llm-provider) below.
|
|
51
|
-
Keys are read from the environment and **never written to
|
|
51
|
+
Keys are read from the **environment** (`process.env`) and **never written to
|
|
52
|
+
disk** by QAIOS. QAIOS does not auto-load a `.env` file — export the key in
|
|
53
|
+
your shell (or use a `.env` runner like `dotenv-cli`) before running.
|
|
52
54
|
- **Playwright** in your project, for `qaios run` / `snapshot` / `explore` / `a11y`:
|
|
53
55
|
```bash
|
|
54
56
|
npm i -D @playwright/test && npx playwright install
|
|
@@ -123,7 +125,7 @@ Run `qaios <command> --help` for the full option list of any command.
|
|
|
123
125
|
|
|
124
126
|
```bash
|
|
125
127
|
# Generate API tests from an OpenAPI spec
|
|
126
|
-
qaios test --type api --spec ./openapi.yaml "exercise the /orders endpoints"
|
|
128
|
+
qaios test --type api --api-spec ./openapi.yaml "exercise the /orders endpoints"
|
|
127
129
|
|
|
128
130
|
# Run a suite; QAIOS classifies failures and self-heals locator drift
|
|
129
131
|
qaios run
|
package/dist/index.js
CHANGED
|
@@ -843,6 +843,14 @@ var McpServerConfig = z.object({
|
|
|
843
843
|
var QaiosConfig = z.object({
|
|
844
844
|
version: z.literal(1),
|
|
845
845
|
mode: Mode.default("LITE"),
|
|
846
|
+
// The application under test. `baseUrl` is read by run / snapshot / fix /
|
|
847
|
+
// test (CLI `--base-url` overrides it per run). OPTIONAL, not defaulted — a
|
|
848
|
+
// `.default({})` would make `qaios init` serialize an empty `app: {}` stub
|
|
849
|
+
// into every config, and a user later hand-adding an `app:` block would then
|
|
850
|
+
// hit a duplicate-key YAML error. Callers use `config?.app?.baseUrl`.
|
|
851
|
+
app: z.object({
|
|
852
|
+
baseUrl: z.string().url().optional()
|
|
853
|
+
}).optional(),
|
|
846
854
|
llm: z.object({
|
|
847
855
|
// Which LLM provider backs every skill. Default stays anthropic so
|
|
848
856
|
// existing projects are unchanged. Set `openai` to use OpenAI instead;
|
|
@@ -2673,6 +2681,20 @@ function tempForTier(tier) {
|
|
|
2673
2681
|
return 0.1;
|
|
2674
2682
|
}
|
|
2675
2683
|
}
|
|
2684
|
+
var DEFAULT_LLM_TIMEOUT_MS = 12e4;
|
|
2685
|
+
function llmTimeoutMs() {
|
|
2686
|
+
const raw = process.env["QAIOS_LLM_TIMEOUT_MS"];
|
|
2687
|
+
if (raw === void 0) return DEFAULT_LLM_TIMEOUT_MS;
|
|
2688
|
+
const n = Number.parseInt(raw, 10);
|
|
2689
|
+
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_LLM_TIMEOUT_MS;
|
|
2690
|
+
}
|
|
2691
|
+
function buildCallSignal(cancelSignal) {
|
|
2692
|
+
const ms = llmTimeoutMs();
|
|
2693
|
+
if (ms <= 0) return cancelSignal;
|
|
2694
|
+
const timeout = AbortSignal.timeout(ms);
|
|
2695
|
+
if (cancelSignal === void 0) return timeout;
|
|
2696
|
+
return AbortSignal.any([timeout, cancelSignal]);
|
|
2697
|
+
}
|
|
2676
2698
|
function schemaToJsonSchema(schema) {
|
|
2677
2699
|
const probe = schema;
|
|
2678
2700
|
if (typeof probe.toJSONSchema === "function") {
|
|
@@ -2742,7 +2764,8 @@ var SkillRunner = class {
|
|
|
2742
2764
|
maxTokens: 16384,
|
|
2743
2765
|
temperature: tempForTier(skill.modelTier)
|
|
2744
2766
|
};
|
|
2745
|
-
|
|
2767
|
+
const signal = buildCallSignal(ctx.cancelSignal);
|
|
2768
|
+
if (signal !== void 0) callOpts.signal = signal;
|
|
2746
2769
|
response = await ctx.llm.call(callOpts);
|
|
2747
2770
|
} catch (err) {
|
|
2748
2771
|
ctx.auditLogger.append({
|
|
@@ -2978,13 +3001,19 @@ var CostTracker = class {
|
|
|
2978
3001
|
const row = this.db.prepare(
|
|
2979
3002
|
`SELECT
|
|
2980
3003
|
COUNT(*) AS calls,
|
|
2981
|
-
COALESCE(SUM(CAST(json_extract(model_call_json, '$.costUsdCents') AS INTEGER)), 0) AS cents
|
|
3004
|
+
COALESCE(SUM(CAST(json_extract(model_call_json, '$.costUsdCents') AS INTEGER)), 0) AS cents,
|
|
3005
|
+
COALESCE(SUM(
|
|
3006
|
+
COALESCE(CAST(json_extract(model_call_json, '$.inputTokens') AS INTEGER), 0) +
|
|
3007
|
+
COALESCE(CAST(json_extract(model_call_json, '$.outputTokens') AS INTEGER), 0) +
|
|
3008
|
+
COALESCE(CAST(json_extract(model_call_json, '$.cacheReadTokens') AS INTEGER), 0) +
|
|
3009
|
+
COALESCE(CAST(json_extract(model_call_json, '$.cacheWriteTokens') AS INTEGER), 0)
|
|
3010
|
+
), 0) AS tokens
|
|
2982
3011
|
FROM audit_log
|
|
2983
3012
|
WHERE workflow_id = ?
|
|
2984
3013
|
AND event = 'model.called'
|
|
2985
3014
|
AND model_call_json IS NOT NULL`
|
|
2986
3015
|
).get(workflowId);
|
|
2987
|
-
return { calls: row.calls, usdCents: row.cents };
|
|
3016
|
+
return { calls: row.calls, usdCents: row.cents, tokens: row.tokens };
|
|
2988
3017
|
}
|
|
2989
3018
|
/**
|
|
2990
3019
|
* Remaining budget for the workflow. Returns negative numbers when
|
|
@@ -5143,6 +5172,7 @@ var Orchestrator = class {
|
|
|
5143
5172
|
if (to === "completed" || to === "failed" || to === "cancelled") {
|
|
5144
5173
|
const snap = this.costTracker.current(workflowId);
|
|
5145
5174
|
updates.costUsdCents = snap.usdCents;
|
|
5175
|
+
updates.costTokens = snap.tokens;
|
|
5146
5176
|
}
|
|
5147
5177
|
this.workflowsRepo.update(workflowId, updates);
|
|
5148
5178
|
const phase = phaseForState(to);
|
|
@@ -9708,7 +9738,8 @@ async function runFix(opts) {
|
|
|
9708
9738
|
const runResult = await adapter.run({
|
|
9709
9739
|
cwd,
|
|
9710
9740
|
workflowId,
|
|
9711
|
-
pattern: resolvedTestFile
|
|
9741
|
+
pattern: resolvedTestFile,
|
|
9742
|
+
extraArgs: ["--retries=2"]
|
|
9712
9743
|
});
|
|
9713
9744
|
const passed = runResult.runRow.failedCount === 0 && runResult.runRow.status !== "errored";
|
|
9714
9745
|
let rolledBack = false;
|
|
@@ -10514,6 +10545,13 @@ async function runMcp(opts) {
|
|
|
10514
10545
|
if (ownsStorage) storage.close();
|
|
10515
10546
|
}
|
|
10516
10547
|
}
|
|
10548
|
+
function withTimeout(promise, ms, message) {
|
|
10549
|
+
let timer;
|
|
10550
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
10551
|
+
timer = setTimeout(() => reject(new McpError("qaios.mcp.test_timeout", message)), ms);
|
|
10552
|
+
});
|
|
10553
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
10554
|
+
}
|
|
10517
10555
|
function listServers(repo, opts, writeOut) {
|
|
10518
10556
|
const servers = repo.list();
|
|
10519
10557
|
if (opts.json === true) {
|
|
@@ -10653,8 +10691,13 @@ async function testServer(repo, opts, writeOut) {
|
|
|
10653
10691
|
servers: [{ ...config, enabled: true }]
|
|
10654
10692
|
});
|
|
10655
10693
|
const ownsClient = opts.mcpClient === void 0;
|
|
10694
|
+
const MCP_TEST_TIMEOUT_MS = 15e3;
|
|
10656
10695
|
try {
|
|
10657
|
-
const tools = await
|
|
10696
|
+
const tools = await withTimeout(
|
|
10697
|
+
client.listTools(opts.name),
|
|
10698
|
+
MCP_TEST_TIMEOUT_MS,
|
|
10699
|
+
`MCP server "${opts.name}" did not respond within ${MCP_TEST_TIMEOUT_MS / 1e3}s \u2014 is it a valid MCP server?`
|
|
10700
|
+
);
|
|
10658
10701
|
writeOut(`\u2713 Connected to "${opts.name}". Tools (${tools.length}):`);
|
|
10659
10702
|
for (const t of tools) {
|
|
10660
10703
|
writeOut(` - ${t.name}${t.description !== void 0 ? ` \u2014 ${t.description}` : ""}`);
|
|
@@ -10766,7 +10809,8 @@ async function runReview(opts) {
|
|
|
10766
10809
|
resumedWorkflows: []
|
|
10767
10810
|
};
|
|
10768
10811
|
}
|
|
10769
|
-
|
|
10812
|
+
const decide = opts.autoApprove === true ? () => "approve" : opts.decideForTests;
|
|
10813
|
+
if (opts.nonInteractive === true && decide === void 0) {
|
|
10770
10814
|
return {
|
|
10771
10815
|
exitCode: ExitCode.USER_ERROR,
|
|
10772
10816
|
decisions: [],
|
|
@@ -10774,7 +10818,7 @@ async function runReview(opts) {
|
|
|
10774
10818
|
resumedWorkflows: [],
|
|
10775
10819
|
error: {
|
|
10776
10820
|
code: "qaios.review.requires_tty",
|
|
10777
|
-
message: "qaios review needs an interactive terminal \u2014 pass --auto-approve or run without --non-interactive."
|
|
10821
|
+
message: "qaios review needs an interactive terminal \u2014 pass --auto-approve to approve all pending gates non-interactively, or run without --non-interactive."
|
|
10778
10822
|
}
|
|
10779
10823
|
};
|
|
10780
10824
|
}
|
|
@@ -10808,9 +10852,9 @@ async function runReview(opts) {
|
|
|
10808
10852
|
);
|
|
10809
10853
|
}
|
|
10810
10854
|
};
|
|
10811
|
-
if (
|
|
10855
|
+
if (decide !== void 0) {
|
|
10812
10856
|
for (const gate of pending) {
|
|
10813
|
-
await onDecide(gate,
|
|
10857
|
+
await onDecide(gate, decide(gate));
|
|
10814
10858
|
}
|
|
10815
10859
|
return {
|
|
10816
10860
|
exitCode: ExitCode.SUCCESS,
|
|
@@ -10819,15 +10863,15 @@ async function runReview(opts) {
|
|
|
10819
10863
|
resumedWorkflows: Array.from(resumedSet)
|
|
10820
10864
|
};
|
|
10821
10865
|
}
|
|
10822
|
-
const [{ render }, { GateReview: GateReview2 }] = await Promise.all([
|
|
10866
|
+
const [React3, { render }, { GateReview: GateReview2 }] = await Promise.all([
|
|
10867
|
+
import('react'),
|
|
10823
10868
|
import('ink'),
|
|
10824
10869
|
Promise.resolve().then(() => (init_GateReview(), GateReview_exports))
|
|
10825
10870
|
]);
|
|
10826
10871
|
await new Promise((resolve) => {
|
|
10827
10872
|
let chain = Promise.resolve();
|
|
10828
10873
|
const instance = render(
|
|
10829
|
-
|
|
10830
|
-
GateReview2({
|
|
10874
|
+
React3.createElement(GateReview2, {
|
|
10831
10875
|
gates: pending,
|
|
10832
10876
|
onDecide: (gate, action) => {
|
|
10833
10877
|
chain = chain.then(() => onDecide(gate, action));
|
|
@@ -11084,9 +11128,32 @@ var GLYPH5 = {
|
|
|
11084
11128
|
warn: "\u26A0",
|
|
11085
11129
|
blocked: "\u23F8"
|
|
11086
11130
|
};
|
|
11131
|
+
var SnapshotSpecError = class extends Error {
|
|
11132
|
+
constructor(userMessage) {
|
|
11133
|
+
super(userMessage);
|
|
11134
|
+
this.userMessage = userMessage;
|
|
11135
|
+
}
|
|
11136
|
+
userMessage;
|
|
11137
|
+
};
|
|
11087
11138
|
function loadSnapshotSpec(specPath) {
|
|
11088
|
-
|
|
11089
|
-
|
|
11139
|
+
let raw;
|
|
11140
|
+
try {
|
|
11141
|
+
raw = JSON.parse(readFileSync(specPath, "utf-8"));
|
|
11142
|
+
} catch (err) {
|
|
11143
|
+
throw new SnapshotSpecError(`could not read/parse ${specPath}: ${err.message}`);
|
|
11144
|
+
}
|
|
11145
|
+
const rawSnapshots = Array.isArray(raw["snapshots"]) ? raw["snapshots"] : Array.isArray(raw["designSpec"]?.["snapshots"]) ? raw["designSpec"]["snapshots"] : [];
|
|
11146
|
+
const snapshots = [];
|
|
11147
|
+
rawSnapshots.forEach((s, i) => {
|
|
11148
|
+
const parsed = VisualSnapshot.safeParse(s);
|
|
11149
|
+
if (!parsed.success) {
|
|
11150
|
+
const issues = parsed.error.issues.map((iss) => `${iss.path.join(".") || "(root)"}: ${iss.message}`).join("; ");
|
|
11151
|
+
throw new SnapshotSpecError(
|
|
11152
|
+
`spec ${specPath}: snapshots[${i}] is invalid \u2014 ${issues}. Each snapshot needs: id, name, route, setupSteps[], viewports[], priority (P0\u2013P3).`
|
|
11153
|
+
);
|
|
11154
|
+
}
|
|
11155
|
+
snapshots.push(parsed.data);
|
|
11156
|
+
});
|
|
11090
11157
|
return {
|
|
11091
11158
|
snapshots,
|
|
11092
11159
|
...typeof raw["featureName"] === "string" ? { featureName: raw["featureName"] } : {}
|
|
@@ -11134,7 +11201,18 @@ async function runSnapshotCapture(opts) {
|
|
|
11134
11201
|
}
|
|
11135
11202
|
};
|
|
11136
11203
|
}
|
|
11137
|
-
|
|
11204
|
+
let allSnapshots;
|
|
11205
|
+
try {
|
|
11206
|
+
allSnapshots = loadSnapshotSpec(specAbs).snapshots;
|
|
11207
|
+
} catch (err) {
|
|
11208
|
+
if (err instanceof SnapshotSpecError) {
|
|
11209
|
+
return {
|
|
11210
|
+
exitCode: ExitCode.USER_ERROR,
|
|
11211
|
+
error: { code: "qaios.snapshot_capture.invalid_spec", message: err.userMessage }
|
|
11212
|
+
};
|
|
11213
|
+
}
|
|
11214
|
+
throw err;
|
|
11215
|
+
}
|
|
11138
11216
|
if (allSnapshots.length === 0) {
|
|
11139
11217
|
return {
|
|
11140
11218
|
exitCode: ExitCode.USER_ERROR,
|
|
@@ -11584,14 +11662,14 @@ async function runSnapshotReview(opts) {
|
|
|
11584
11662
|
}
|
|
11585
11663
|
};
|
|
11586
11664
|
}
|
|
11587
|
-
const [{ render }, { SnapshotReview: SnapshotReview2 }] = await Promise.all([
|
|
11665
|
+
const [React3, { render }, { SnapshotReview: SnapshotReview2 }] = await Promise.all([
|
|
11666
|
+
import('react'),
|
|
11588
11667
|
import('ink'),
|
|
11589
11668
|
Promise.resolve().then(() => (init_SnapshotReview(), SnapshotReview_exports))
|
|
11590
11669
|
]);
|
|
11591
11670
|
await new Promise((resolve) => {
|
|
11592
11671
|
const instance = render(
|
|
11593
|
-
|
|
11594
|
-
SnapshotReview2({
|
|
11672
|
+
React3.createElement(SnapshotReview2, {
|
|
11595
11673
|
items,
|
|
11596
11674
|
onDecide,
|
|
11597
11675
|
onExit: () => {
|
|
@@ -12278,7 +12356,11 @@ function buildProgram() {
|
|
|
12278
12356
|
}
|
|
12279
12357
|
process.exit(result.exitCode);
|
|
12280
12358
|
});
|
|
12281
|
-
program.command("explore <url>").description("Run an exploratory testing session against a URL").option(
|
|
12359
|
+
program.command("explore <url>").description("Run an exploratory testing session against a URL").option(
|
|
12360
|
+
"--duration <seconds>",
|
|
12361
|
+
"time budget in seconds (min 60, default 600)",
|
|
12362
|
+
(v) => parseInt(v, 10)
|
|
12363
|
+
).option("--focus <text>", "optional natural-language focus hint").option("--charter-only", "generate charter and stop; no findings, no defects").option("--no-defects", "skip defect.create + filing for findings").option("--defect-target <target>", "where to file defects: stdout | github | jira").option("--defect-repo <repo>", "github repo (owner/repo) when --defect-target=github").option("--defect-project <project>", "jira project key when --defect-target=jira").action(async (url, cmdOpts, command) => {
|
|
12282
12364
|
const globalOpts = command.parent?.opts() ?? {};
|
|
12283
12365
|
const opts = {
|
|
12284
12366
|
url,
|
|
@@ -12427,11 +12509,12 @@ function buildProgram() {
|
|
|
12427
12509
|
configGroup.command("show").description("Print resolved config (defaults applied)").action((cmdOpts, command) => {
|
|
12428
12510
|
void runConfigVerb("show", cmdOpts, command, {});
|
|
12429
12511
|
});
|
|
12430
|
-
program.command("review").description("Walk pending gates and approve/reject (interactive Ink TUI; W9-T2)").option("--workflow <id>", "restrict to one workflow id").action(async (cmdOpts, command) => {
|
|
12512
|
+
program.command("review").description("Walk pending gates and approve/reject (interactive Ink TUI; W9-T2)").option("--workflow <id>", "restrict to one workflow id").option("--auto-approve", "approve all pending gates without prompting (CI-friendly)").action(async (cmdOpts, command) => {
|
|
12431
12513
|
const globalOpts = command.parent?.opts() ?? {};
|
|
12432
12514
|
const workflowFlag = typeof cmdOpts["workflow"] === "string" ? cmdOpts["workflow"] : typeof globalOpts["workflow"] === "string" ? globalOpts["workflow"] : void 0;
|
|
12433
12515
|
const opts = {
|
|
12434
12516
|
nonInteractive: Boolean(globalOpts["nonInteractive"]),
|
|
12517
|
+
autoApprove: Boolean(cmdOpts["autoApprove"]),
|
|
12435
12518
|
json: Boolean(globalOpts["json"]),
|
|
12436
12519
|
quiet: Boolean(globalOpts["quiet"])
|
|
12437
12520
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qatonic_innovations/qaios",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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",
|