@q-agent/agent 0.1.1 → 0.1.7
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 +28 -1
- package/dist/src/api.js +97 -1
- package/dist/src/cli.js +18 -3
- package/dist/src/config.js +25 -0
- package/dist/src/ensureBrowser.js +4 -1
- package/dist/src/paths.js +15 -0
- package/dist/src/playwrightConfig.js +87 -42
- package/dist/src/report.js +4 -0
- package/dist/src/runner.js +354 -63
- package/dist/src/ui.js +400 -161
- package/dist/src/version.js +61 -0
- package/dist/test/api.test.js +50 -0
- package/dist/test/playwrightConfig.test.js +84 -0
- package/dist/test/report.test.js +4 -0
- package/dist/test/version.test.js +21 -0
- package/package.json +36 -2
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The running agent's version.
|
|
4
|
+
*
|
|
5
|
+
* Read from the package manifest at runtime (rather than a compile-time import)
|
|
6
|
+
* so both the `npx @q-agent/agent` package and the packaged desktop bundle report
|
|
7
|
+
* the *actual* installed build — the number one signal when diagnosing a stale
|
|
8
|
+
* `npx` cache serving an old agent. Compiled output lives at `dist/src/`, so the
|
|
9
|
+
* manifest is two levels up. Any failure falls back to `"unknown"` — a missing
|
|
10
|
+
* version must never crash the CLI.
|
|
11
|
+
*/
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.agentVersion = agentVersion;
|
|
47
|
+
const path = __importStar(require("path"));
|
|
48
|
+
let cached = null;
|
|
49
|
+
/** The agent's semver (e.g. "0.1.6"), or "unknown" if the manifest can't be read. */
|
|
50
|
+
function agentVersion() {
|
|
51
|
+
if (cached !== null)
|
|
52
|
+
return cached;
|
|
53
|
+
try {
|
|
54
|
+
const manifest = require(path.join(__dirname, "..", "..", "package.json"));
|
|
55
|
+
cached = manifest.version || "unknown";
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
cached = "unknown";
|
|
59
|
+
}
|
|
60
|
+
return cached;
|
|
61
|
+
}
|
package/dist/test/api.test.js
CHANGED
|
@@ -135,6 +135,44 @@ function mockFetch(handler) {
|
|
|
135
135
|
const body = JSON.parse(calls[0].init.body);
|
|
136
136
|
strict_1.default.deepEqual(body, { passed: 3, failed: 1, log: "tail" });
|
|
137
137
|
});
|
|
138
|
+
(0, node_test_1.test)("postHealFix starts a job then polls /agent/heal/{caseId}/fix/{jobId} for the action", async () => {
|
|
139
|
+
// Async flow (#313): POST starts the job (returns jobId), GET polls until done.
|
|
140
|
+
mockFetch((url, init) => {
|
|
141
|
+
if (init.method === "POST")
|
|
142
|
+
return Response.json({ jobId: "job-1", status: "running" });
|
|
143
|
+
return Response.json({ status: "done", result: { action: "fixed", code: "// fixed", diff: "@@" } });
|
|
144
|
+
});
|
|
145
|
+
const out = await api.postHealFix(cfg, 99, {
|
|
146
|
+
currentCode: "// old", error: "boom", output: "tail", domDistilled: { path: "/x" }, attempt: 2,
|
|
147
|
+
});
|
|
148
|
+
// First call: POST to start with the attempt body.
|
|
149
|
+
strict_1.default.equal(calls[0].url, "http://127.0.0.1:8787/agent/heal/99/fix");
|
|
150
|
+
strict_1.default.equal(calls[0].init.method, "POST");
|
|
151
|
+
const body = JSON.parse(calls[0].init.body);
|
|
152
|
+
strict_1.default.deepEqual(body, { currentCode: "// old", error: "boom", output: "tail", domDistilled: { path: "/x" }, attempt: 2 });
|
|
153
|
+
// Second call: GET poll on the returned job id.
|
|
154
|
+
strict_1.default.equal(calls[1].url, "http://127.0.0.1:8787/agent/heal/99/fix/job-1");
|
|
155
|
+
strict_1.default.equal(calls[1].init.method, "GET");
|
|
156
|
+
strict_1.default.equal(out.action, "fixed");
|
|
157
|
+
strict_1.default.equal(out.code, "// fixed");
|
|
158
|
+
});
|
|
159
|
+
(0, node_test_1.test)("postHealFix surfaces a server-side error job", async () => {
|
|
160
|
+
mockFetch((url, init) => {
|
|
161
|
+
if (init.method === "POST")
|
|
162
|
+
return Response.json({ jobId: "job-2", status: "running" });
|
|
163
|
+
return Response.json({ status: "error", error: "Claude timed out" });
|
|
164
|
+
});
|
|
165
|
+
await strict_1.default.rejects(() => api.postHealFix(cfg, 99, {
|
|
166
|
+
currentCode: "// old", error: "boom", output: "", domDistilled: null, attempt: 1,
|
|
167
|
+
}), /Claude timed out/);
|
|
168
|
+
});
|
|
169
|
+
(0, node_test_1.test)("postHealFinalize posts the outcome to /agent/heal/{caseId}/finalize", async () => {
|
|
170
|
+
mockFetch(() => new Response(null, { status: 200 }));
|
|
171
|
+
await api.postHealFinalize(cfg, 99, { finalStatus: "pass", finalCode: "// x", attempts: [] });
|
|
172
|
+
strict_1.default.equal(calls[0].url, "http://127.0.0.1:8787/agent/heal/99/finalize");
|
|
173
|
+
const body = JSON.parse(calls[0].init.body);
|
|
174
|
+
strict_1.default.equal(body.finalStatus, "pass");
|
|
175
|
+
});
|
|
138
176
|
(0, node_test_1.test)("redeemDevice posts code+name with no auth header", async () => {
|
|
139
177
|
mockFetch(() => Response.json({ deviceToken: "abc", deviceId: 7 }));
|
|
140
178
|
const result = await api.redeemDevice("http://127.0.0.1:8787", "PAIR123", "my-laptop");
|
|
@@ -144,3 +182,15 @@ function mockFetch(handler) {
|
|
|
144
182
|
const body = JSON.parse(calls[0].init.body);
|
|
145
183
|
strict_1.default.deepEqual(body, { code: "PAIR123", name: "my-laptop" });
|
|
146
184
|
});
|
|
185
|
+
(0, node_test_1.test)("fetchWithTimeout aborts a stalled request once the timeout elapses", async () => {
|
|
186
|
+
// A server that never responds but honors the abort signal.
|
|
187
|
+
mockFetch((_u, init) => new Promise((_resolve, reject) => {
|
|
188
|
+
init.signal.addEventListener("abort", () => reject(new Error("aborted")));
|
|
189
|
+
}));
|
|
190
|
+
await strict_1.default.rejects(api.fetchWithTimeout("http://127.0.0.1:8787/slow", { method: "POST" }, 20));
|
|
191
|
+
});
|
|
192
|
+
(0, node_test_1.test)("fetchWithTimeout returns the response when it resolves before the timeout", async () => {
|
|
193
|
+
mockFetch(() => new Response(null, { status: 200 }));
|
|
194
|
+
const res = await api.fetchWithTimeout("http://127.0.0.1:8787/ok", {}, 1000);
|
|
195
|
+
strict_1.default.equal(res.status, 200);
|
|
196
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for the injected-fixtures port (`src/playwrightConfig.ts`).
|
|
4
|
+
* Mirrors the server's `test_fixtures_ts_contents` / `test_apply_fixtures_always_injects`
|
|
5
|
+
* (api/tests/test_execution.py) so the agent's DOM capture stays in lock-step
|
|
6
|
+
* with the server runner.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
42
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
43
|
+
};
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
46
|
+
const fs = __importStar(require("node:fs"));
|
|
47
|
+
const os = __importStar(require("node:os"));
|
|
48
|
+
const path = __importStar(require("node:path"));
|
|
49
|
+
const node_test_1 = require("node:test");
|
|
50
|
+
const playwrightConfig_1 = require("../src/playwrightConfig");
|
|
51
|
+
(0, node_test_1.test)("fixturesTs always wires DOM capture; sessionStorage replay is gated", () => {
|
|
52
|
+
const sessionFile = "/tmp/sessionStorage.json";
|
|
53
|
+
const replay = (0, playwrightConfig_1.fixturesTs)(sessionFile, true);
|
|
54
|
+
strict_1.default.ok(replay.includes("export const test"));
|
|
55
|
+
strict_1.default.ok(replay.includes("testInfo.attach('qagent-dom-raw'"));
|
|
56
|
+
strict_1.default.ok(replay.includes("testInfo.attach('qagent-dom-distilled'"));
|
|
57
|
+
strict_1.default.ok(replay.includes(JSON.stringify(sessionFile)));
|
|
58
|
+
strict_1.default.ok(replay.includes("addInitScript"), "replay=true injects the session init script");
|
|
59
|
+
const noReplay = (0, playwrightConfig_1.fixturesTs)(sessionFile, false);
|
|
60
|
+
strict_1.default.ok(noReplay.includes("testInfo.attach('qagent-dom-distilled'"));
|
|
61
|
+
strict_1.default.ok(!noReplay.includes("addInitScript"), "replay=false drops the session init script");
|
|
62
|
+
});
|
|
63
|
+
(0, node_test_1.test)("applyFixtures always rewrites imports to './fixtures' + writes fixtures.ts", () => {
|
|
64
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "qa-fx-"));
|
|
65
|
+
const specName = "1428-TC-01.spec.ts";
|
|
66
|
+
const specPath = path.join(dir, specName);
|
|
67
|
+
const original = "import { test, expect } from '@playwright/test';\n" +
|
|
68
|
+
"test('x', async ({ page }) => { await page.goto('/'); });\n";
|
|
69
|
+
fs.writeFileSync(specPath, original, "utf-8");
|
|
70
|
+
const sessionFile = path.join(dir, "sessionStorage.json");
|
|
71
|
+
// Even without session replay, DOM capture means fixtures are injected.
|
|
72
|
+
(0, playwrightConfig_1.applyFixtures)(dir, [specName], sessionFile, false);
|
|
73
|
+
const rewritten = fs.readFileSync(specPath, "utf-8");
|
|
74
|
+
strict_1.default.ok(rewritten.includes("'./fixtures'"));
|
|
75
|
+
strict_1.default.ok(!rewritten.includes("'@playwright/test'"));
|
|
76
|
+
const fixtures = fs.readFileSync(path.join(dir, "fixtures.ts"), "utf-8");
|
|
77
|
+
strict_1.default.ok(fixtures.includes("qagent-dom-distilled"));
|
|
78
|
+
strict_1.default.ok(!fixtures.includes("addInitScript"));
|
|
79
|
+
// With replay enabled, the init script is added; specs stay pointed at './fixtures'.
|
|
80
|
+
(0, playwrightConfig_1.applyFixtures)(dir, [specName], sessionFile, true);
|
|
81
|
+
strict_1.default.ok(fs.readFileSync(specPath, "utf-8").includes("'./fixtures'"));
|
|
82
|
+
strict_1.default.ok(fs.readFileSync(path.join(dir, "fixtures.ts"), "utf-8").includes("addInitScript"));
|
|
83
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
84
|
+
});
|
package/dist/test/report.test.js
CHANGED
|
@@ -139,6 +139,8 @@ const report_1 = require("../src/report");
|
|
|
139
139
|
{ name: "screenshot", path: "/tmp/shot.png" },
|
|
140
140
|
{ name: "video", path: "/tmp/vid.webm" },
|
|
141
141
|
{ name: "trace", path: "/tmp/trace.zip" },
|
|
142
|
+
{ name: "qagent-dom-raw", path: "/tmp/dom.html" },
|
|
143
|
+
{ name: "qagent-dom-distilled", path: "/tmp/dom.json" },
|
|
142
144
|
{ name: "stdout", path: "/tmp/stdout.txt" },
|
|
143
145
|
{ name: "screenshot", path: "" },
|
|
144
146
|
],
|
|
@@ -155,6 +157,8 @@ const report_1 = require("../src/report");
|
|
|
155
157
|
{ kind: "screenshot", path: "/tmp/shot.png" },
|
|
156
158
|
{ kind: "video", path: "/tmp/vid.webm" },
|
|
157
159
|
{ kind: "trace", path: "/tmp/trace.zip" },
|
|
160
|
+
{ kind: "dom", path: "/tmp/dom.html" },
|
|
161
|
+
{ kind: "dom-distilled", path: "/tmp/dom.json" },
|
|
158
162
|
]);
|
|
159
163
|
});
|
|
160
164
|
(0, node_test_1.test)("flattens nested describe-block suites under one file", () => {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tests for the runtime version lookup (`src/version.ts`) — the agent must be
|
|
4
|
+
* able to report its actual installed build (for `--version` and the startup
|
|
5
|
+
* banner), which is the key signal when diagnosing a stale npx cache.
|
|
6
|
+
*/
|
|
7
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
8
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
9
|
+
};
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
12
|
+
const node_test_1 = require("node:test");
|
|
13
|
+
const version_1 = require("../src/version");
|
|
14
|
+
(0, node_test_1.test)("agentVersion reads a real semver from the package manifest (not 'unknown')", () => {
|
|
15
|
+
const v = (0, version_1.agentVersion)();
|
|
16
|
+
strict_1.default.notEqual(v, "unknown", "expected the manifest version to resolve at runtime");
|
|
17
|
+
strict_1.default.match(v, /^\d+\.\d+\.\d+/, `expected semver, got ${v}`);
|
|
18
|
+
});
|
|
19
|
+
(0, node_test_1.test)("agentVersion is stable across calls (cached)", () => {
|
|
20
|
+
strict_1.default.equal((0, version_1.agentVersion)(), (0, version_1.agentVersion)());
|
|
21
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@q-agent/agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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": {
|
|
@@ -27,17 +27,51 @@
|
|
|
27
27
|
"test": "npm run build && node --test dist/test",
|
|
28
28
|
"release": "node scripts/release.mjs",
|
|
29
29
|
"prepublishOnly": "npm run build",
|
|
30
|
-
"package:win": "node scripts/package-win.mjs"
|
|
30
|
+
"package:win": "node scripts/package-win.mjs",
|
|
31
|
+
"desktop": "npm run build && electron electron/main.cjs",
|
|
32
|
+
"dist:desktop": "npm run build && electron-builder --win"
|
|
31
33
|
},
|
|
32
34
|
"dependencies": {
|
|
33
35
|
"@playwright/test": "^1.61.1",
|
|
34
36
|
"commander": "^12.1.0",
|
|
37
|
+
"electron-updater": "^6.3.9",
|
|
35
38
|
"playwright": "^1.61.1"
|
|
36
39
|
},
|
|
37
40
|
"devDependencies": {
|
|
38
41
|
"@types/node": "^20.14.0",
|
|
42
|
+
"electron": "^33.2.0",
|
|
43
|
+
"electron-builder": "^25.1.8",
|
|
39
44
|
"esbuild": "^0.24.0",
|
|
40
45
|
"postject": "^1.0.0-alpha.6",
|
|
41
46
|
"typescript": "^5.5.4"
|
|
47
|
+
},
|
|
48
|
+
"build": {
|
|
49
|
+
"appId": "click.chuongnd.qagent-agent",
|
|
50
|
+
"productName": "Q-Agent Local Agent",
|
|
51
|
+
"extraMetadata": {
|
|
52
|
+
"main": "electron/main.cjs",
|
|
53
|
+
"qagentServer": "https://qagent.chuongnd.click/api"
|
|
54
|
+
},
|
|
55
|
+
"files": [
|
|
56
|
+
"dist/**/*",
|
|
57
|
+
"electron/**/*",
|
|
58
|
+
"vendor/**/*",
|
|
59
|
+
"package.json"
|
|
60
|
+
],
|
|
61
|
+
"artifactName": "qagent-agent-setup.${ext}",
|
|
62
|
+
"asar": false,
|
|
63
|
+
"win": {
|
|
64
|
+
"target": "nsis",
|
|
65
|
+
"signAndEditExecutable": false
|
|
66
|
+
},
|
|
67
|
+
"publish": [
|
|
68
|
+
{
|
|
69
|
+
"provider": "generic",
|
|
70
|
+
"url": "https://qagent.chuongnd.click/downloads/"
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
"directories": {
|
|
74
|
+
"output": "dist-bin/desktop"
|
|
75
|
+
}
|
|
42
76
|
}
|
|
43
77
|
}
|