@q-agent/agent 0.1.27 → 0.1.29
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.
|
@@ -54,7 +54,7 @@ export default defineConfig({
|
|
|
54
54
|
reporter: __REPORTERS__,
|
|
55
55
|
use: {
|
|
56
56
|
headless: __HEADLESS__,
|
|
57
|
-
screenshot: '
|
|
57
|
+
screenshot: 'on',
|
|
58
58
|
video: '__VIDEO__',
|
|
59
59
|
trace: '__TRACE__',
|
|
60
60
|
__EXTRA_USE__ },
|
|
@@ -72,7 +72,7 @@ __EXTRA_USE__ },
|
|
|
72
72
|
* @param opts Heal-tuning knobs (timeouts + evidence); defaults match a normal run.
|
|
73
73
|
*/
|
|
74
74
|
function writeConfig(specDir, workers, headless, baseUrl = "", storageState = "", opts = {}) {
|
|
75
|
-
const { testTimeoutMs = 30000, actionTimeoutMs, heavyEvidence = true, liveReporterPath } = opts;
|
|
75
|
+
const { testTimeoutMs = 30000, actionTimeoutMs, heavyEvidence = true, captureVideo = false, liveReporterPath } = opts;
|
|
76
76
|
// JSON reporter (drives report.json for evidence + reconcile) + optionally the
|
|
77
77
|
// live reporter (streams per-test results). Paths JSON-escaped for Windows.
|
|
78
78
|
const reporters = ["['json', { outputFile: 'report.json' }]"];
|
|
@@ -89,13 +89,16 @@ function writeConfig(specDir, workers, headless, baseUrl = "", storageState = ""
|
|
|
89
89
|
extraLines.push(` navigationTimeout: ${Math.trunc(actionTimeoutMs)},`);
|
|
90
90
|
}
|
|
91
91
|
const extraUse = extraLines.length ? extraLines.join("\n") + "\n" : "";
|
|
92
|
-
const
|
|
92
|
+
const trace = heavyEvidence ? "retain-on-failure" : "off";
|
|
93
|
+
// Video follows the "Capture video" setting (always-on when enabled), not the
|
|
94
|
+
// failure-only trace policy — but intermediate heal attempts still skip it.
|
|
95
|
+
const video = captureVideo && heavyEvidence ? "on" : "off";
|
|
93
96
|
const content = PLAYWRIGHT_CONFIG_TEMPLATE.replace("__TIMEOUT__", String(Math.trunc(testTimeoutMs)))
|
|
94
97
|
.replace("__WORKERS__", String(workers))
|
|
95
98
|
.replace("__HEADLESS__", headless ? "true" : "false")
|
|
96
99
|
.replace("__REPORTERS__", reportersStr)
|
|
97
|
-
.replace("__VIDEO__",
|
|
98
|
-
.replace("__TRACE__",
|
|
100
|
+
.replace("__VIDEO__", video)
|
|
101
|
+
.replace("__TRACE__", trace)
|
|
99
102
|
.replace("__EXTRA_USE__", extraUse);
|
|
100
103
|
fs.writeFileSync(path.join(specDir, "playwright.config.ts"), content, "utf-8");
|
|
101
104
|
}
|
|
@@ -151,6 +154,15 @@ function fixturesTs(sessionFile, replaySession, captureRaw = true) {
|
|
|
151
154
|
" // Always-on DOM capture: after each test, snapshot the live page so the\n" +
|
|
152
155
|
" // runner (evidence) and self-heal loop (real selectors) can use it.\n" +
|
|
153
156
|
" _domCapture: [async ({ page }, use, testInfo) => {\n" +
|
|
157
|
+
" // Console + network capture (#456): collect for the whole test, pass or\n" +
|
|
158
|
+
" // fail. Listeners registered BEFORE use() so nothing is missed; the JSON\n" +
|
|
159
|
+
" // is attached after and parsed into console_logs/network_logs columns.\n" +
|
|
160
|
+
" const __net: any[] = []; const __con: any[] = []; const __started = new Map();\n" +
|
|
161
|
+
" page.on('request', (r) => { try { __started.set(r, Date.now()); } catch {} });\n" +
|
|
162
|
+
" page.on('response', (resp) => { try { if (__net.length < 300) { const req = resp.request(); const t0 = __started.get(req); __net.push({ method: req.method(), url: req.url(), status: resp.status(), durationMs: t0 ? Date.now() - t0 : 0 }); } } catch {} });\n" +
|
|
163
|
+
" page.on('requestfailed', (r) => { try { if (__net.length < 300) { const t0 = __started.get(r); __net.push({ method: r.method(), url: r.url(), status: 0, durationMs: t0 ? Date.now() - t0 : 0, failed: true }); } } catch {} });\n" +
|
|
164
|
+
" page.on('console', (msg) => { try { if (__con.length < 500) __con.push({ level: msg.type(), text: String(msg.text()).slice(0, 2000) }); } catch {} });\n" +
|
|
165
|
+
" page.on('pageerror', (err) => { try { if (__con.length < 500) __con.push({ level: 'error', text: String((err && err.message) || err).slice(0, 2000) }); } catch {} });\n" +
|
|
154
166
|
" await use();\n" +
|
|
155
167
|
" // Distilled inventory FIRST (what self-heal needs) — retry once after a\n" +
|
|
156
168
|
" // short settle so a transiently-busy failure page still yields real\n" +
|
|
@@ -188,6 +200,16 @@ function fixturesTs(sessionFile, replaySession, captureRaw = true) {
|
|
|
188
200
|
" } catch {}\n" +
|
|
189
201
|
" }\n" +
|
|
190
202
|
rawBlock +
|
|
203
|
+
" try {\n" +
|
|
204
|
+
" const netPath = testInfo.outputPath('qagent-network.json');\n" +
|
|
205
|
+
" fs.writeFileSync(netPath, JSON.stringify(__net), 'utf-8');\n" +
|
|
206
|
+
" await testInfo.attach('qagent-network', { path: netPath, contentType: 'application/json' });\n" +
|
|
207
|
+
" } catch {}\n" +
|
|
208
|
+
" try {\n" +
|
|
209
|
+
" const conPath = testInfo.outputPath('qagent-console.json');\n" +
|
|
210
|
+
" fs.writeFileSync(conPath, JSON.stringify(__con), 'utf-8');\n" +
|
|
211
|
+
" await testInfo.attach('qagent-console', { path: conPath, contentType: 'application/json' });\n" +
|
|
212
|
+
" } catch {}\n" +
|
|
191
213
|
" }, { auto: true }],\n" +
|
|
192
214
|
"});\n" +
|
|
193
215
|
"\n" +
|
package/dist/src/report.js
CHANGED
|
@@ -32,6 +32,10 @@ const ATTACHMENT_KIND_MAP = {
|
|
|
32
32
|
// raw page HTML + a distilled interactable-element inventory.
|
|
33
33
|
"qagent-dom-raw": "dom",
|
|
34
34
|
"qagent-dom-distilled": "dom-distilled",
|
|
35
|
+
// Console + network captured by the fixtures (#456): uploaded via the evidence
|
|
36
|
+
// path but decoded server-side into console_logs/network_logs (not media rows).
|
|
37
|
+
"qagent-console": "console",
|
|
38
|
+
"qagent-network": "network",
|
|
35
39
|
};
|
|
36
40
|
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
37
41
|
/**
|
package/dist/src/runner.js
CHANGED
|
@@ -258,6 +258,7 @@ async function processJob(cfg, job) {
|
|
|
258
258
|
const { storageState, sessionStoragePath } = auth;
|
|
259
259
|
(0, playwrightConfig_1.writeConfig)(workDir, job.workers, job.headless, job.baseUrl, storageState, {
|
|
260
260
|
liveReporterPath: (0, paths_1.vendorLiveReporter)(),
|
|
261
|
+
captureVideo: Boolean(job.captureVideo),
|
|
261
262
|
});
|
|
262
263
|
// Always inject the generated fixtures.ts (DOM capture on every run) + rewrite
|
|
263
264
|
// spec imports to it. sessionStorage replay (MSAL/SPA tokens) is additionally
|
|
@@ -398,6 +399,13 @@ async function processJob(cfg, job) {
|
|
|
398
399
|
* the workDir is always removed, even if some uploads fail or time out.
|
|
399
400
|
*/
|
|
400
401
|
async function uploadEvidenceThenCleanup(cfg, executionId, pendingEvidence, workDir) {
|
|
402
|
+
// Tell the UI evidence is still incoming so it can show a loader instead of an
|
|
403
|
+
// empty panel while these deferred uploads land (results are already visible).
|
|
404
|
+
if (pendingEvidence.length > 0) {
|
|
405
|
+
await api
|
|
406
|
+
.postEvent(cfg, executionId, "exec.evidence.uploading", { count: pendingEvidence.length })
|
|
407
|
+
.catch(() => { });
|
|
408
|
+
}
|
|
401
409
|
try {
|
|
402
410
|
for (const ev of pendingEvidence) {
|
|
403
411
|
await api
|
|
@@ -413,6 +421,13 @@ async function uploadEvidenceThenCleanup(cfg, executionId, pendingEvidence, work
|
|
|
413
421
|
}
|
|
414
422
|
finally {
|
|
415
423
|
fs.rmSync(workDir, { recursive: true, force: true });
|
|
424
|
+
// Signal completion so the UI drops the loader and refetches the now-uploaded
|
|
425
|
+
// evidence — always fire (even on partial failure) so the loader never sticks.
|
|
426
|
+
if (pendingEvidence.length > 0) {
|
|
427
|
+
await api
|
|
428
|
+
.postEvent(cfg, executionId, "exec.evidence.done", {})
|
|
429
|
+
.catch(() => { });
|
|
430
|
+
}
|
|
416
431
|
}
|
|
417
432
|
}
|
|
418
433
|
/**
|
|
@@ -507,6 +522,7 @@ async function processHealJob(cfg, job, heal) {
|
|
|
507
522
|
testTimeoutMs: heal.testTimeoutMs,
|
|
508
523
|
actionTimeoutMs: heal.actionTimeoutMs,
|
|
509
524
|
heavyEvidence: isFinalAttempt,
|
|
525
|
+
captureVideo: Boolean(job.captureVideo),
|
|
510
526
|
});
|
|
511
527
|
const replaySession = Boolean(job.manualAuth && storageState && sessionStoragePath && fs.statSync(sessionStoragePath).size > 0);
|
|
512
528
|
(0, playwrightConfig_1.applyFixtures)(workDir, [spec.filename], sessionStoragePath || path.join(workDir, "sessionStorage.json"), replaySession, isFinalAttempt);
|
|
@@ -978,7 +994,7 @@ async function processAuthoringJob(cfg, job) {
|
|
|
978
994
|
}
|
|
979
995
|
const sess = origin ? (0, session_1.sessionPathsForOrigin)(origin) : null;
|
|
980
996
|
const profileDir = sess ? path.join(sess.dir, "browser-profile") : "";
|
|
981
|
-
const finalize = async (code, discovered, summary, ok, costUsd = 0) => {
|
|
997
|
+
const finalize = async (code, discovered, summary, ok, costUsd = 0, refreshedCredentials = "") => {
|
|
982
998
|
await api
|
|
983
999
|
.postAuthoringFinalize(cfg, job.sessionId, {
|
|
984
1000
|
code,
|
|
@@ -986,6 +1002,7 @@ async function processAuthoringJob(cfg, job) {
|
|
|
986
1002
|
summary,
|
|
987
1003
|
ok,
|
|
988
1004
|
costUsd,
|
|
1005
|
+
refreshedCredentials,
|
|
989
1006
|
})
|
|
990
1007
|
.catch((err) => console.error("postAuthoringFinalize failed:", err));
|
|
991
1008
|
};
|
|
@@ -1195,7 +1212,19 @@ async function processAuthoringJob(cfg, job) {
|
|
|
1195
1212
|
case: job.caseId, phase: ok ? "done" : "failed", message: (summary || "").slice(0, 300), costUsd,
|
|
1196
1213
|
})
|
|
1197
1214
|
.catch(() => { });
|
|
1198
|
-
|
|
1215
|
+
// Auto-rotation (#cred-rotate): if we ran with the app's saved credential and
|
|
1216
|
+
// the CLI refreshed the OAuth token in our temp config dir, post the rotated
|
|
1217
|
+
// .credentials.json back so the server captures it before workDir is deleted.
|
|
1218
|
+
let refreshedCredentials = "";
|
|
1219
|
+
if (claudeConfigDir) {
|
|
1220
|
+
try {
|
|
1221
|
+
const credFile = path.join(claudeConfigDir, ".credentials.json");
|
|
1222
|
+
if (fs.existsSync(credFile))
|
|
1223
|
+
refreshedCredentials = fs.readFileSync(credFile, "utf-8");
|
|
1224
|
+
}
|
|
1225
|
+
catch { /* best-effort — a missing/unreadable file just skips rotation */ }
|
|
1226
|
+
}
|
|
1227
|
+
await finalize(code, discovered, summary, ok, costUsd, refreshedCredentials);
|
|
1199
1228
|
}
|
|
1200
1229
|
finally {
|
|
1201
1230
|
try {
|
|
@@ -60,6 +60,32 @@ const playwrightConfig_1 = require("../src/playwrightConfig");
|
|
|
60
60
|
strict_1.default.ok(noReplay.includes("testInfo.attach('qagent-dom-distilled'"));
|
|
61
61
|
strict_1.default.ok(!noReplay.includes("addInitScript"), "replay=false drops the session init script");
|
|
62
62
|
});
|
|
63
|
+
(0, node_test_1.test)("fixturesTs captures console + network on every test (#456)", () => {
|
|
64
|
+
const fx = (0, playwrightConfig_1.fixturesTs)("/tmp/sessionStorage.json", false);
|
|
65
|
+
strict_1.default.ok(fx.includes("testInfo.attach('qagent-network'"));
|
|
66
|
+
strict_1.default.ok(fx.includes("testInfo.attach('qagent-console'"));
|
|
67
|
+
strict_1.default.ok(fx.includes("page.on('response'"), "network responses are collected");
|
|
68
|
+
strict_1.default.ok(fx.includes("page.on('console'"), "console messages are collected");
|
|
69
|
+
});
|
|
70
|
+
(0, node_test_1.test)("writeConfig: screenshot always-on; video honors the setting (#456)", () => {
|
|
71
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "qa-cfg-"));
|
|
72
|
+
const read = () => fs.readFileSync(path.join(dir, "playwright.config.ts"), "utf-8");
|
|
73
|
+
// Screenshot is always 'on' now (captured pass or fail).
|
|
74
|
+
(0, playwrightConfig_1.writeConfig)(dir, 1, true);
|
|
75
|
+
strict_1.default.ok(read().includes("screenshot: 'on'"));
|
|
76
|
+
// Capture-video ON → video 'on' regardless of pass/fail; trace stays failure-only.
|
|
77
|
+
(0, playwrightConfig_1.writeConfig)(dir, 1, true, "", "", { captureVideo: true });
|
|
78
|
+
strict_1.default.ok(read().includes("video: 'on'"));
|
|
79
|
+
strict_1.default.ok(read().includes("trace: 'retain-on-failure'"));
|
|
80
|
+
// Capture-video OFF → no video.
|
|
81
|
+
(0, playwrightConfig_1.writeConfig)(dir, 1, true, "", "", { captureVideo: false });
|
|
82
|
+
strict_1.default.ok(read().includes("video: 'off'"));
|
|
83
|
+
// Intermediate heal attempts (heavyEvidence=false) never record video even when enabled.
|
|
84
|
+
(0, playwrightConfig_1.writeConfig)(dir, 1, true, "", "", { captureVideo: true, heavyEvidence: false });
|
|
85
|
+
strict_1.default.ok(read().includes("video: 'off'"));
|
|
86
|
+
strict_1.default.ok(read().includes("trace: 'off'"));
|
|
87
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
88
|
+
});
|
|
63
89
|
(0, node_test_1.test)("applyFixtures always rewrites imports to './fixtures' + writes fixtures.ts", () => {
|
|
64
90
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "qa-fx-"));
|
|
65
91
|
const specName = "1428-TC-01.spec.ts";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@q-agent/agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29",
|
|
4
4
|
"description": "Q-Agent Local Agent — claims execution jobs from the Q-Agent server and runs Playwright locally, so manual login/MFA happens on the user's own machine and session credentials never leave it.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"bin": {
|