@q-agent/agent 0.1.26 → 0.1.28

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
@@ -507,6 +508,7 @@ async function processHealJob(cfg, job, heal) {
507
508
  testTimeoutMs: heal.testTimeoutMs,
508
509
  actionTimeoutMs: heal.actionTimeoutMs,
509
510
  heavyEvidence: isFinalAttempt,
511
+ captureVideo: Boolean(job.captureVideo),
510
512
  });
511
513
  const replaySession = Boolean(job.manualAuth && storageState && sessionStoragePath && fs.statSync(sessionStoragePath).size > 0);
512
514
  (0, playwrightConfig_1.applyFixtures)(workDir, [spec.filename], sessionStoragePath || path.join(workDir, "sessionStorage.json"), replaySession, isFinalAttempt);
@@ -1100,10 +1102,17 @@ async function processAuthoringJob(cfg, job) {
1100
1102
  // (#420), so abort the local Claude run immediately instead of burning the
1101
1103
  // rest of the budget on work whose result will be rejected anyway.
1102
1104
  let aborted = false;
1105
+ // AGENT LOG mirrors the web trail's Settings verbosity (#438): in "concise"
1106
+ // (default) skip the raw tool/Bash step lines (▷ …) so the local log shows
1107
+ // only Claude's readable narration. The server WS still gets every line — the
1108
+ // web filters its own stream — and abort-on-stop keys on those posts.
1109
+ const conciseLog = (job.logVerbosity ?? "concise") === "concise";
1103
1110
  const emitStep = (line) => {
1104
1111
  const trimmed = line.length > 300 ? line.slice(0, 300) + "…" : line;
1105
1112
  console.log(`[authoring ${job.caseId}] ${trimmed}`);
1106
- (0, bus_1.emit)("authoring-step", { caseId: job.caseId, line: trimmed });
1113
+ if (!(conciseLog && trimmed.trimStart().startsWith(""))) {
1114
+ (0, bus_1.emit)("authoring-step", { caseId: job.caseId, line: trimmed });
1115
+ }
1107
1116
  void api
1108
1117
  .postAuthoringEventAlive(cfg, job.sessionId, "authoring.progress", {
1109
1118
  case: job.caseId, phase: "step", message: trimmed,
package/dist/src/ui.js CHANGED
@@ -497,6 +497,7 @@ function renderCells(){
497
497
  }
498
498
 
499
499
  function timeStr(ts){ return new Date(ts).toTimeString().slice(0,8); }
500
+ var LOG_EMPTY_HTML = '<div id="logEmptyPh" style="color:#5c5c6e;padding:2px 0;line-height:1.7">Ready \\u00b7 waiting for activity\\u2026<br><span style="color:#4c4c5a;font-size:10.5px">Runs, live-authoring and self-heal will stream here.</span></div>';
500
501
  function logColor(t){
501
502
  if (t === "error" || t === "auth-error") return "#ff9ea1";
502
503
  if (t === "auth-waiting") return "#fbbf24";
@@ -524,6 +525,7 @@ function logMsg(ev){
524
525
  function addLog(ev){
525
526
  if (ev.type === "agent-status") return; // status events aren't log lines; pill is driven by view state
526
527
  var msg = logMsg(ev); if (!msg) return;
528
+ var ph = document.getElementById("logEmptyPh"); if (ph) ph.remove(); // first real line clears the placeholder
527
529
  var row = document.createElement("div");
528
530
  row.style.cssText = "display:flex;gap:10px;animation:logIn .4s ease both";
529
531
  var t = document.createElement("span"); t.style.cssText = "color:#5c5c6e;flex-shrink:0"; t.textContent = timeStr(ev.ts);
@@ -582,7 +584,9 @@ function showConnected(s){
582
584
  el("sessionId").textContent = "device #" + (s.deviceId==null?"?":s.deviceId);
583
585
  // The SSE stream replays the recent-events buffer on connect, so seed the log
584
586
  // ONLY from the stream (seeding from /api/state too would double every line).
585
- el("log").innerHTML = ""; openStream();
587
+ // Show a placeholder until the first line arrives so an idle/just-reconnected
588
+ // agent reads as "ready & waiting", not a broken empty panel (#agent-log-empty).
589
+ el("log").innerHTML = LOG_EMPTY_HTML; openStream();
586
590
  }
587
591
  function refresh(){
588
592
  fetch("/api/state").then(function(r){ return r.json(); }).then(function(s){
@@ -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.26",
3
+ "version": "0.1.28",
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": {