@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: 'only-on-failure',
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 retain = heavyEvidence ? "retain-on-failure" : "off";
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__", retain)
98
- .replace("__TRACE__", retain)
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" +
@@ -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
  /**
@@ -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
- await finalize(code, discovered, summary, ok, costUsd);
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.27",
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": {