@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.
- package/dist/src/playwrightConfig.js +27 -5
- package/dist/src/report.js +4 -0
- package/dist/src/runner.js +10 -1
- package/dist/src/ui.js +5 -1
- package/dist/test/playwrightConfig.test.js +26 -0
- package/package.json +1 -1
|
@@ -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
|
|
@@ -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
|
-
(
|
|
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
|
-
|
|
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.
|
|
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": {
|