@vostride/agent-qa-dashboard 0.2.2 → 0.2.4
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 +78 -0
- package/dist/{index-Ds4Lv4mT.d.cts → index-Cxygerok.d.cts} +16 -627
- package/dist/{index-Ds4Lv4mT.d.ts → index-Cxygerok.d.ts} +16 -627
- package/dist/index.cjs +337 -740
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -201
- package/dist/index.d.ts +7 -201
- package/dist/index.js +347 -743
- package/dist/index.js.map +1 -1
- package/dist/server/index.cjs +311 -573
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +321 -577
- package/dist/server/index.js.map +1 -1
- package/package.json +13 -13
package/dist/server/index.cjs
CHANGED
|
@@ -45,6 +45,7 @@ var import_node_crypto8 = require("node:crypto");
|
|
|
45
45
|
var import_promises9 = require("node:fs/promises");
|
|
46
46
|
var import_node_path10 = require("node:path");
|
|
47
47
|
var import_node_os3 = require("node:os");
|
|
48
|
+
var import_node_util5 = require("node:util");
|
|
48
49
|
|
|
49
50
|
// src/db/run-owner-host.ts
|
|
50
51
|
var import_node_child_process = require("node:child_process");
|
|
@@ -340,7 +341,7 @@ var TestFileManager = class {
|
|
|
340
341
|
constructor(workspace) {
|
|
341
342
|
this.workspace = workspace;
|
|
342
343
|
}
|
|
343
|
-
//
|
|
344
|
+
// Each list call reads the workspace itself; each file is written whole, one writer at a time.
|
|
344
345
|
underLock = createPathLock();
|
|
345
346
|
async list() {
|
|
346
347
|
const records = await (0, import_agent_qa_core.discoverWorkspaceFiles)({ workspace: this.workspace, kind: "test" });
|
|
@@ -504,8 +505,7 @@ var ConfigManager = class _ConfigManager {
|
|
|
504
505
|
this.configPath = configPath;
|
|
505
506
|
this.mask = mask;
|
|
506
507
|
}
|
|
507
|
-
//
|
|
508
|
-
// add one. A missing file holds none.
|
|
508
|
+
// A whole document may keep a marker the file already holds, as often, but never add one.
|
|
509
509
|
static addsShownMarker(current, next) {
|
|
510
510
|
const held = shownMarkerCounts(current);
|
|
511
511
|
for (const [marker, count] of shownMarkerCounts(next)) {
|
|
@@ -513,13 +513,13 @@ var ConfigManager = class _ConfigManager {
|
|
|
513
513
|
}
|
|
514
514
|
return false;
|
|
515
515
|
}
|
|
516
|
-
//
|
|
516
|
+
// A half-edited file is named, never served as the partial config yaml recovers.
|
|
517
517
|
async readWithRevision() {
|
|
518
518
|
const read = await this.readParsed();
|
|
519
519
|
if (read.kind !== "ok") return read;
|
|
520
520
|
return { kind: "ok", config: read.config, raw: read.raw, revision: read.revision };
|
|
521
521
|
}
|
|
522
|
-
//
|
|
522
|
+
// The payload and the file view pass through the declared mask and nothing else.
|
|
523
523
|
async readMasked() {
|
|
524
524
|
const read = await this.readParsed();
|
|
525
525
|
if (read.kind === "absent") {
|
|
@@ -563,8 +563,7 @@ var ConfigManager = class _ConfigManager {
|
|
|
563
563
|
revision
|
|
564
564
|
};
|
|
565
565
|
}
|
|
566
|
-
//
|
|
567
|
-
// read the same bytes and drop each other's field.
|
|
566
|
+
// One transaction per request, serialised so overlapping autosaves never drop each other's field.
|
|
568
567
|
async applyChanges(changes, options = {}) {
|
|
569
568
|
const marked = changeWithShownMarker(changes);
|
|
570
569
|
if (marked !== void 0) {
|
|
@@ -579,13 +578,11 @@ var ConfigManager = class _ConfigManager {
|
|
|
579
578
|
}
|
|
580
579
|
return this.serialized(() => this.applyChangesOnce(changes, options.capturePriors === true));
|
|
581
580
|
}
|
|
582
|
-
//
|
|
583
|
-
// that no longer holds what the change left there was written since, and is never overwritten.
|
|
581
|
+
// An Undo restores newest first; a field written after the change is never overwritten.
|
|
584
582
|
async restorePriors(priors) {
|
|
585
583
|
return this.serialized(() => this.restorePriorsOnce(priors));
|
|
586
584
|
}
|
|
587
|
-
//
|
|
588
|
-
// bytes it was built from, read again in this path's turn. Anything else is somebody's edit.
|
|
585
|
+
// A generated whole-file change publishes only over the bytes it is built from, re-read in turn.
|
|
589
586
|
async replaceDocument(expectedRevision, content) {
|
|
590
587
|
return this.serialized(async () => {
|
|
591
588
|
let current;
|
|
@@ -712,8 +709,7 @@ var ConfigManager = class _ConfigManager {
|
|
|
712
709
|
await this.writeAtomic(next);
|
|
713
710
|
return capturePriors ? { kind: "ok", revision: revisionOf(next), baseRevision, priors } : { kind: "ok", revision: revisionOf(next), baseRevision };
|
|
714
711
|
}
|
|
715
|
-
//
|
|
716
|
-
// (chmod, because writeFile's mode is filtered by the umask), and replaces it in one rename.
|
|
712
|
+
// The temp file sits beside the real one and is chmodded, as the umask filters `writeFile` modes.
|
|
717
713
|
async writeAtomic(content) {
|
|
718
714
|
const target = await this.writeTarget();
|
|
719
715
|
const directory = (0, import_node_path3.dirname)(target);
|
|
@@ -1913,6 +1909,12 @@ var HookRegistryManager = class {
|
|
|
1913
1909
|
|
|
1914
1910
|
// src/server/body-parser.ts
|
|
1915
1911
|
var DEFAULT_MAX_BYTES = 1024 * 1024;
|
|
1912
|
+
function carriesNonJsonBody(req) {
|
|
1913
|
+
const hasBody = req.headers["transfer-encoding"] !== void 0 || Number(req.headers["content-length"] ?? "0") > 0;
|
|
1914
|
+
if (!hasBody) return false;
|
|
1915
|
+
const type = req.headers["content-type"];
|
|
1916
|
+
return type === void 0 || type.split(";", 1)[0].trim().toLowerCase() !== "application/json";
|
|
1917
|
+
}
|
|
1916
1918
|
function readJsonBody(req, maxBytes = DEFAULT_MAX_BYTES) {
|
|
1917
1919
|
return new Promise((resolve5, reject) => {
|
|
1918
1920
|
const chunks = [];
|
|
@@ -1947,6 +1949,7 @@ function readJsonBody(req, maxBytes = DEFAULT_MAX_BYTES) {
|
|
|
1947
1949
|
// src/live-editor/live-session.ts
|
|
1948
1950
|
var import_node_crypto5 = require("node:crypto");
|
|
1949
1951
|
var import_node_path7 = require("node:path");
|
|
1952
|
+
var import_node_util3 = require("node:util");
|
|
1950
1953
|
var import_agent_qa_core6 = require("@vostride/agent-qa-core");
|
|
1951
1954
|
var import_agent_qa_runner = require("@vostride/agent-qa-runner");
|
|
1952
1955
|
var import_agent_qa_runner_local = require("@vostride/agent-qa-runner-local");
|
|
@@ -2034,12 +2037,10 @@ async function loadLocalBindings(configPath) {
|
|
|
2034
2037
|
try {
|
|
2035
2038
|
const content = await (0, import_promises6.readFile)(filePath, "utf-8");
|
|
2036
2039
|
const parsed = (0, import_yaml5.parse)(content);
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
filePath
|
|
2042
|
-
} : { filePath };
|
|
2040
|
+
(0, import_agent_qa_core5.refuseRetiredLocalBindings)(parsed, filePath);
|
|
2041
|
+
if (!parsed || typeof parsed !== "object") return { filePath };
|
|
2042
|
+
const bindings = parsed;
|
|
2043
|
+
return { devices: bindings.devices, apps: bindings.apps, filePath };
|
|
2043
2044
|
} catch (err) {
|
|
2044
2045
|
if (err.code === "ENOENT") return null;
|
|
2045
2046
|
throw err;
|
|
@@ -2129,44 +2130,15 @@ async function prepareMobileLiveSession(input) {
|
|
|
2129
2130
|
appActivity: resolved.app.appActivity,
|
|
2130
2131
|
deepLinkAppId: resolved.app.deepLinkAppId,
|
|
2131
2132
|
appPath: resolved.app.install?.path,
|
|
2132
|
-
browserstackApp: resolved.app.install?.browserstack,
|
|
2133
2133
|
appState: resolved.appState
|
|
2134
2134
|
}
|
|
2135
2135
|
};
|
|
2136
2136
|
}
|
|
2137
2137
|
|
|
2138
2138
|
// src/live-editor/live-session.ts
|
|
2139
|
-
var LiveProductionRefusedError = class extends Error {
|
|
2140
|
-
productionTargetNames;
|
|
2141
|
-
constructor(productionTargetNames) {
|
|
2142
|
-
super((0, import_agent_qa_schema2.buildProductionRefusal)(productionTargetNames));
|
|
2143
|
-
this.name = "LiveProductionRefusedError";
|
|
2144
|
-
this.productionTargetNames = (0, import_agent_qa_schema2.distinctInFirstOccurrenceOrder)(productionTargetNames);
|
|
2145
|
-
}
|
|
2146
|
-
};
|
|
2147
2139
|
function isConfigRecord(value) {
|
|
2148
2140
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2149
2141
|
}
|
|
2150
|
-
function liveTargetName(targetName) {
|
|
2151
|
-
if (typeof targetName !== "string") return void 0;
|
|
2152
|
-
const trimmed = targetName.trim();
|
|
2153
|
-
return trimmed.length > 0 ? trimmed : void 0;
|
|
2154
|
-
}
|
|
2155
|
-
async function resolveLiveTargetProduction(configManager, targetName) {
|
|
2156
|
-
const name = liveTargetName(targetName);
|
|
2157
|
-
if (name === void 0 || configManager === void 0) {
|
|
2158
|
-
return { productionTargetNames: [], allowProduction: false };
|
|
2159
|
-
}
|
|
2160
|
-
const config = await readConfigValues(configManager);
|
|
2161
|
-
const registry = isConfigRecord(config.registry) ? config.registry : void 0;
|
|
2162
|
-
const targets = registry !== void 0 && isConfigRecord(registry.targets) ? registry.targets : void 0;
|
|
2163
|
-
const declared = targets !== void 0 && Object.hasOwn(targets, name) ? targets[name] : void 0;
|
|
2164
|
-
const production = isConfigRecord(declared) && declared.environment === "production";
|
|
2165
|
-
return {
|
|
2166
|
-
productionTargetNames: production ? [name] : [],
|
|
2167
|
-
allowProduction: config.allowProduction === true
|
|
2168
|
-
};
|
|
2169
|
-
}
|
|
2170
2142
|
async function readEntityUse(entity, readers) {
|
|
2171
2143
|
if (entity === void 0) return void 0;
|
|
2172
2144
|
const find = entity.type === "suite" ? readers.findSuite : readers.findTest;
|
|
@@ -2186,6 +2158,12 @@ var LivePlannerLimitsRefusedError = class extends Error {
|
|
|
2186
2158
|
this.name = "LivePlannerLimitsRefusedError";
|
|
2187
2159
|
}
|
|
2188
2160
|
};
|
|
2161
|
+
var LiveNavigationTimeoutRefusedError = class extends Error {
|
|
2162
|
+
constructor(message) {
|
|
2163
|
+
super(message);
|
|
2164
|
+
this.name = "LiveNavigationTimeoutRefusedError";
|
|
2165
|
+
}
|
|
2166
|
+
};
|
|
2189
2167
|
var LIVE_LIMIT_LEAVES = ["maxActions", "maxPlanningTurns"];
|
|
2190
2168
|
function shownLimitValue(value) {
|
|
2191
2169
|
return typeof value === "number" ? String(value) : JSON.stringify(value) ?? String(value);
|
|
@@ -2234,9 +2212,30 @@ async function resolveLivePlannerLimits(configManager, entities, readers) {
|
|
|
2234
2212
|
}
|
|
2235
2213
|
return (0, import_agent_qa_schema2.resolvePlannerLimits)(config.use, ...uses);
|
|
2236
2214
|
}
|
|
2237
|
-
function
|
|
2238
|
-
|
|
2239
|
-
|
|
2215
|
+
async function resolveLiveNavigationTimeoutMs(configManager, entities, readers) {
|
|
2216
|
+
const config = configManager === void 0 ? {} : await readConfigValues(configManager);
|
|
2217
|
+
const layers = [
|
|
2218
|
+
{ layer: "project config", use: config.use }
|
|
2219
|
+
];
|
|
2220
|
+
for (const entity of [].concat(entities ?? [])) {
|
|
2221
|
+
const use = await readEntityUse(entity, readers);
|
|
2222
|
+
layers.push({ layer: `${entity.type} file ${entity.id}`, use });
|
|
2223
|
+
}
|
|
2224
|
+
let navigation;
|
|
2225
|
+
for (const { layer, use } of layers) {
|
|
2226
|
+
if (!isConfigRecord(use) || !isConfigRecord(use.timeout)) continue;
|
|
2227
|
+
if (!Object.hasOwn(use.timeout, "navigation")) continue;
|
|
2228
|
+
const value = use.timeout.navigation;
|
|
2229
|
+
const parsed = import_agent_qa_core6.TimeoutConfigSchema.shape.navigation.safeParse(value);
|
|
2230
|
+
if (!parsed.success) {
|
|
2231
|
+
const issue = parsed.error.issues[0]?.message ?? "invalid value";
|
|
2232
|
+
throw new LiveNavigationTimeoutRefusedError(
|
|
2233
|
+
`${layer} sets use.timeout.navigation to ${shownLimitValue(value)}: ${issue}. The CLI refuses this file too.`
|
|
2234
|
+
);
|
|
2235
|
+
}
|
|
2236
|
+
navigation = parsed.data;
|
|
2237
|
+
}
|
|
2238
|
+
return navigation;
|
|
2240
2239
|
}
|
|
2241
2240
|
var IDLE_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
2242
2241
|
var LIVE_HUB_CLOSE_LIMIT_MS = 2e3;
|
|
@@ -2372,8 +2371,7 @@ var LiveSession = class {
|
|
|
2372
2371
|
planner = null;
|
|
2373
2372
|
verifier = null;
|
|
2374
2373
|
modelProfiles = null;
|
|
2375
|
-
//
|
|
2376
|
-
// rooted at the session's `.agent-qa` runtime dir (the FsStorageStateStore rooting idiom).
|
|
2374
|
+
// Built lazily on first use, rooted at the session's `.agent-qa` runtime dir.
|
|
2377
2375
|
storageStateStore = null;
|
|
2378
2376
|
model = null;
|
|
2379
2377
|
variableStore = null;
|
|
@@ -2401,15 +2399,14 @@ var LiveSession = class {
|
|
|
2401
2399
|
mobileAppiumLease = null;
|
|
2402
2400
|
secretStore;
|
|
2403
2401
|
secretRedactor;
|
|
2404
|
-
//
|
|
2405
|
-
// setup hooks failed has no adapter and therefore no hub; its subject declares `none`.
|
|
2402
|
+
// One hub per session at handle 0, created once the adapter exists; no adapter means no hub.
|
|
2406
2403
|
liveHub = null;
|
|
2407
2404
|
attachment = null;
|
|
2408
2405
|
resolveEnded = () => {
|
|
2409
2406
|
};
|
|
2410
2407
|
ended;
|
|
2411
2408
|
cleanupRun = null;
|
|
2412
|
-
//
|
|
2409
|
+
// Which control socket is the current one; only its close may detach the session.
|
|
2413
2410
|
attachmentGeneration = 0;
|
|
2414
2411
|
releaseAttachment = null;
|
|
2415
2412
|
get modelName() {
|
|
@@ -2431,11 +2428,7 @@ var LiveSession = class {
|
|
|
2431
2428
|
hub.register(0, registration?.capability, registration?.fallback);
|
|
2432
2429
|
this.liveHub = hub;
|
|
2433
2430
|
}
|
|
2434
|
-
|
|
2435
|
-
* Make a control socket the current one. `release` is how the session lets go of it later: the
|
|
2436
|
-
* previous attachment's is called with `superseded` here (428 WR-01), so one socket drives the
|
|
2437
|
-
* session at a time.
|
|
2438
|
-
*/
|
|
2431
|
+
// The previous attachment's `release` gets `superseded`, so one socket drives the session.
|
|
2439
2432
|
beginAttachment(release) {
|
|
2440
2433
|
const previous = this.releaseAttachment;
|
|
2441
2434
|
this.attachmentGeneration += 1;
|
|
@@ -2454,11 +2447,7 @@ var LiveSession = class {
|
|
|
2454
2447
|
}
|
|
2455
2448
|
this.pendingMessages = [];
|
|
2456
2449
|
}
|
|
2457
|
-
|
|
2458
|
-
* A command's reply, for whichever control socket is attached now (428 CR-01). A command can
|
|
2459
|
-
* outlive the socket that issued it, and its outcome belongs to the page, not to that socket:
|
|
2460
|
-
* with none attached it is held and flushed to the next one.
|
|
2461
|
-
*/
|
|
2450
|
+
// A reply can outlive its socket, so it goes to the current socket or waits for the next one.
|
|
2462
2451
|
deliver(message) {
|
|
2463
2452
|
if (this.messageSink) {
|
|
2464
2453
|
this.messageSink(message);
|
|
@@ -2569,7 +2558,7 @@ var LiveSession = class {
|
|
|
2569
2558
|
continue;
|
|
2570
2559
|
}
|
|
2571
2560
|
const result = await (0, import_agent_qa_runner.runHooks)([hookDef], {
|
|
2572
|
-
executor: new import_agent_qa_runner_local.DockerHookExecutor(),
|
|
2561
|
+
executor: new import_agent_qa_runner_local.DockerHookExecutor({ version: (0, import_agent_qa_core6.getAgentQaVersion)() }),
|
|
2573
2562
|
env: this.getHookEnvVars(),
|
|
2574
2563
|
secretStore: this.secretStore,
|
|
2575
2564
|
secretRedactor: this.secretRedactor
|
|
@@ -2643,7 +2632,6 @@ var LiveSession = class {
|
|
|
2643
2632
|
device: config.device ? {
|
|
2644
2633
|
name: config.device.name,
|
|
2645
2634
|
platform,
|
|
2646
|
-
transport: "local",
|
|
2647
2635
|
match: config.device
|
|
2648
2636
|
} : void 0
|
|
2649
2637
|
};
|
|
@@ -2708,15 +2696,6 @@ var LiveSession = class {
|
|
|
2708
2696
|
}
|
|
2709
2697
|
}
|
|
2710
2698
|
async initialize(config) {
|
|
2711
|
-
const found = await resolveLiveTargetProduction(this.deps.configManager, config.targetName);
|
|
2712
|
-
const refusal = liveProductionRefusal({
|
|
2713
|
-
productionTargetNames: (0, import_agent_qa_schema2.distinctInFirstOccurrenceOrder)([
|
|
2714
|
-
...config.production?.productionTargetNames ?? [],
|
|
2715
|
-
...found.productionTargetNames
|
|
2716
|
-
]),
|
|
2717
|
-
allowProduction: found.allowProduction
|
|
2718
|
-
});
|
|
2719
|
-
if (refusal !== void 0) throw refusal;
|
|
2720
2699
|
this.platform = config.platform;
|
|
2721
2700
|
this.targetName = typeof config.targetName === "string" && config.targetName.trim().length > 0 ? config.targetName.trim() : null;
|
|
2722
2701
|
this._modelName = config.llmConfig.model ?? "unknown";
|
|
@@ -2751,7 +2730,11 @@ var LiveSession = class {
|
|
|
2751
2730
|
} else {
|
|
2752
2731
|
const { WebPlatformAdapter } = await import("@vostride/agent-qa-web");
|
|
2753
2732
|
const adapter = new WebPlatformAdapter();
|
|
2754
|
-
await adapter.setup({
|
|
2733
|
+
await adapter.setup({
|
|
2734
|
+
platform: "web",
|
|
2735
|
+
browser: { name: "chromium", headless: config.headless ?? true },
|
|
2736
|
+
...config.navigationTimeoutMs === void 0 ? {} : { timeouts: { navigation: config.navigationTimeoutMs } }
|
|
2737
|
+
});
|
|
2755
2738
|
this.adapter = adapter;
|
|
2756
2739
|
if (config.url) {
|
|
2757
2740
|
const resolvedUrl = this.resolveSecrets(config.url);
|
|
@@ -2813,7 +2796,7 @@ var LiveSession = class {
|
|
|
2813
2796
|
}
|
|
2814
2797
|
const startedAt = performance.now();
|
|
2815
2798
|
try {
|
|
2816
|
-
const result = await new import_agent_qa_runner_local.DockerHookExecutor().execute(hookDef, {
|
|
2799
|
+
const result = await new import_agent_qa_runner_local.DockerHookExecutor({ version: (0, import_agent_qa_core6.getAgentQaVersion)() }).execute(hookDef, {
|
|
2817
2800
|
env: this.getHookEnvVars(),
|
|
2818
2801
|
secretEnv: (0, import_agent_qa_runner.resolveSecretEnv)(this.secretStore),
|
|
2819
2802
|
secretRedactor: this.secretRedactor
|
|
@@ -2980,12 +2963,7 @@ var LiveSession = class {
|
|
|
2980
2963
|
};
|
|
2981
2964
|
return this.redact(result);
|
|
2982
2965
|
}
|
|
2983
|
-
//
|
|
2984
|
-
// lifecycle (executeStepCommand for single steps, executeTestCommand for
|
|
2985
|
-
// the per-test loop that runs many steps under one outer gate).
|
|
2986
|
-
// D-14 (Phase 360): lazily construct the run-scoped storage-state store, rooted at the session's
|
|
2987
|
-
// `.agent-qa` runtime dir. All fs writes live in runner-local (validate:arch storageNeutral);
|
|
2988
|
-
// the loop records only { name, fingerprint }. Absent configPath falls back to the cwd root.
|
|
2966
|
+
// Lazily built and rooted at the session's `.agent-qa` dir; fs writes stay in runner-local.
|
|
2989
2967
|
async getStorageStateStore() {
|
|
2990
2968
|
let store = this.storageStateStore;
|
|
2991
2969
|
if (!store) {
|
|
@@ -3124,7 +3102,9 @@ ${errorLines.join("\n")}`;
|
|
|
3124
3102
|
testLimits = this.plannerLimitsForTest !== void 0 && testDraft.testId.length > 0 ? await this.plannerLimitsForTest(testDraft.testId) : this.plannerLimits ?? (0, import_agent_qa_schema2.resolvePlannerLimits)();
|
|
3125
3103
|
} catch (error) {
|
|
3126
3104
|
if (!(error instanceof LivePlannerLimitsRefusedError)) throw error;
|
|
3127
|
-
|
|
3105
|
+
const line = `[live-session] executeTestCommand: ${error.message}`;
|
|
3106
|
+
const detail = { testName: testDraft.testName };
|
|
3107
|
+
(0, import_agent_qa_core6.printRunText)((0, import_node_util3.format)(line, detail), () => console.warn(line, detail));
|
|
3128
3108
|
return this.redact({
|
|
3129
3109
|
status: "failed",
|
|
3130
3110
|
duration: performance.now() - start,
|
|
@@ -3144,11 +3124,13 @@ ${errorLines.join("\n")}`;
|
|
|
3144
3124
|
if (!setupResult.allPassed) {
|
|
3145
3125
|
const failedHook = setupHookPayloads.find((h) => h.status === "failed");
|
|
3146
3126
|
const detail = failedHook ? `Setup hook "${failedHook.hookName}" failed${failedHook.error ? `: ${failedHook.error}` : ""}${failedHook.stderr ? ` \u2014 ${failedHook.stderr.split("\n")[0].slice(0, 200)}` : ""}` : "Per-test setup hook failed";
|
|
3147
|
-
|
|
3127
|
+
const line = `[live-session] executeTestCommand: ${detail}`;
|
|
3128
|
+
const context = {
|
|
3148
3129
|
testName: testDraft.testName,
|
|
3149
3130
|
setupHooks: testDraft.setup,
|
|
3150
3131
|
failedHook: failedHook ? { name: failedHook.hookName, stderr: failedHook.stderr, error: failedHook.error } : null
|
|
3151
|
-
}
|
|
3132
|
+
};
|
|
3133
|
+
(0, import_agent_qa_core6.printRunText)((0, import_node_util3.format)(line, context), () => console.warn(line, context));
|
|
3152
3134
|
return {
|
|
3153
3135
|
status: "failed",
|
|
3154
3136
|
duration: performance.now() - start,
|
|
@@ -3226,7 +3208,9 @@ ${errorLines.join("\n")}`;
|
|
|
3226
3208
|
topLevelError = failedHook ? `Teardown hook "${failedHook.hookName}" failed${failedHook.error ? `: ${failedHook.error}` : ""}` : "Teardown hook failed";
|
|
3227
3209
|
}
|
|
3228
3210
|
if (topLevelError) {
|
|
3229
|
-
|
|
3211
|
+
const line = `[live-session] executeTestCommand: ${topLevelError}`;
|
|
3212
|
+
const detail = { testName: testDraft.testName };
|
|
3213
|
+
(0, import_agent_qa_core6.printRunText)((0, import_node_util3.format)(line, detail), () => console.warn(line, detail));
|
|
3230
3214
|
}
|
|
3231
3215
|
return this.redact({
|
|
3232
3216
|
status: finalStatus,
|
|
@@ -3242,10 +3226,7 @@ ${errorLines.join("\n")}`;
|
|
|
3242
3226
|
this.status = "idle";
|
|
3243
3227
|
}
|
|
3244
3228
|
}
|
|
3245
|
-
//
|
|
3246
|
-
// hook payloads (hook-complete) for inclusion in the LiveTestResultPayload.
|
|
3247
|
-
// The capture sits beside the message sink rather than wrapping it, so a
|
|
3248
|
-
// control socket that reattaches mid-hook keeps its sink (428 CR-01).
|
|
3229
|
+
// Captures dispatched hook payloads beside the message sink, so a reattached socket keeps it.
|
|
3249
3230
|
async runLifecycleHooksCapturing(phase, hookIds, sink, owner = { scope: "suite" }) {
|
|
3250
3231
|
this.hookCaptures.add(sink);
|
|
3251
3232
|
try {
|
|
@@ -3489,6 +3470,21 @@ function refuseUnparseableRequest(res) {
|
|
|
3489
3470
|
});
|
|
3490
3471
|
res.end(body);
|
|
3491
3472
|
}
|
|
3473
|
+
function refuseForeignHostPage(res, port) {
|
|
3474
|
+
if (res.headersSent) {
|
|
3475
|
+
res.destroy();
|
|
3476
|
+
return;
|
|
3477
|
+
}
|
|
3478
|
+
const p = String(port);
|
|
3479
|
+
const body = `The agent-qa dashboard answers only at localhost, 127.0.0.1 or [::1].
|
|
3480
|
+
Open http://localhost:${p}/ on the machine it runs on, or forward the port with ssh -L ${p}:localhost:${p} <host>.
|
|
3481
|
+
`;
|
|
3482
|
+
res.writeHead(403, {
|
|
3483
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
3484
|
+
"Content-Length": Buffer.byteLength(body)
|
|
3485
|
+
});
|
|
3486
|
+
res.end(body);
|
|
3487
|
+
}
|
|
3492
3488
|
function backstopServerError(res, req, error) {
|
|
3493
3489
|
const kind = error instanceof Error ? error.constructor.name : typeof error;
|
|
3494
3490
|
console.error(
|
|
@@ -3568,10 +3564,6 @@ function respondJson(res, status, data) {
|
|
|
3568
3564
|
res.end(body);
|
|
3569
3565
|
}
|
|
3570
3566
|
function serveRunEvidenceRead(req, res, deps) {
|
|
3571
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req)) {
|
|
3572
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
3573
|
-
return;
|
|
3574
|
-
}
|
|
3575
3567
|
const query = new URL(req.url ?? "/", "http://localhost").searchParams;
|
|
3576
3568
|
const entries = [["runId", deps.runId]];
|
|
3577
3569
|
const seen = /* @__PURE__ */ new Set(["runId"]);
|
|
@@ -3613,10 +3605,6 @@ function respondFeedbackRead(res, deps, functionId, input, parse) {
|
|
|
3613
3605
|
});
|
|
3614
3606
|
}
|
|
3615
3607
|
function serveRunMarksHistoryRead(req, res, deps) {
|
|
3616
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
|
|
3617
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
3618
|
-
return;
|
|
3619
|
-
}
|
|
3620
3608
|
if (new URL(req.url ?? "/", "http://localhost").search !== "") {
|
|
3621
3609
|
respondJson(res, 400, { error: "invalid_marks_input" });
|
|
3622
3610
|
return;
|
|
@@ -3635,10 +3623,6 @@ function serveRunMarksHistoryRead(req, res, deps) {
|
|
|
3635
3623
|
);
|
|
3636
3624
|
}
|
|
3637
3625
|
function serveRunMarkPreviewRead(req, res, deps) {
|
|
3638
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
|
|
3639
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
3640
|
-
return;
|
|
3641
|
-
}
|
|
3642
3626
|
void readJsonBody(req).then((body) => {
|
|
3643
3627
|
const parsed = import_agent_qa_schema3.RunsMarkInputSchema.safeParse(body);
|
|
3644
3628
|
if (!parsed.success) {
|
|
@@ -3661,10 +3645,6 @@ function serveRunMarkPreviewRead(req, res, deps) {
|
|
|
3661
3645
|
});
|
|
3662
3646
|
}
|
|
3663
3647
|
function serveRunDetailRead(req, res, deps) {
|
|
3664
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
|
|
3665
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
3666
|
-
return;
|
|
3667
|
-
}
|
|
3668
3648
|
void readRunDetail(deps.host, deps.runId).then(
|
|
3669
3649
|
(outcome) => {
|
|
3670
3650
|
if (outcome.kind === "refused") {
|
|
@@ -3814,10 +3794,6 @@ async function readRunAnalysis(host, runId) {
|
|
|
3814
3794
|
}
|
|
3815
3795
|
}
|
|
3816
3796
|
function serveRunAnalysisRead(req, res, deps) {
|
|
3817
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
|
|
3818
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
3819
|
-
return;
|
|
3820
|
-
}
|
|
3821
3797
|
void readRunAnalysis(deps.host, deps.runId).then(
|
|
3822
3798
|
(outcome) => {
|
|
3823
3799
|
if (outcome.kind === "refused") {
|
|
@@ -4041,10 +4017,7 @@ function serveRunEventStream(req, res, deps) {
|
|
|
4041
4017
|
else pending.push(frame);
|
|
4042
4018
|
};
|
|
4043
4019
|
const frameOf = (event) => (
|
|
4044
|
-
//
|
|
4045
|
-
// data IS the envelope's own JSON. No projection, no rename, no second vocabulary. The filter
|
|
4046
|
-
// DROPS envelopes; it never edits one, so a filtered stream's frames are byte-identical to the
|
|
4047
|
-
// firehose's — with gaps in the ids, which is what seq-as-id means once a filter exists.
|
|
4020
|
+
// The filter drops envelopes and never edits one, so filtered frames match the firehose bytes.
|
|
4048
4021
|
`id: ${String(event.seq)}
|
|
4049
4022
|
event: ${event.type}
|
|
4050
4023
|
data: ${JSON.stringify(event)}
|
|
@@ -4106,12 +4079,6 @@ data: ${JSON.stringify(event)}
|
|
|
4106
4079
|
deps.openStreams?.add(teardown);
|
|
4107
4080
|
req.on("close", teardown);
|
|
4108
4081
|
}
|
|
4109
|
-
function runDenialOf(error) {
|
|
4110
|
-
if (typeof error !== "object" || error === null) return void 0;
|
|
4111
|
-
const neutralReason = error.neutralReason;
|
|
4112
|
-
if (typeof neutralReason !== "string" || neutralReason.trim().length === 0) return void 0;
|
|
4113
|
-
return { error: "run_denied", neutralReason };
|
|
4114
|
-
}
|
|
4115
4082
|
function createRunServiceRoutes(ports) {
|
|
4116
4083
|
const openStreams = /* @__PURE__ */ new Set();
|
|
4117
4084
|
function handleStart(req, res) {
|
|
@@ -4142,12 +4109,7 @@ function createRunServiceRoutes(ports) {
|
|
|
4142
4109
|
ack = await ports.invokeRunStart(
|
|
4143
4110
|
parsed.data.patterns === void 0 ? {} : { patterns: parsed.data.patterns }
|
|
4144
4111
|
);
|
|
4145
|
-
} catch
|
|
4146
|
-
const denial = runDenialOf(error);
|
|
4147
|
-
if (denial !== void 0) {
|
|
4148
|
-
respondJson2(res, 403, denial);
|
|
4149
|
-
return;
|
|
4150
|
-
}
|
|
4112
|
+
} catch {
|
|
4151
4113
|
refuse(res, 500, "run_start_failed");
|
|
4152
4114
|
return;
|
|
4153
4115
|
}
|
|
@@ -4227,6 +4189,10 @@ function createRunServiceRoutes(ports) {
|
|
|
4227
4189
|
refuse(res, 403, "cross_site_refused");
|
|
4228
4190
|
return;
|
|
4229
4191
|
}
|
|
4192
|
+
if (carriesNonJsonBody(req)) {
|
|
4193
|
+
refuse(res, 415, "unsupported_media_type");
|
|
4194
|
+
return;
|
|
4195
|
+
}
|
|
4230
4196
|
const matched = matchRunServiceRoute(req.method, path);
|
|
4231
4197
|
if (matched === void 0) {
|
|
4232
4198
|
refuse(res, 404, "unknown_route");
|
|
@@ -4492,8 +4458,7 @@ data: ${JSON.stringify(again.body)}
|
|
|
4492
4458
|
// A failed re-read is not a reason to close a live channel: the next envelope will trigger
|
|
4493
4459
|
// another, and the page's own poll is the floor underneath both.
|
|
4494
4460
|
() => void 0
|
|
4495
|
-
//
|
|
4496
|
-
// failure arm never sees its own fulfilment arm's throw (UF-01, on the nested chain).
|
|
4461
|
+
// A throw from the arm above is a dead socket, and `.then`'s failure arm never sees it.
|
|
4497
4462
|
).catch(failStream);
|
|
4498
4463
|
});
|
|
4499
4464
|
heartbeat = setInterval(() => {
|
|
@@ -4530,10 +4495,6 @@ async function readRunKnowledge(host, runId) {
|
|
|
4530
4495
|
}
|
|
4531
4496
|
}
|
|
4532
4497
|
function serveRunKnowledgeRead(req, res, deps) {
|
|
4533
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
|
|
4534
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
4535
|
-
return;
|
|
4536
|
-
}
|
|
4537
4498
|
void readRunKnowledge(deps.host, deps.runId).then(
|
|
4538
4499
|
(outcome) => {
|
|
4539
4500
|
if (outcome.kind === "refused") {
|
|
@@ -4574,10 +4535,6 @@ async function markRun(host, runId, body) {
|
|
|
4574
4535
|
}
|
|
4575
4536
|
}
|
|
4576
4537
|
function registerRunMarkRoute(req, res, deps) {
|
|
4577
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
|
|
4578
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
4579
|
-
return;
|
|
4580
|
-
}
|
|
4581
4538
|
void readJsonBody(req).then(async (body) => {
|
|
4582
4539
|
const outcome = await markRun(deps.host, deps.runId, body);
|
|
4583
4540
|
if (outcome.kind === "refused") {
|
|
@@ -4595,10 +4552,6 @@ function registerRunMarkRoute(req, res, deps) {
|
|
|
4595
4552
|
// src/server/config-routes.ts
|
|
4596
4553
|
var import_agent_qa_schema9 = require("@vostride/agent-qa-schema");
|
|
4597
4554
|
function registerConfigWriteRoute(req, res, deps) {
|
|
4598
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
4599
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
4600
|
-
return;
|
|
4601
|
-
}
|
|
4602
4555
|
void readJsonBody(req).then(async (body) => {
|
|
4603
4556
|
const parsed = import_agent_qa_schema9.ConfigWriteRequestSchema.safeParse(body);
|
|
4604
4557
|
if (!parsed.success) {
|
|
@@ -4656,10 +4609,6 @@ function registerConfigUndoRoute(req, res, deps) {
|
|
|
4656
4609
|
respondJson(res, 405, { error: "method_not_allowed" });
|
|
4657
4610
|
return;
|
|
4658
4611
|
}
|
|
4659
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
4660
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
4661
|
-
return;
|
|
4662
|
-
}
|
|
4663
4612
|
void readJsonBody(req).then(async (body) => {
|
|
4664
4613
|
const parsed = import_agent_qa_schema9.ConfigUndoRequestSchema.safeParse(body);
|
|
4665
4614
|
if (!parsed.success) {
|
|
@@ -4889,10 +4838,6 @@ async function connectName(request2, deps) {
|
|
|
4889
4838
|
});
|
|
4890
4839
|
}
|
|
4891
4840
|
function registerLlmConnectRoute(req, res, deps) {
|
|
4892
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
4893
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
4894
|
-
return;
|
|
4895
|
-
}
|
|
4896
4841
|
void readJsonBody(req).then(async (body) => {
|
|
4897
4842
|
const answer = await connectLlm(body, deps);
|
|
4898
4843
|
respondJson(res, answer.status, answer.body);
|
|
@@ -5214,25 +5159,10 @@ function llmConnectionTestTimeoutMs(config) {
|
|
|
5214
5159
|
return REMOTE_LLM_CONNECTION_TEST_TIMEOUT_MS;
|
|
5215
5160
|
}
|
|
5216
5161
|
function json(res, data, status = 200) {
|
|
5217
|
-
const body = JSON.stringify(data);
|
|
5218
|
-
res.writeHead(status, {
|
|
5219
|
-
"Content-Type": "application/json",
|
|
5220
|
-
"Access-Control-Allow-Origin": "*",
|
|
5221
|
-
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
5222
|
-
"Access-Control-Allow-Headers": "Content-Type"
|
|
5223
|
-
});
|
|
5224
|
-
res.end(body);
|
|
5225
|
-
}
|
|
5226
|
-
function jsonNoCors(res, data, status = 200) {
|
|
5227
5162
|
const body = JSON.stringify(data);
|
|
5228
5163
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
5229
5164
|
res.end(body);
|
|
5230
5165
|
}
|
|
5231
|
-
function refuseCrossSiteDisplayRead(req, res) {
|
|
5232
|
-
if (refuseCrossSiteRequest(req) === void 0) return false;
|
|
5233
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
5234
|
-
return true;
|
|
5235
|
-
}
|
|
5236
5166
|
var INSIGHTS_KNOWLEDGE_RUN_CAP = 200;
|
|
5237
5167
|
var INSIGHTS_KNOWLEDGE_CONCURRENCY = 4;
|
|
5238
5168
|
async function isPostProcessingSkipped(host, runId) {
|
|
@@ -5296,14 +5226,6 @@ async function readInsightsKnowledgeChanges(host, db, window) {
|
|
|
5296
5226
|
function notFound(res, message = "Not found") {
|
|
5297
5227
|
json(res, { error: message }, 404);
|
|
5298
5228
|
}
|
|
5299
|
-
function cors(res) {
|
|
5300
|
-
res.writeHead(204, {
|
|
5301
|
-
"Access-Control-Allow-Origin": "*",
|
|
5302
|
-
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
5303
|
-
"Access-Control-Allow-Headers": "Content-Type"
|
|
5304
|
-
});
|
|
5305
|
-
res.end();
|
|
5306
|
-
}
|
|
5307
5229
|
function isPlainRecord(value) {
|
|
5308
5230
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5309
5231
|
}
|
|
@@ -5356,7 +5278,7 @@ function normalizeRunFilterValue(value) {
|
|
|
5356
5278
|
function buildRunRequestAttributes(input) {
|
|
5357
5279
|
const internal = (0, import_agent_qa_core9.buildInternalRunAttributes)({
|
|
5358
5280
|
trigger: input.trigger,
|
|
5359
|
-
runner:
|
|
5281
|
+
runner: "local"
|
|
5360
5282
|
});
|
|
5361
5283
|
const user = (0, import_agent_qa_core9.validateUserRunAttributes)(input.userAttributes, "run attributes");
|
|
5362
5284
|
return (0, import_agent_qa_core9.mergeRunAttributes)(internal, user);
|
|
@@ -5801,9 +5723,7 @@ var RESPONSE_SAFE_RUN_METADATA_KEYS = /* @__PURE__ */ new Set([
|
|
|
5801
5723
|
"timeoutSource",
|
|
5802
5724
|
"timeoutBaseMs",
|
|
5803
5725
|
"timeoutBufferMs",
|
|
5804
|
-
"suiteIndex"
|
|
5805
|
-
"farmSessionUrl",
|
|
5806
|
-
"runDevice"
|
|
5726
|
+
"suiteIndex"
|
|
5807
5727
|
]);
|
|
5808
5728
|
function sanitizeRunMetadataForResponse(metadata) {
|
|
5809
5729
|
if (metadata === null || metadata === void 0) return metadata ?? null;
|
|
@@ -5833,17 +5753,7 @@ var RESPONSE_SAFE_RUN_READ_METHODS = [
|
|
|
5833
5753
|
"getRunsByTest"
|
|
5834
5754
|
];
|
|
5835
5755
|
var RUN_READS_WITHOUT_ROWS = /* @__PURE__ */ new Set([
|
|
5836
|
-
//
|
|
5837
|
-
//
|
|
5838
|
-
// The five WRITES return nothing; they are here for the same reason `insertRun` is.
|
|
5839
|
-
//
|
|
5840
|
-
// `readRunDetailRows` IS A READ AND IS STILL HERE, and that is the judgement this set exists to
|
|
5841
|
-
// make explicit rather than to hide. It returns members, steps and sub-actions — the bus
|
|
5842
|
-
// payloads as the subscriber MASKED them (T-363-05) — and NO `RunRow` and no
|
|
5843
|
-
// `RunArtifactRow`. It therefore carries none of the run metadata the 401-23 scrub law exists
|
|
5844
|
-
// to strip, so wrapping it would be wrapping a shape that has nothing to strip. This router
|
|
5845
|
-
// does not call it either: the detail arm dispatches through the injected host, which is the
|
|
5846
|
-
// whole of D-03.
|
|
5756
|
+
// `readRunDetailRows` returns masked bus payloads and no run row, so it has no metadata to strip.
|
|
5847
5757
|
"deleteMemberProjection",
|
|
5848
5758
|
"incrementRunMemberAttempts",
|
|
5849
5759
|
"insertRunStep",
|
|
@@ -5854,23 +5764,23 @@ var RUN_READS_WITHOUT_ROWS = /* @__PURE__ */ new Set([
|
|
|
5854
5764
|
"finalizeRunArtifact",
|
|
5855
5765
|
"getRunsBySuiteIdCount",
|
|
5856
5766
|
"getRunsByTestCount",
|
|
5857
|
-
//
|
|
5767
|
+
// Run ids and a count, no row; each id is dispatched through the agent host.
|
|
5858
5768
|
"listInsightsWindowRunIds",
|
|
5859
5769
|
"insertPendingRun",
|
|
5860
5770
|
"insertRun",
|
|
5861
5771
|
"insertRunArtifact",
|
|
5862
5772
|
"listRunAttributeKeys",
|
|
5863
5773
|
"listRunAttributeValues",
|
|
5864
|
-
//
|
|
5774
|
+
// An owner (pid, incarnation, host), no run metadata.
|
|
5865
5775
|
"readRunOwner",
|
|
5866
|
-
//
|
|
5776
|
+
// Ids and owners only, no run metadata.
|
|
5867
5777
|
"listRunningRunOwners",
|
|
5868
|
-
//
|
|
5778
|
+
// Compare-and-set writes that return a boolean.
|
|
5869
5779
|
"finalizeOrphanRun",
|
|
5870
5780
|
"finalizeOwnRun",
|
|
5871
|
-
//
|
|
5781
|
+
// A compare-and-set write that returns a boolean, no run metadata.
|
|
5872
5782
|
"closeOwnRun",
|
|
5873
|
-
//
|
|
5783
|
+
// A compare-and-set write that returns a boolean, no run metadata.
|
|
5874
5784
|
"claimRunningRow",
|
|
5875
5785
|
"stageRunArtifact",
|
|
5876
5786
|
"updateRun",
|
|
@@ -5941,10 +5851,7 @@ function withResponseSafeRunReads(db) {
|
|
|
5941
5851
|
// `sanitizeArtifactForResponse`; a caller reaching for the bare row has no such step, so the
|
|
5942
5852
|
// projection is its only choke point. Same allowlist, so the two cannot drift.
|
|
5943
5853
|
getRunArtifact: { value: (runId) => scrubArtifact(db.getRunArtifact(runId)) },
|
|
5944
|
-
//
|
|
5945
|
-
// parent, whose `run` rows this override scrubbed; nothing has written a child row since
|
|
5946
|
-
// 402-09, and the shape went with the column. `getRunArtifact` above remains the choke point
|
|
5947
|
-
// for the payload itself, and this wrapper delegates to it.
|
|
5854
|
+
// One artifact; `getRunArtifact` is the choke point for its payload, and this delegates to it.
|
|
5948
5855
|
getRunArtifactBundle: {
|
|
5949
5856
|
value: (runId) => ({ artifact: scrubArtifact(db.getRunArtifactBundle(runId).artifact) })
|
|
5950
5857
|
}
|
|
@@ -6092,10 +5999,18 @@ function createRouter(dbOrDeps) {
|
|
|
6092
5999
|
json(res, { error: "Bad Request" }, 400);
|
|
6093
6000
|
return;
|
|
6094
6001
|
}
|
|
6002
|
+
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
6003
|
+
json(res, { error: "cross_site_refused" }, 403);
|
|
6004
|
+
return;
|
|
6005
|
+
}
|
|
6006
|
+
if (carriesNonJsonBody(req)) {
|
|
6007
|
+
json(res, { error: "unsupported_media_type" }, 415);
|
|
6008
|
+
return;
|
|
6009
|
+
}
|
|
6095
6010
|
const path = url.pathname;
|
|
6096
6011
|
if (path === "/api/config" && req.method === "PATCH") {
|
|
6097
6012
|
if (!configManager) {
|
|
6098
|
-
|
|
6013
|
+
json(res, { error: "config_unavailable" }, 503);
|
|
6099
6014
|
return;
|
|
6100
6015
|
}
|
|
6101
6016
|
registerConfigWriteRoute(req, res, { configManager, undoStore });
|
|
@@ -6115,7 +6030,7 @@ function createRouter(dbOrDeps) {
|
|
|
6115
6030
|
}
|
|
6116
6031
|
if (path === "/api/config/revision" && req.method === "GET") {
|
|
6117
6032
|
if (!configManager) {
|
|
6118
|
-
|
|
6033
|
+
json(res, { error: "config_unavailable" }, 503);
|
|
6119
6034
|
return;
|
|
6120
6035
|
}
|
|
6121
6036
|
registerConfigRevisionRoute(req, res, { configManager });
|
|
@@ -6123,7 +6038,7 @@ function createRouter(dbOrDeps) {
|
|
|
6123
6038
|
}
|
|
6124
6039
|
if (path === "/api/llm/connect" && req.method === "POST") {
|
|
6125
6040
|
if (!configManager) {
|
|
6126
|
-
|
|
6041
|
+
json(res, { error: "config_unavailable" }, 503);
|
|
6127
6042
|
return;
|
|
6128
6043
|
}
|
|
6129
6044
|
registerLlmConnectRoute(req, res, {
|
|
@@ -6135,16 +6050,6 @@ function createRouter(dbOrDeps) {
|
|
|
6135
6050
|
});
|
|
6136
6051
|
return;
|
|
6137
6052
|
}
|
|
6138
|
-
if (path === "/api/runs/trigger" || path === "/api/queue/enqueue") {
|
|
6139
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
6140
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
6141
|
-
return;
|
|
6142
|
-
}
|
|
6143
|
-
}
|
|
6144
|
-
if (req.method === "OPTIONS") {
|
|
6145
|
-
cors(res);
|
|
6146
|
-
return;
|
|
6147
|
-
}
|
|
6148
6053
|
if (path === "/api/auth-states" && req.method === "GET") {
|
|
6149
6054
|
if (!configManager || !deps.configPath) {
|
|
6150
6055
|
json(res, { error: "Auth-state metadata not available" }, 503);
|
|
@@ -6168,11 +6073,10 @@ function createRouter(dbOrDeps) {
|
|
|
6168
6073
|
return;
|
|
6169
6074
|
}
|
|
6170
6075
|
if (path === "/api/runs" && req.method === "GET") {
|
|
6171
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
6172
6076
|
const statuses = url.searchParams.getAll("status");
|
|
6173
6077
|
const unknownStatus = statuses.find((word) => !isRunStatusWord(word));
|
|
6174
6078
|
if (unknownStatus !== void 0) {
|
|
6175
|
-
|
|
6079
|
+
json(res, { error: `Unknown run status: ${unknownStatus}.` }, 400);
|
|
6176
6080
|
return;
|
|
6177
6081
|
}
|
|
6178
6082
|
const name = url.searchParams.get("name") ?? void 0;
|
|
@@ -6204,12 +6108,12 @@ function createRouter(dbOrDeps) {
|
|
|
6204
6108
|
)).sort((a, b) => a.localeCompare(b));
|
|
6205
6109
|
const filtered = platformFilteredRuns.filter((run) => matchesTargetFilter(run, target));
|
|
6206
6110
|
const runs = filtered.slice(offset, offset + limit);
|
|
6207
|
-
|
|
6111
|
+
json(res, {
|
|
6208
6112
|
runs,
|
|
6209
6113
|
total: filtered.length,
|
|
6210
6114
|
targets: targetOptions
|
|
6211
6115
|
});
|
|
6212
|
-
}).catch(() =>
|
|
6116
|
+
}).catch(() => json(res, { error: "Failed to list runs" }, 500));
|
|
6213
6117
|
return;
|
|
6214
6118
|
}
|
|
6215
6119
|
if (path === "/api/runs/attributes/keys" && req.method === "GET") {
|
|
@@ -6247,7 +6151,6 @@ function createRouter(dbOrDeps) {
|
|
|
6247
6151
|
}
|
|
6248
6152
|
const runArtifactMatch = path.match(/^\/api\/runs\/([^/]+)\/artifact$/);
|
|
6249
6153
|
if (runArtifactMatch && req.method === "GET") {
|
|
6250
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
6251
6154
|
const runId = decodeURIComponent(runArtifactMatch[1]);
|
|
6252
6155
|
const run = db.getRun(runId);
|
|
6253
6156
|
if (!run) {
|
|
@@ -6255,7 +6158,7 @@ function createRouter(dbOrDeps) {
|
|
|
6255
6158
|
return;
|
|
6256
6159
|
}
|
|
6257
6160
|
const bundle = db.getRunArtifactBundle(runId);
|
|
6258
|
-
|
|
6161
|
+
json(res, {
|
|
6259
6162
|
run,
|
|
6260
6163
|
artifact: sanitizeArtifactForResponse(bundle.artifact),
|
|
6261
6164
|
missingSections: getArtifactMissingSections(bundle.artifact)
|
|
@@ -6263,7 +6166,6 @@ function createRouter(dbOrDeps) {
|
|
|
6263
6166
|
return;
|
|
6264
6167
|
}
|
|
6265
6168
|
if (path.startsWith("/api/runs/") && path.endsWith("/execution-logs") && req.method === "GET") {
|
|
6266
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
6267
6169
|
const segments = path.split("/");
|
|
6268
6170
|
const id = segments[3];
|
|
6269
6171
|
const run = db.getRun(id);
|
|
@@ -6274,37 +6176,37 @@ function createRouter(dbOrDeps) {
|
|
|
6274
6176
|
const stepId = url.searchParams.get("stepId") ?? void 0;
|
|
6275
6177
|
const type = url.searchParams.get("type") ?? void 0;
|
|
6276
6178
|
const logs = db.getExecutionLogs({ runId: id, stepId, type });
|
|
6277
|
-
|
|
6179
|
+
json(res, { logs });
|
|
6278
6180
|
return;
|
|
6279
6181
|
}
|
|
6280
6182
|
if (path.startsWith("/api/runs/") && path.endsWith("/mark") && req.method === "POST") {
|
|
6281
6183
|
if (deps.agentHost === void 0) {
|
|
6282
|
-
|
|
6184
|
+
json(res, { error: "agent_host_unavailable" }, 503);
|
|
6283
6185
|
return;
|
|
6284
6186
|
}
|
|
6285
6187
|
let runId;
|
|
6286
6188
|
try {
|
|
6287
6189
|
runId = decodeURIComponent(path.slice("/api/runs/".length, -"/mark".length));
|
|
6288
6190
|
} catch {
|
|
6289
|
-
|
|
6191
|
+
json(res, { error: "invalid_mark_input" }, 400);
|
|
6290
6192
|
return;
|
|
6291
6193
|
}
|
|
6292
|
-
registerRunMarkRoute(req, res, { host: deps.agentHost, runId
|
|
6194
|
+
registerRunMarkRoute(req, res, { host: deps.agentHost, runId });
|
|
6293
6195
|
return;
|
|
6294
6196
|
}
|
|
6295
6197
|
if (req.method === "POST" && /^\/api\/runs\/[^/]+\/mark-preview$/.test(path)) {
|
|
6296
6198
|
if (deps.agentHost === void 0) {
|
|
6297
|
-
|
|
6199
|
+
json(res, { error: "agent_host_unavailable" }, 503);
|
|
6298
6200
|
return;
|
|
6299
6201
|
}
|
|
6300
6202
|
let runId;
|
|
6301
6203
|
try {
|
|
6302
6204
|
runId = decodeURIComponent(path.slice("/api/runs/".length, -"/mark-preview".length));
|
|
6303
6205
|
} catch {
|
|
6304
|
-
|
|
6206
|
+
json(res, { error: "invalid_run_id" }, 400);
|
|
6305
6207
|
return;
|
|
6306
6208
|
}
|
|
6307
|
-
serveRunMarkPreviewRead(req, res, { runId, host: deps.agentHost
|
|
6209
|
+
serveRunMarkPreviewRead(req, res, { runId, host: deps.agentHost });
|
|
6308
6210
|
return;
|
|
6309
6211
|
}
|
|
6310
6212
|
if (path.startsWith("/api/runs/") && path.endsWith("/cancel") && req.method === "POST") {
|
|
@@ -6383,44 +6285,39 @@ function createRouter(dbOrDeps) {
|
|
|
6383
6285
|
return;
|
|
6384
6286
|
}
|
|
6385
6287
|
if (req.method === "GET" && /^\/api\/runs\/[^/]+\/live-url$/.test(path)) {
|
|
6386
|
-
const crossSite = refuseCrossSiteRequest(req);
|
|
6387
|
-
if (crossSite !== void 0) {
|
|
6388
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
6389
|
-
return;
|
|
6390
|
-
}
|
|
6391
6288
|
if (!deps.mintLiveUrl) {
|
|
6392
|
-
|
|
6289
|
+
json(res, { error: "live_view_unavailable" }, 503);
|
|
6393
6290
|
return;
|
|
6394
6291
|
}
|
|
6395
6292
|
let liveUrlRunId;
|
|
6396
6293
|
try {
|
|
6397
6294
|
liveUrlRunId = decodeURIComponent(path.slice("/api/runs/".length, -"/live-url".length));
|
|
6398
6295
|
} catch {
|
|
6399
|
-
|
|
6296
|
+
json(res, { error: "unknown_run" }, 404);
|
|
6400
6297
|
return;
|
|
6401
6298
|
}
|
|
6402
6299
|
const rawHandle = handleParamOf(req);
|
|
6403
6300
|
const parsedHandle = (0, import_agent_qa_schema11.parseHandleParam)(rawHandle);
|
|
6404
6301
|
if (parsedHandle.kind === "invalid") {
|
|
6405
|
-
|
|
6302
|
+
json(res, { error: "invalid_handle" }, 400);
|
|
6406
6303
|
return;
|
|
6407
6304
|
}
|
|
6408
6305
|
const handle = parsedHandle.kind === "absent" ? 0 : parsedHandle.index;
|
|
6409
6306
|
void deps.mintLiveUrl(liveUrlRunId, handle).then(
|
|
6410
6307
|
(result) => {
|
|
6411
|
-
|
|
6308
|
+
json(res, result);
|
|
6412
6309
|
},
|
|
6413
6310
|
(error) => {
|
|
6414
6311
|
const name = error instanceof Error ? error.name : "";
|
|
6415
6312
|
if (name === "UnknownRunError") {
|
|
6416
|
-
|
|
6313
|
+
json(res, { error: "unknown_run" }, 404);
|
|
6417
6314
|
return;
|
|
6418
6315
|
}
|
|
6419
6316
|
if (name === "LiveViewUnavailableError") {
|
|
6420
|
-
|
|
6317
|
+
json(res, { error: "live_view_unavailable" }, 503);
|
|
6421
6318
|
return;
|
|
6422
6319
|
}
|
|
6423
|
-
|
|
6320
|
+
json(res, { error: "live_url_mint_failed" }, 500);
|
|
6424
6321
|
}
|
|
6425
6322
|
);
|
|
6426
6323
|
return;
|
|
@@ -6449,32 +6346,32 @@ function createRouter(dbOrDeps) {
|
|
|
6449
6346
|
}
|
|
6450
6347
|
if (req.method === "GET" && /^\/api\/runs\/[^/]+\/evidence$/.test(path)) {
|
|
6451
6348
|
if (!deps.agentHost) {
|
|
6452
|
-
|
|
6349
|
+
json(res, { error: "agent_host_unavailable" }, 503);
|
|
6453
6350
|
return;
|
|
6454
6351
|
}
|
|
6455
6352
|
let runId;
|
|
6456
6353
|
try {
|
|
6457
6354
|
runId = decodeURIComponent(path.slice("/api/runs/".length, -"/evidence".length));
|
|
6458
6355
|
} catch {
|
|
6459
|
-
|
|
6356
|
+
json(res, { error: "invalid_run_id" }, 400);
|
|
6460
6357
|
return;
|
|
6461
6358
|
}
|
|
6462
|
-
serveRunEvidenceRead(req, res, { runId, host: deps.agentHost
|
|
6359
|
+
serveRunEvidenceRead(req, res, { runId, host: deps.agentHost });
|
|
6463
6360
|
return;
|
|
6464
6361
|
}
|
|
6465
6362
|
if (req.method === "GET" && /^\/api\/runs\/[^/]+\/marks$/.test(path)) {
|
|
6466
6363
|
if (!deps.agentHost) {
|
|
6467
|
-
|
|
6364
|
+
json(res, { error: "agent_host_unavailable" }, 503);
|
|
6468
6365
|
return;
|
|
6469
6366
|
}
|
|
6470
6367
|
let runId;
|
|
6471
6368
|
try {
|
|
6472
6369
|
runId = decodeURIComponent(path.slice("/api/runs/".length, -"/marks".length));
|
|
6473
6370
|
} catch {
|
|
6474
|
-
|
|
6371
|
+
json(res, { error: "invalid_run_id" }, 400);
|
|
6475
6372
|
return;
|
|
6476
6373
|
}
|
|
6477
|
-
serveRunMarksHistoryRead(req, res, { runId, host: deps.agentHost
|
|
6374
|
+
serveRunMarksHistoryRead(req, res, { runId, host: deps.agentHost });
|
|
6478
6375
|
return;
|
|
6479
6376
|
}
|
|
6480
6377
|
if (req.method === "GET" && /^\/api\/runs\/[^/]+\/detail$/.test(path)) {
|
|
@@ -6489,7 +6386,7 @@ function createRouter(dbOrDeps) {
|
|
|
6489
6386
|
json(res, { error: "invalid_run_id" }, 400);
|
|
6490
6387
|
return;
|
|
6491
6388
|
}
|
|
6492
|
-
serveRunDetailRead(req, res, { runId: detailRunId, host: deps.agentHost
|
|
6389
|
+
serveRunDetailRead(req, res, { runId: detailRunId, host: deps.agentHost });
|
|
6493
6390
|
return;
|
|
6494
6391
|
}
|
|
6495
6392
|
if (req.method === "GET" && /^\/api\/runs\/[^/]+\/analysis$/.test(path)) {
|
|
@@ -6504,7 +6401,7 @@ function createRouter(dbOrDeps) {
|
|
|
6504
6401
|
json(res, { error: "invalid_run_id" }, 400);
|
|
6505
6402
|
return;
|
|
6506
6403
|
}
|
|
6507
|
-
serveRunAnalysisRead(req, res, { runId: analysisRunId, host: deps.agentHost
|
|
6404
|
+
serveRunAnalysisRead(req, res, { runId: analysisRunId, host: deps.agentHost });
|
|
6508
6405
|
return;
|
|
6509
6406
|
}
|
|
6510
6407
|
if (req.method === "GET" && /^\/api\/runs\/[^/]+\/knowledge$/.test(path)) {
|
|
@@ -6521,8 +6418,7 @@ function createRouter(dbOrDeps) {
|
|
|
6521
6418
|
}
|
|
6522
6419
|
serveRunKnowledgeRead(req, res, {
|
|
6523
6420
|
runId: knowledgeRunId,
|
|
6524
|
-
host: deps.agentHost
|
|
6525
|
-
boundary: "human"
|
|
6421
|
+
host: deps.agentHost
|
|
6526
6422
|
});
|
|
6527
6423
|
return;
|
|
6528
6424
|
}
|
|
@@ -6572,14 +6468,13 @@ function createRouter(dbOrDeps) {
|
|
|
6572
6468
|
notFound(res);
|
|
6573
6469
|
return;
|
|
6574
6470
|
}
|
|
6575
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
6576
6471
|
const id = segments[3];
|
|
6577
6472
|
const run = db.getRun(id);
|
|
6578
6473
|
if (!run) {
|
|
6579
6474
|
notFound(res, "Run not found");
|
|
6580
6475
|
return;
|
|
6581
6476
|
}
|
|
6582
|
-
|
|
6477
|
+
json(res, { run });
|
|
6583
6478
|
return;
|
|
6584
6479
|
}
|
|
6585
6480
|
if (path === "/api/stats/costs" && req.method === "GET") {
|
|
@@ -6590,19 +6485,18 @@ function createRouter(dbOrDeps) {
|
|
|
6590
6485
|
return;
|
|
6591
6486
|
}
|
|
6592
6487
|
if (path === "/api/insights/knowledge-changes" && req.method === "GET") {
|
|
6593
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
6594
6488
|
if (!deps.agentHost) {
|
|
6595
|
-
|
|
6489
|
+
json(res, { status: "unavailable", reason: "agent_host_unavailable" }, 503);
|
|
6596
6490
|
return;
|
|
6597
6491
|
}
|
|
6598
6492
|
const from = url.searchParams.get("from") ?? void 0;
|
|
6599
6493
|
const to = url.searchParams.get("to") ?? void 0;
|
|
6600
6494
|
void readInsightsKnowledgeChanges(deps.agentHost, db, { from, to }).then(
|
|
6601
6495
|
(body) => {
|
|
6602
|
-
|
|
6496
|
+
json(res, body);
|
|
6603
6497
|
},
|
|
6604
6498
|
() => {
|
|
6605
|
-
|
|
6499
|
+
json(res, { status: "unavailable", reason: "read_failed" }, 500);
|
|
6606
6500
|
}
|
|
6607
6501
|
);
|
|
6608
6502
|
return;
|
|
@@ -6668,10 +6562,7 @@ function createRouter(dbOrDeps) {
|
|
|
6668
6562
|
serveRunEventStream(req, res, {
|
|
6669
6563
|
runId,
|
|
6670
6564
|
handleParam: handleParamOf(req),
|
|
6671
|
-
//
|
|
6672
|
-
// it from. `url.searchParams.get('lastEventId')` would resolve a repeated parameter to its
|
|
6673
|
-
// first value — the one answer `lastEventIdParamOf` exists to refuse — and a second
|
|
6674
|
-
// header/query precedence rule here would be a second contract for where a client resumes.
|
|
6565
|
+
// Read from the same pair as the authed door; `searchParams.get` takes a repeated first.
|
|
6675
6566
|
resume: resumePointOf(req.headers["last-event-id"], lastEventIdParamOf(req)),
|
|
6676
6567
|
attach: deps.runServiceAttach
|
|
6677
6568
|
});
|
|
@@ -6713,7 +6604,6 @@ function createRouter(dbOrDeps) {
|
|
|
6713
6604
|
try {
|
|
6714
6605
|
attributes = buildRunRequestAttributes({
|
|
6715
6606
|
trigger: "api",
|
|
6716
|
-
runner: "local",
|
|
6717
6607
|
userAttributes: body.attributes
|
|
6718
6608
|
});
|
|
6719
6609
|
} catch (err) {
|
|
@@ -6721,25 +6611,10 @@ function createRouter(dbOrDeps) {
|
|
|
6721
6611
|
return;
|
|
6722
6612
|
}
|
|
6723
6613
|
const args = normalizedFileTarget ? [normalizedFileTarget.executionPath] : [];
|
|
6724
|
-
const source = isSuiteFile ? "suite" : "dashboard";
|
|
6725
|
-
try {
|
|
6726
|
-
await executionBackend?.preflight?.({ args, source });
|
|
6727
|
-
} catch (error) {
|
|
6728
|
-
const denial = runDenialOf(error);
|
|
6729
|
-
if (denial !== void 0) {
|
|
6730
|
-
json(res, denial, 403);
|
|
6731
|
-
return;
|
|
6732
|
-
}
|
|
6733
|
-
json(res, { error: error instanceof Error ? error.message : String(error) }, 400);
|
|
6734
|
-
return;
|
|
6735
|
-
}
|
|
6736
6614
|
const runId = jobQueue.enqueue({
|
|
6737
6615
|
name: body.name,
|
|
6738
6616
|
filePath: normalizedFileTarget?.storagePath,
|
|
6739
|
-
//
|
|
6740
|
-
// with member handles, so a parent artifact kind would claim parenthood over rows
|
|
6741
|
-
// nothing creates. `isSuiteFile` still travels in `metadata.isSuite` below, and the run's
|
|
6742
|
-
// own `suite_id` plus the artifact's `source.kind` snapshot still say it is a suite.
|
|
6617
|
+
// No parent kind is minted; `metadata.isSuite` and `suite_id` say it is a suite.
|
|
6743
6618
|
kind: "test",
|
|
6744
6619
|
attributes,
|
|
6745
6620
|
priority: body.priority,
|
|
@@ -6822,17 +6697,6 @@ function createRouter(dbOrDeps) {
|
|
|
6822
6697
|
args.push("--disable-cache");
|
|
6823
6698
|
}
|
|
6824
6699
|
const isSuite = Boolean(isSuiteFile);
|
|
6825
|
-
try {
|
|
6826
|
-
await executionBackend?.preflight?.({ args, source: isSuite ? "suite" : "dashboard" });
|
|
6827
|
-
} catch (error) {
|
|
6828
|
-
const denial = runDenialOf(error);
|
|
6829
|
-
if (denial !== void 0) {
|
|
6830
|
-
json(res, denial, 403);
|
|
6831
|
-
return;
|
|
6832
|
-
}
|
|
6833
|
-
json(res, { error: error instanceof Error ? error.message : String(error) }, 400);
|
|
6834
|
-
return;
|
|
6835
|
-
}
|
|
6836
6700
|
let testName = body.file ? (0, import_node_path8.basename)(normalizedFileTarget?.storagePath ?? body.file, ".yaml") : "unknown";
|
|
6837
6701
|
let fileParallel;
|
|
6838
6702
|
let platform;
|
|
@@ -6875,11 +6739,10 @@ function createRouter(dbOrDeps) {
|
|
|
6875
6739
|
const runId = jobQueue.enqueue({
|
|
6876
6740
|
name: testName,
|
|
6877
6741
|
filePath: normalizedFileTarget?.storagePath ?? body.file,
|
|
6878
|
-
//
|
|
6742
|
+
// No parent kind is minted; see the enqueue route above.
|
|
6879
6743
|
kind: "test",
|
|
6880
6744
|
attributes: buildRunRequestAttributes({
|
|
6881
|
-
trigger: triggerSource.trigger
|
|
6882
|
-
runner: body.local === false ? "browserstack" : "local"
|
|
6745
|
+
trigger: triggerSource.trigger
|
|
6883
6746
|
}),
|
|
6884
6747
|
platform,
|
|
6885
6748
|
parallel: effectiveParallel,
|
|
@@ -7432,9 +7295,8 @@ function createRouter(dbOrDeps) {
|
|
|
7432
7295
|
return;
|
|
7433
7296
|
}
|
|
7434
7297
|
if (path === "/api/config" && req.method === "GET") {
|
|
7435
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
7436
7298
|
if (!configManager) {
|
|
7437
|
-
|
|
7299
|
+
json(res, { error: "Config management not available" }, 501);
|
|
7438
7300
|
return;
|
|
7439
7301
|
}
|
|
7440
7302
|
;
|
|
@@ -7442,7 +7304,7 @@ function createRouter(dbOrDeps) {
|
|
|
7442
7304
|
try {
|
|
7443
7305
|
const read = await configManager.readMasked();
|
|
7444
7306
|
if (read.kind === "file_invalid") {
|
|
7445
|
-
|
|
7307
|
+
json(res, { error: "file_invalid", message: read.message }, 409);
|
|
7446
7308
|
return;
|
|
7447
7309
|
}
|
|
7448
7310
|
const { config, revision, lines, fileIssues } = read;
|
|
@@ -7452,7 +7314,7 @@ function createRouter(dbOrDeps) {
|
|
|
7452
7314
|
const activeLlm = use?.llm ?? config.defaultLLM;
|
|
7453
7315
|
const defaultCfg = Array.isArray(llms) ? llms.find((c) => c?.name === activeLlm) : void 0;
|
|
7454
7316
|
const provider = defaultCfg?.provider ?? null;
|
|
7455
|
-
|
|
7317
|
+
json(res, {
|
|
7456
7318
|
config,
|
|
7457
7319
|
provider,
|
|
7458
7320
|
revision,
|
|
@@ -7460,45 +7322,17 @@ function createRouter(dbOrDeps) {
|
|
|
7460
7322
|
...fileIssues.length > 0 ? { fileIssues } : {}
|
|
7461
7323
|
});
|
|
7462
7324
|
} catch (err) {
|
|
7463
|
-
|
|
7325
|
+
json(res, { error: err instanceof Error ? err.message : "Failed to read config" }, 500);
|
|
7464
7326
|
}
|
|
7465
7327
|
})();
|
|
7466
7328
|
return;
|
|
7467
7329
|
}
|
|
7468
|
-
if (path === "/api/config/farm/test-connection" && req.method === "POST") {
|
|
7469
|
-
readJsonBody(req).then(async (body) => {
|
|
7470
|
-
if (body.provider !== "browserstack") {
|
|
7471
|
-
json(res, { error: "Only BrowserStack is currently supported" }, 400);
|
|
7472
|
-
return;
|
|
7473
|
-
}
|
|
7474
|
-
try {
|
|
7475
|
-
const auth = Buffer.from(`${body.username}:${body.accessKey}`).toString("base64");
|
|
7476
|
-
const response = await fetch("https://api-cloud.browserstack.com/app-automate/devices.json", {
|
|
7477
|
-
headers: { Authorization: `Basic ${auth}` }
|
|
7478
|
-
});
|
|
7479
|
-
if (response.ok) {
|
|
7480
|
-
json(res, { success: true });
|
|
7481
|
-
} else if (response.status === 401) {
|
|
7482
|
-
json(res, { success: false, error: "Invalid credentials" });
|
|
7483
|
-
} else {
|
|
7484
|
-
json(res, { success: false, error: `BrowserStack API returned ${response.status}` });
|
|
7485
|
-
}
|
|
7486
|
-
} catch (err) {
|
|
7487
|
-
json(res, { success: false, error: err instanceof Error ? err.message : "Connection failed" });
|
|
7488
|
-
}
|
|
7489
|
-
}).catch(() => json(res, { error: "Invalid request body" }, 400));
|
|
7490
|
-
return;
|
|
7491
|
-
}
|
|
7492
7330
|
if (path.startsWith("/api/auth/") && req.method === "DELETE") {
|
|
7493
7331
|
const segments = path.split("/");
|
|
7494
7332
|
if (segments.length === 4 && segments[2] === "auth") {
|
|
7495
7333
|
const providerName = decodeURIComponent(segments[3]);
|
|
7496
7334
|
if (providerName === "status" || providerName === "credential" || providerName === "oauth") {
|
|
7497
7335
|
} else {
|
|
7498
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
7499
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
7500
|
-
return;
|
|
7501
|
-
}
|
|
7502
7336
|
;
|
|
7503
7337
|
(async () => {
|
|
7504
7338
|
try {
|
|
@@ -7509,13 +7343,13 @@ function createRouter(dbOrDeps) {
|
|
|
7509
7343
|
return credential;
|
|
7510
7344
|
});
|
|
7511
7345
|
if (removed === void 0) {
|
|
7512
|
-
|
|
7346
|
+
json(res, { deleted: false });
|
|
7513
7347
|
return;
|
|
7514
7348
|
}
|
|
7515
7349
|
const undo = undoStore.put({ kind: "credential", name: providerName, credential: removed }, Date.now());
|
|
7516
|
-
|
|
7350
|
+
json(res, { deleted: true, undo });
|
|
7517
7351
|
} catch (err) {
|
|
7518
|
-
|
|
7352
|
+
json(res, { error: err instanceof Error ? err.message : "Failed to delete credential" }, 500);
|
|
7519
7353
|
}
|
|
7520
7354
|
})();
|
|
7521
7355
|
return;
|
|
@@ -7625,27 +7459,21 @@ function createRouter(dbOrDeps) {
|
|
|
7625
7459
|
const segments = path.split("/");
|
|
7626
7460
|
const provider = segments.length >= 6 ? decodeURIComponent(segments[4] ?? "") : "";
|
|
7627
7461
|
const action = segments.length >= 6 ? segments[5] : "";
|
|
7628
|
-
const authDoor = req.method === "POST" && (action === "start" || action === "exchange");
|
|
7629
|
-
if (authDoor && refuseCrossSiteRequest(req) !== void 0) {
|
|
7630
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
7631
|
-
return;
|
|
7632
|
-
}
|
|
7633
7462
|
const plugin = provider ? (0, import_agent_qa_core9.getLLMAuthProviderPlugin)(provider) : void 0;
|
|
7634
7463
|
if (!plugin) {
|
|
7635
|
-
;
|
|
7636
|
-
(authDoor ? jsonNoCors : json)(res, { error: `Auth plugin provider "${provider}" is not registered` }, 404);
|
|
7464
|
+
json(res, { error: `Auth plugin provider "${provider}" is not registered` }, 404);
|
|
7637
7465
|
return;
|
|
7638
7466
|
}
|
|
7639
7467
|
if (action === "start" && req.method === "POST") {
|
|
7640
7468
|
readJsonBody(req).then(async (body) => {
|
|
7641
7469
|
if (!plugin.startAuth) {
|
|
7642
|
-
|
|
7470
|
+
json(res, { error: `Provider "${provider}" does not support dashboard auth start` }, 400);
|
|
7643
7471
|
return;
|
|
7644
7472
|
}
|
|
7645
7473
|
try {
|
|
7646
7474
|
const target = await requirePluginAuthConfig(configManager, body.configName, plugin);
|
|
7647
7475
|
if (!target.ok) {
|
|
7648
|
-
|
|
7476
|
+
json(res, credentialRefusalBody(target), target.status);
|
|
7649
7477
|
return;
|
|
7650
7478
|
}
|
|
7651
7479
|
const started = await plugin.startAuth({
|
|
@@ -7681,16 +7509,16 @@ function createRouter(dbOrDeps) {
|
|
|
7681
7509
|
session.error = err instanceof Error ? err.message : "Authentication failed";
|
|
7682
7510
|
}).finally(() => cleanupPluginOAuthSession(session));
|
|
7683
7511
|
}
|
|
7684
|
-
|
|
7512
|
+
json(res, {
|
|
7685
7513
|
authorizeUrl: started.authorizeUrl,
|
|
7686
7514
|
sessionId,
|
|
7687
7515
|
mode: plugin.dashboardAuth.mode,
|
|
7688
7516
|
created: target.created
|
|
7689
7517
|
});
|
|
7690
7518
|
} catch (err) {
|
|
7691
|
-
|
|
7519
|
+
json(res, { error: err instanceof Error ? err.message : "Failed to start auth plugin flow" }, 500);
|
|
7692
7520
|
}
|
|
7693
|
-
}).catch(() =>
|
|
7521
|
+
}).catch(() => json(res, { error: "Invalid request body" }, 400));
|
|
7694
7522
|
return;
|
|
7695
7523
|
}
|
|
7696
7524
|
if (action === "result" && req.method === "GET") {
|
|
@@ -7718,22 +7546,22 @@ function createRouter(dbOrDeps) {
|
|
|
7718
7546
|
if (action === "exchange" && req.method === "POST") {
|
|
7719
7547
|
readJsonBody(req).then(async (body) => {
|
|
7720
7548
|
if (!plugin.exchangeCode) {
|
|
7721
|
-
|
|
7549
|
+
json(res, { error: `Provider "${provider}" does not support code exchange` }, 400);
|
|
7722
7550
|
return;
|
|
7723
7551
|
}
|
|
7724
7552
|
const sessionId = typeof body.sessionId === "string" ? body.sessionId.trim() : "";
|
|
7725
7553
|
const code = typeof body.code === "string" ? body.code.trim() : "";
|
|
7726
7554
|
if (!sessionId || !code) {
|
|
7727
|
-
|
|
7555
|
+
json(res, { error: "sessionId and code are required" }, 400);
|
|
7728
7556
|
return;
|
|
7729
7557
|
}
|
|
7730
7558
|
const session = pluginOAuthSessions.get(sessionId);
|
|
7731
7559
|
if (!session || session.providerId !== provider) {
|
|
7732
|
-
|
|
7560
|
+
json(res, { error: "Auth session not found" }, 404);
|
|
7733
7561
|
return;
|
|
7734
7562
|
}
|
|
7735
7563
|
if (session.status !== "pending") {
|
|
7736
|
-
|
|
7564
|
+
json(res, { error: "Auth session is no longer pending" }, 409);
|
|
7737
7565
|
return;
|
|
7738
7566
|
}
|
|
7739
7567
|
try {
|
|
@@ -7755,61 +7583,57 @@ function createRouter(dbOrDeps) {
|
|
|
7755
7583
|
session.error = CREDENTIAL_PROVIDER_CONFLICT.message;
|
|
7756
7584
|
cleanupPluginOAuthSession(session);
|
|
7757
7585
|
pluginOAuthSessions.delete(sessionId);
|
|
7758
|
-
|
|
7586
|
+
json(res, credentialRefusalBody(CREDENTIAL_PROVIDER_CONFLICT), CREDENTIAL_PROVIDER_CONFLICT.status);
|
|
7759
7587
|
return;
|
|
7760
7588
|
}
|
|
7761
7589
|
session.status = "completed";
|
|
7762
7590
|
cleanupPluginOAuthSession(session);
|
|
7763
7591
|
pluginOAuthSessions.delete(sessionId);
|
|
7764
|
-
|
|
7592
|
+
json(res, { status: "completed", saved: true });
|
|
7765
7593
|
} catch (err) {
|
|
7766
7594
|
session.status = "error";
|
|
7767
7595
|
session.error = err instanceof Error ? err.message : "Token exchange failed";
|
|
7768
7596
|
cleanupPluginOAuthSession(session);
|
|
7769
7597
|
pluginOAuthSessions.delete(sessionId);
|
|
7770
|
-
|
|
7598
|
+
json(res, { error: session.error }, 500);
|
|
7771
7599
|
}
|
|
7772
|
-
}).catch(() =>
|
|
7600
|
+
}).catch(() => json(res, { error: "Invalid request body" }, 400));
|
|
7773
7601
|
return;
|
|
7774
7602
|
}
|
|
7775
7603
|
json(res, { error: "Auth plugin route not found" }, 404);
|
|
7776
7604
|
return;
|
|
7777
7605
|
}
|
|
7778
7606
|
if (path === "/api/auth/credential" && req.method === "POST") {
|
|
7779
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
7780
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
7781
|
-
return;
|
|
7782
|
-
}
|
|
7783
7607
|
readJsonBody(req).then(async (body) => {
|
|
7784
7608
|
const provider = typeof body.provider === "string" ? body.provider.trim() : "";
|
|
7785
7609
|
const credentialType = typeof body.type === "string" ? body.type.trim() : "";
|
|
7786
7610
|
const secret = typeof body.secret === "string" ? body.secret.trim() : "";
|
|
7787
7611
|
if (typeof body.configName !== "string" || body.configName.trim() === "") {
|
|
7788
|
-
|
|
7612
|
+
json(res, { error: "configName is required" }, 400);
|
|
7789
7613
|
return;
|
|
7790
7614
|
}
|
|
7791
7615
|
if (!provider || !isKnownLLMProvider(provider)) {
|
|
7792
|
-
|
|
7616
|
+
json(res, { error: "valid provider is required" }, 400);
|
|
7793
7617
|
return;
|
|
7794
7618
|
}
|
|
7795
7619
|
if (credentialType !== "api-key" && credentialType !== "bearer-token") {
|
|
7796
|
-
|
|
7620
|
+
json(res, { error: "type must be api-key or bearer-token" }, 400);
|
|
7797
7621
|
return;
|
|
7798
7622
|
}
|
|
7799
7623
|
if (!secret) {
|
|
7800
|
-
|
|
7624
|
+
json(res, { error: "secret is required" }, 400);
|
|
7801
7625
|
return;
|
|
7802
7626
|
}
|
|
7803
7627
|
if ((0, import_agent_qa_core9.getLLMAuthProviderPlugin)(provider)) {
|
|
7804
|
-
|
|
7628
|
+
json(res, { error: "Subscription providers use OAuth login" }, 400);
|
|
7805
7629
|
return;
|
|
7806
7630
|
}
|
|
7807
7631
|
if (credentialType === "bearer-token" && provider !== "anthropic-compatible") {
|
|
7808
|
-
|
|
7632
|
+
json(res, { error: "bearer-token credentials are only supported for anthropic-compatible configs" }, 400);
|
|
7809
7633
|
return;
|
|
7810
7634
|
}
|
|
7811
7635
|
if (credentialType === "api-key" && !API_KEY_CREDENTIAL_PROVIDERS.has(provider)) {
|
|
7812
|
-
|
|
7636
|
+
json(res, { error: "api-key credentials are not supported for this provider" }, 400);
|
|
7813
7637
|
return;
|
|
7814
7638
|
}
|
|
7815
7639
|
try {
|
|
@@ -7824,35 +7648,31 @@ function createRouter(dbOrDeps) {
|
|
|
7824
7648
|
return checked;
|
|
7825
7649
|
});
|
|
7826
7650
|
if (!target.ok) {
|
|
7827
|
-
|
|
7651
|
+
json(res, credentialRefusalBody(target), target.status);
|
|
7828
7652
|
return;
|
|
7829
7653
|
}
|
|
7830
|
-
|
|
7654
|
+
json(res, { saved: true, created: target.created });
|
|
7831
7655
|
} catch (err) {
|
|
7832
|
-
|
|
7656
|
+
json(res, { error: err instanceof Error ? err.message : "Failed to save credential" }, 500);
|
|
7833
7657
|
}
|
|
7834
|
-
}).catch(() =>
|
|
7658
|
+
}).catch(() => json(res, { error: "Invalid request body" }, 400));
|
|
7835
7659
|
return;
|
|
7836
7660
|
}
|
|
7837
7661
|
if (path === "/api/auth/credential/move" && req.method === "POST") {
|
|
7838
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
7839
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
7840
|
-
return;
|
|
7841
|
-
}
|
|
7842
7662
|
readJsonBody(req).then(async (raw) => {
|
|
7843
7663
|
const parsed = import_agent_qa_schema11.LlmCredentialMoveRequestSchema.safeParse(raw);
|
|
7844
7664
|
if (!parsed.success) {
|
|
7845
|
-
|
|
7665
|
+
json(res, { error: "invalid_setup_input" }, 400);
|
|
7846
7666
|
return;
|
|
7847
7667
|
}
|
|
7848
7668
|
const from = checkCredentialName(parsed.data.from);
|
|
7849
7669
|
const to = checkCredentialName(parsed.data.to);
|
|
7850
7670
|
if (!from.ok || !to.ok) {
|
|
7851
|
-
|
|
7671
|
+
json(res, { error: "name_invalid" }, 400);
|
|
7852
7672
|
return;
|
|
7853
7673
|
}
|
|
7854
7674
|
if (!configManager) {
|
|
7855
|
-
|
|
7675
|
+
json(res, { error: "Config manager is required to move credentials" }, 400);
|
|
7856
7676
|
return;
|
|
7857
7677
|
}
|
|
7858
7678
|
try {
|
|
@@ -7870,25 +7690,21 @@ function createRouter(dbOrDeps) {
|
|
|
7870
7690
|
return void 0;
|
|
7871
7691
|
});
|
|
7872
7692
|
if (refusal) {
|
|
7873
|
-
|
|
7693
|
+
json(res, credentialRefusalBody(refusal), refusal.status);
|
|
7874
7694
|
return;
|
|
7875
7695
|
}
|
|
7876
|
-
|
|
7696
|
+
json(res, { moved: true });
|
|
7877
7697
|
} catch (err) {
|
|
7878
|
-
|
|
7698
|
+
json(res, { error: err instanceof Error ? err.message : "Failed to move credential" }, 500);
|
|
7879
7699
|
}
|
|
7880
|
-
}).catch(() =>
|
|
7700
|
+
}).catch(() => json(res, { error: "invalid_setup_input" }, 400));
|
|
7881
7701
|
return;
|
|
7882
7702
|
}
|
|
7883
7703
|
if (path === "/api/llm/test" && req.method === "POST") {
|
|
7884
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
7885
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
7886
|
-
return;
|
|
7887
|
-
}
|
|
7888
7704
|
readJsonBody(req).then(async (body) => {
|
|
7889
7705
|
const rawBody = body;
|
|
7890
7706
|
if ("apiKey" in rawBody) {
|
|
7891
|
-
|
|
7707
|
+
json(res, {
|
|
7892
7708
|
success: false,
|
|
7893
7709
|
error: "invalid_request",
|
|
7894
7710
|
message: "apiKey is not accepted in LLM test requests. Save credentials for the named config instead."
|
|
@@ -7898,7 +7714,7 @@ function createRouter(dbOrDeps) {
|
|
|
7898
7714
|
const providerName = typeof body.provider === "string" ? body.provider.trim() : "";
|
|
7899
7715
|
const modelName = typeof body.model === "string" ? body.model.trim() : "";
|
|
7900
7716
|
if (!providerName || !modelName) {
|
|
7901
|
-
|
|
7717
|
+
json(res, { success: false, error: "invalid_request", message: "provider and model are required" }, 400);
|
|
7902
7718
|
return;
|
|
7903
7719
|
}
|
|
7904
7720
|
const configName = typeof body.configName === "string" ? body.configName.trim() : "";
|
|
@@ -7923,7 +7739,7 @@ function createRouter(dbOrDeps) {
|
|
|
7923
7739
|
}
|
|
7924
7740
|
const parsedConfig = import_agent_qa_core9.ModelConfigSchema.safeParse(llmCandidate);
|
|
7925
7741
|
if (!parsedConfig.success) {
|
|
7926
|
-
|
|
7742
|
+
json(res, {
|
|
7927
7743
|
success: false,
|
|
7928
7744
|
error: "invalid_request",
|
|
7929
7745
|
message: parsedConfig.error.issues.map((issue) => `${issue.path.map(String).join(".") || "config"}: ${issue.message}`).join("; ")
|
|
@@ -7935,7 +7751,7 @@ function createRouter(dbOrDeps) {
|
|
|
7935
7751
|
const coreAuth = await import("@vostride/agent-qa-core");
|
|
7936
7752
|
const resolvedAuth = await coreAuth.resolveLLMAuth(configName, llmConfig2);
|
|
7937
7753
|
if (resolvedAuth.kind === "missing") {
|
|
7938
|
-
|
|
7754
|
+
json(res, {
|
|
7939
7755
|
success: false,
|
|
7940
7756
|
error: "missing_credential",
|
|
7941
7757
|
message: resolvedAuth.message
|
|
@@ -7964,7 +7780,7 @@ function createRouter(dbOrDeps) {
|
|
|
7964
7780
|
} finally {
|
|
7965
7781
|
clearTimeout(timeout);
|
|
7966
7782
|
}
|
|
7967
|
-
|
|
7783
|
+
json(res, {
|
|
7968
7784
|
success: true,
|
|
7969
7785
|
model: modelName,
|
|
7970
7786
|
provider: providerName,
|
|
@@ -7979,7 +7795,7 @@ function createRouter(dbOrDeps) {
|
|
|
7979
7795
|
const providerMessage = withDeclaredMask(declaredMask, (mask) => mask(unsavedMessage));
|
|
7980
7796
|
const errorClass = (0, import_agent_qa_schema11.classifyProviderError)(providerMessage ?? unsavedMessage, statusCode);
|
|
7981
7797
|
const message = providerMessage === void 0 ? (0, import_agent_qa_schema11.providerErrorSentence)(errorClass, "").trimEnd() : (0, import_agent_qa_schema11.providerErrorSentence)(errorClass, providerMessage);
|
|
7982
|
-
|
|
7798
|
+
json(res, {
|
|
7983
7799
|
success: false,
|
|
7984
7800
|
error: errorClass,
|
|
7985
7801
|
message,
|
|
@@ -7989,7 +7805,7 @@ function createRouter(dbOrDeps) {
|
|
|
7989
7805
|
...unauthenticated ? { unauthenticated: true, authMessage } : {}
|
|
7990
7806
|
});
|
|
7991
7807
|
}
|
|
7992
|
-
}).catch(() =>
|
|
7808
|
+
}).catch(() => json(res, { success: false, error: "invalid_request", message: "Invalid request body" }, 400));
|
|
7993
7809
|
return;
|
|
7994
7810
|
}
|
|
7995
7811
|
if (path === "/api/cache/purge" && req.method === "POST") {
|
|
@@ -7998,56 +7814,15 @@ function createRouter(dbOrDeps) {
|
|
|
7998
7814
|
return;
|
|
7999
7815
|
}
|
|
8000
7816
|
readJsonBody(req).then(async (body) => {
|
|
8001
|
-
if (
|
|
8002
|
-
json(res, { error: "
|
|
7817
|
+
if (body.all !== true) {
|
|
7818
|
+
json(res, { error: "all is required" }, 400);
|
|
8003
7819
|
return;
|
|
8004
7820
|
}
|
|
8005
7821
|
try {
|
|
8006
7822
|
const cfg = await readConfigValues(configManager);
|
|
8007
|
-
const cacheDir = cfg.services?.cache?.dir ?? cfg.cache?.dir;
|
|
8008
|
-
if (!cacheDir) {
|
|
8009
|
-
json(res, { error: "Cache directory not configured" }, 400);
|
|
8010
|
-
return;
|
|
8011
|
-
}
|
|
8012
7823
|
const configDir = deps.configPath ? (0, import_node_path8.dirname)((0, import_node_path8.resolve)(deps.configPath)) : process.cwd();
|
|
8013
|
-
const
|
|
8014
|
-
|
|
8015
|
-
try {
|
|
8016
|
-
branchDirs = await (0, import_promises7.readdir)(resolvedCacheDir);
|
|
8017
|
-
} catch (err) {
|
|
8018
|
-
if (err.code === "ENOENT") {
|
|
8019
|
-
json(res, { purged: 0 });
|
|
8020
|
-
return;
|
|
8021
|
-
}
|
|
8022
|
-
throw err;
|
|
8023
|
-
}
|
|
8024
|
-
let purged = 0;
|
|
8025
|
-
for (const branch of branchDirs) {
|
|
8026
|
-
const branchPath = (0, import_node_path8.join)(resolvedCacheDir, branch);
|
|
8027
|
-
let entryStat;
|
|
8028
|
-
try {
|
|
8029
|
-
entryStat = await (0, import_promises7.stat)(branchPath);
|
|
8030
|
-
} catch {
|
|
8031
|
-
continue;
|
|
8032
|
-
}
|
|
8033
|
-
if (!entryStat.isDirectory()) {
|
|
8034
|
-
await (0, import_promises7.rm)(branchPath, { force: true });
|
|
8035
|
-
continue;
|
|
8036
|
-
}
|
|
8037
|
-
let files;
|
|
8038
|
-
try {
|
|
8039
|
-
files = await (0, import_promises7.readdir)(branchPath);
|
|
8040
|
-
} catch {
|
|
8041
|
-
continue;
|
|
8042
|
-
}
|
|
8043
|
-
for (const file of files) {
|
|
8044
|
-
if (!file.endsWith(".json")) continue;
|
|
8045
|
-
await (0, import_promises7.rm)((0, import_node_path8.join)(branchPath, file), { force: true });
|
|
8046
|
-
purged++;
|
|
8047
|
-
}
|
|
8048
|
-
await (0, import_promises7.rm)(branchPath, { recursive: true, force: true }).catch(() => {
|
|
8049
|
-
});
|
|
8050
|
-
}
|
|
7824
|
+
const cacheDir = cfg.services?.cache?.dir ?? import_agent_qa_core9.DEFAULT_AGENT_QA_CACHE_DIR;
|
|
7825
|
+
const purged = await (0, import_agent_qa_runner_local2.purgeActionCache)((0, import_node_path8.resolve)(configDir, cacheDir));
|
|
8051
7826
|
json(res, { purged });
|
|
8052
7827
|
} catch (err) {
|
|
8053
7828
|
json(res, { error: err instanceof Error ? err.message : "Cache purge failed" }, 500);
|
|
@@ -8131,7 +7906,7 @@ function createRouter(dbOrDeps) {
|
|
|
8131
7906
|
return;
|
|
8132
7907
|
}
|
|
8133
7908
|
const workspaceEnv = await readWorkspaceEnvVars(workspacePaths);
|
|
8134
|
-
const result = await new import_agent_qa_runner_local2.DockerHookExecutor().execute(resolvedHook, {
|
|
7909
|
+
const result = await new import_agent_qa_runner_local2.DockerHookExecutor({ version: (0, import_agent_qa_core9.getAgentQaVersion)() }).execute(resolvedHook, {
|
|
8135
7910
|
env: {
|
|
8136
7911
|
...workspaceEnv,
|
|
8137
7912
|
...validation.overrides
|
|
@@ -8167,7 +7942,7 @@ function createRouter(dbOrDeps) {
|
|
|
8167
7942
|
variables: shown.variables,
|
|
8168
7943
|
sandbox: {
|
|
8169
7944
|
runtime: resolvedHook.runtime,
|
|
8170
|
-
image: import_agent_qa_core9.
|
|
7945
|
+
image: (0, import_agent_qa_core9.hookRunnerImage)(resolvedHook.runtime, (0, import_agent_qa_core9.getAgentQaVersion)()),
|
|
8171
7946
|
networkMode: resolvedHook.network ? "enabled" : "disabled",
|
|
8172
7947
|
dockerVersion: null,
|
|
8173
7948
|
networkLogsAvailable: shown.networkLogs.length > 0,
|
|
@@ -8371,12 +8146,8 @@ function createRouter(dbOrDeps) {
|
|
|
8371
8146
|
return;
|
|
8372
8147
|
}
|
|
8373
8148
|
if (req.method === "GET" && /^\/api\/live-editor\/sessions\/[^/]+\/live-url$/.test(path)) {
|
|
8374
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
8375
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
8376
|
-
return;
|
|
8377
|
-
}
|
|
8378
8149
|
if (!deps.mintEditorLiveUrl) {
|
|
8379
|
-
|
|
8150
|
+
json(res, { error: "live_view_unavailable" }, 503);
|
|
8380
8151
|
return;
|
|
8381
8152
|
}
|
|
8382
8153
|
let editorSessionId;
|
|
@@ -8385,33 +8156,32 @@ function createRouter(dbOrDeps) {
|
|
|
8385
8156
|
path.slice("/api/live-editor/sessions/".length, -"/live-url".length)
|
|
8386
8157
|
);
|
|
8387
8158
|
} catch {
|
|
8388
|
-
|
|
8159
|
+
json(res, { error: "unknown_session" }, 404);
|
|
8389
8160
|
return;
|
|
8390
8161
|
}
|
|
8391
8162
|
void deps.mintEditorLiveUrl(editorSessionId).then(
|
|
8392
8163
|
(result) => {
|
|
8393
|
-
|
|
8164
|
+
json(res, result);
|
|
8394
8165
|
},
|
|
8395
8166
|
(error) => {
|
|
8396
8167
|
const name = error instanceof Error ? error.name : "";
|
|
8397
8168
|
if (name === "UnknownLiveEditorSessionError") {
|
|
8398
|
-
|
|
8169
|
+
json(res, { error: "unknown_session" }, 404);
|
|
8399
8170
|
return;
|
|
8400
8171
|
}
|
|
8401
8172
|
if (name === "LiveViewUnavailableError") {
|
|
8402
|
-
|
|
8173
|
+
json(res, { error: "live_view_unavailable" }, 503);
|
|
8403
8174
|
return;
|
|
8404
8175
|
}
|
|
8405
|
-
|
|
8176
|
+
json(res, { error: "live_url_mint_failed" }, 500);
|
|
8406
8177
|
}
|
|
8407
8178
|
);
|
|
8408
8179
|
return;
|
|
8409
8180
|
}
|
|
8410
8181
|
const liveEditorAuthStateMatch = path.match(/^\/api\/live-editor\/sessions\/([^/]+)\/auth-state$/);
|
|
8411
8182
|
if (liveEditorAuthStateMatch && req.method === "POST") {
|
|
8412
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
8413
8183
|
if (!sessionManager) {
|
|
8414
|
-
|
|
8184
|
+
json(res, { error: "Live editor not available" }, 503);
|
|
8415
8185
|
return;
|
|
8416
8186
|
}
|
|
8417
8187
|
;
|
|
@@ -8422,35 +8192,34 @@ function createRouter(dbOrDeps) {
|
|
|
8422
8192
|
const body = await readJsonBody(req);
|
|
8423
8193
|
const parsedName = import_agent_qa_core9.AuthStateNameSchema.safeParse(typeof body?.name === "string" ? body.name : "");
|
|
8424
8194
|
if (!parsedName.success) {
|
|
8425
|
-
|
|
8195
|
+
json(res, { error: "Auth state name must be a lowercase slug." }, 400);
|
|
8426
8196
|
return;
|
|
8427
8197
|
}
|
|
8428
8198
|
stateName = parsedName.data;
|
|
8429
8199
|
session = sessionManager.getSession(decodeURIComponent(liveEditorAuthStateMatch[1]));
|
|
8430
8200
|
if (!session) {
|
|
8431
|
-
|
|
8201
|
+
json(res, { error: "Session not found" }, 404);
|
|
8432
8202
|
return;
|
|
8433
8203
|
}
|
|
8434
8204
|
if (typeof session.captureWebAuthState !== "function") {
|
|
8435
|
-
|
|
8205
|
+
json(res, {
|
|
8436
8206
|
error: `Could not save auth state "${stateName}" for target "${getSessionTargetNameForAuthState(session)}".`
|
|
8437
8207
|
}, 500);
|
|
8438
8208
|
return;
|
|
8439
8209
|
}
|
|
8440
8210
|
const authState = await session.captureWebAuthState(stateName, { replace: body?.replace === true });
|
|
8441
|
-
|
|
8211
|
+
json(res, { authState });
|
|
8442
8212
|
} catch (err) {
|
|
8443
8213
|
const targetName = getSessionTargetNameForAuthState(session);
|
|
8444
8214
|
const response = buildAuthStateSaveErrorMessage(stateName || "unknown", targetName, err);
|
|
8445
|
-
|
|
8215
|
+
json(res, { error: response.message }, response.status);
|
|
8446
8216
|
}
|
|
8447
8217
|
})();
|
|
8448
8218
|
return;
|
|
8449
8219
|
}
|
|
8450
8220
|
if (path === "/api/live-editor/sessions" && req.method === "POST") {
|
|
8451
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
8452
8221
|
if (!sessionManager) {
|
|
8453
|
-
|
|
8222
|
+
json(res, { error: "Live editor not available" }, 503);
|
|
8454
8223
|
return;
|
|
8455
8224
|
}
|
|
8456
8225
|
;
|
|
@@ -8458,16 +8227,10 @@ function createRouter(dbOrDeps) {
|
|
|
8458
8227
|
try {
|
|
8459
8228
|
const body = await readJsonBody(req);
|
|
8460
8229
|
const targetName = typeof body?.targetName === "string" ? body.targetName : void 0;
|
|
8461
|
-
const production = await resolveLiveTargetProduction(configManager, targetName);
|
|
8462
|
-
const productionRefusal = liveProductionRefusal(production);
|
|
8463
|
-
if (productionRefusal !== void 0) {
|
|
8464
|
-
jsonNoCors(res, { error: productionRefusal.message }, 403);
|
|
8465
|
-
return;
|
|
8466
|
-
}
|
|
8467
8230
|
const platform = body?.platform ?? "web";
|
|
8468
8231
|
const runtimeLLM = await resolveDashboardRuntimeLLM(configManager, llmConfig, authFetch);
|
|
8469
8232
|
if (!runtimeLLM.ok) {
|
|
8470
|
-
|
|
8233
|
+
json(res, { error: runtimeLLM.error }, 400);
|
|
8471
8234
|
return;
|
|
8472
8235
|
}
|
|
8473
8236
|
const setupHooks = Array.isArray(body?.setupHooks) ? body.setupHooks.filter((value) => typeof value === "string" && value.trim().length > 0) : [];
|
|
@@ -8486,14 +8249,14 @@ function createRouter(dbOrDeps) {
|
|
|
8486
8249
|
let resolvedHooks = /* @__PURE__ */ new Map();
|
|
8487
8250
|
let hookRegistryError;
|
|
8488
8251
|
if (!workspacePaths) {
|
|
8489
|
-
|
|
8252
|
+
json(res, { error: "Workspace path resolution not available" }, 500);
|
|
8490
8253
|
return;
|
|
8491
8254
|
}
|
|
8492
8255
|
try {
|
|
8493
8256
|
const envContent = await (0, import_promises7.readFile)(workspacePaths.envFile.absolutePath, "utf-8");
|
|
8494
8257
|
envVars = (0, import_agent_qa_core9.parseEnvFile)(envContent);
|
|
8495
8258
|
} catch {
|
|
8496
|
-
|
|
8259
|
+
json(res, { error: `workspace.envFile file could not be loaded: ${workspacePaths.envFile.workspaceRelativePath}` }, 400);
|
|
8497
8260
|
return;
|
|
8498
8261
|
}
|
|
8499
8262
|
try {
|
|
@@ -8501,7 +8264,7 @@ function createRouter(dbOrDeps) {
|
|
|
8501
8264
|
secretStore = import_agent_qa_runner2.SecretStore.fromEnvContent(secretsContent);
|
|
8502
8265
|
secretRedactor = new import_agent_qa_runner2.SecretRedactor(secretStore);
|
|
8503
8266
|
} catch {
|
|
8504
|
-
|
|
8267
|
+
json(res, { error: `workspace.secretsFile file could not be loaded: ${workspacePaths.secretsFile.workspaceRelativePath}` }, 400);
|
|
8505
8268
|
return;
|
|
8506
8269
|
}
|
|
8507
8270
|
const hookCatalog = await readWorkspaceHooks(configManager, deps.configPath);
|
|
@@ -8512,6 +8275,11 @@ function createRouter(dbOrDeps) {
|
|
|
8512
8275
|
...suiteFileManager === void 0 ? {} : { findSuite: (id) => suiteFileManager.findBySuiteId(id) }
|
|
8513
8276
|
};
|
|
8514
8277
|
const plannerLimits = await resolveLivePlannerLimits(configManager, entityRef, entityReaders);
|
|
8278
|
+
const navigationTimeoutMs = await resolveLiveNavigationTimeoutMs(
|
|
8279
|
+
configManager,
|
|
8280
|
+
entityRef,
|
|
8281
|
+
entityReaders
|
|
8282
|
+
);
|
|
8515
8283
|
const sessionSuiteRef = entityRef?.type === "suite" ? entityRef : void 0;
|
|
8516
8284
|
const plannerLimitsForTest = (testId) => resolveLivePlannerLimits(
|
|
8517
8285
|
configManager,
|
|
@@ -8522,7 +8290,6 @@ function createRouter(dbOrDeps) {
|
|
|
8522
8290
|
{
|
|
8523
8291
|
platform,
|
|
8524
8292
|
targetName,
|
|
8525
|
-
production: { productionTargetNames: production.productionTargetNames },
|
|
8526
8293
|
plannerLimits,
|
|
8527
8294
|
plannerLimitsForTest,
|
|
8528
8295
|
llmConfig: runtimeLLM.llmConfig,
|
|
@@ -8536,6 +8303,7 @@ function createRouter(dbOrDeps) {
|
|
|
8536
8303
|
hookRegistryError,
|
|
8537
8304
|
url: body?.url,
|
|
8538
8305
|
headless: body?.headless ?? false,
|
|
8306
|
+
navigationTimeoutMs,
|
|
8539
8307
|
useDeviceName: typeof body?.useDeviceName === "string" ? body.useDeviceName : void 0,
|
|
8540
8308
|
appState: body?.appState === "preserve" || body?.appState === "reset" ? body.appState : void 0,
|
|
8541
8309
|
bundleId: typeof body?.bundleId === "string" ? body.bundleId : void 0,
|
|
@@ -8545,38 +8313,37 @@ function createRouter(dbOrDeps) {
|
|
|
8545
8313
|
},
|
|
8546
8314
|
entityRef
|
|
8547
8315
|
);
|
|
8548
|
-
|
|
8316
|
+
json(res, { sessionId, sessionNumber }, 201);
|
|
8549
8317
|
} catch (err) {
|
|
8550
8318
|
const message = err instanceof Error ? err.message : String(err);
|
|
8551
|
-
const
|
|
8552
|
-
|
|
8319
|
+
const refused = err instanceof LivePlannerLimitsRefusedError || err instanceof LiveNavigationTimeoutRefusedError;
|
|
8320
|
+
const status = refused ? 400 : 500;
|
|
8321
|
+
json(res, { error: message }, status);
|
|
8553
8322
|
}
|
|
8554
8323
|
})();
|
|
8555
8324
|
return;
|
|
8556
8325
|
}
|
|
8557
8326
|
if (path === "/api/live-editor/sessions" && req.method === "GET") {
|
|
8558
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
8559
8327
|
if (!sessionManager) {
|
|
8560
|
-
|
|
8328
|
+
json(res, { error: "Live editor not available" }, 503);
|
|
8561
8329
|
return;
|
|
8562
8330
|
}
|
|
8563
|
-
|
|
8331
|
+
json(res, { sessions: sessionManager.listSessions() });
|
|
8564
8332
|
return;
|
|
8565
8333
|
}
|
|
8566
8334
|
const liveEditorSessionMatch = path.match(/^\/api\/live-editor\/sessions\/([^/]+)$/);
|
|
8567
8335
|
if (liveEditorSessionMatch && req.method === "DELETE") {
|
|
8568
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
8569
8336
|
if (!sessionManager) {
|
|
8570
|
-
|
|
8337
|
+
json(res, { error: "Live editor not available" }, 503);
|
|
8571
8338
|
return;
|
|
8572
8339
|
}
|
|
8573
8340
|
const sessionId = liveEditorSessionMatch[1];
|
|
8574
8341
|
(async () => {
|
|
8575
8342
|
const terminated = await sessionManager.terminateSession(sessionId);
|
|
8576
8343
|
if (terminated) {
|
|
8577
|
-
|
|
8344
|
+
json(res, { ok: true });
|
|
8578
8345
|
} else {
|
|
8579
|
-
|
|
8346
|
+
json(res, { error: "Session not found" }, 404);
|
|
8580
8347
|
}
|
|
8581
8348
|
})();
|
|
8582
8349
|
return;
|
|
@@ -8625,6 +8392,7 @@ function createRouter(dbOrDeps) {
|
|
|
8625
8392
|
// src/queue/job-queue.ts
|
|
8626
8393
|
var import_node_events = require("node:events");
|
|
8627
8394
|
var import_node_os2 = require("node:os");
|
|
8395
|
+
var import_agent_qa_core10 = require("@vostride/agent-qa-core");
|
|
8628
8396
|
var NOT_CANCELLABLE = { kind: "not-cancellable" };
|
|
8629
8397
|
var JobQueue = class extends import_node_events.EventEmitter {
|
|
8630
8398
|
db;
|
|
@@ -8647,8 +8415,7 @@ var JobQueue = class extends import_node_events.EventEmitter {
|
|
|
8647
8415
|
this.pollInterval = setInterval(() => this.dispatchReported("a poll"), intervalMs);
|
|
8648
8416
|
this.on("enqueued", () => this.dispatchReported("an enqueue"));
|
|
8649
8417
|
}
|
|
8650
|
-
//
|
|
8651
|
-
// still frees its slot, and nothing reads the closed database.
|
|
8418
|
+
// The server closes the database after this; a run settling later still frees its slot.
|
|
8652
8419
|
stop() {
|
|
8653
8420
|
this.stopped = true;
|
|
8654
8421
|
if (this.pollInterval) {
|
|
@@ -8734,7 +8501,10 @@ var JobQueue = class extends import_node_events.EventEmitter {
|
|
|
8734
8501
|
this.processNext();
|
|
8735
8502
|
} catch (error) {
|
|
8736
8503
|
const message = error instanceof Error ? error.message : String(error);
|
|
8737
|
-
|
|
8504
|
+
(0, import_agent_qa_core10.printRunText)(
|
|
8505
|
+
`Queue could not dispatch the next run after ${trigger}: ${message}`,
|
|
8506
|
+
(text) => console.error(text)
|
|
8507
|
+
);
|
|
8738
8508
|
}
|
|
8739
8509
|
}
|
|
8740
8510
|
getActiveCount() {
|
|
@@ -8743,9 +8513,7 @@ var JobQueue = class extends import_node_events.EventEmitter {
|
|
|
8743
8513
|
getConcurrency() {
|
|
8744
8514
|
return this.concurrency;
|
|
8745
8515
|
}
|
|
8746
|
-
//
|
|
8747
|
-
// 442-15 (GAP-D2): own is the stamp this handle wrote, or equal non-null start times.
|
|
8748
|
-
// 2026-10-17 (444-06, D-11): a running row is never written here; the run answers with its verdict.
|
|
8516
|
+
// Only the owner stops its running row, never written here; a gone owner's row is an orphan.
|
|
8749
8517
|
cancel(runId) {
|
|
8750
8518
|
const run = this.db.getRun(runId);
|
|
8751
8519
|
if (!run) return NOT_CANCELLABLE;
|
|
@@ -8775,21 +8543,7 @@ var JobQueue = class extends import_node_events.EventEmitter {
|
|
|
8775
8543
|
const run = this.db.getRun(runId);
|
|
8776
8544
|
return run && run.status !== "running" && run.status !== "pending" ? { kind: "finished", status: run.status } : NOT_CANCELLABLE;
|
|
8777
8545
|
}
|
|
8778
|
-
//
|
|
8779
|
-
// MODEL THAT MINTED ITS CHILDREN.
|
|
8780
|
-
//
|
|
8781
|
-
// ~19 lines stood here walking the run's child rows and cancelling each member row. Under
|
|
8782
|
-
// the one-run model a parallel suite is ONE run row with member handles (402-04) — nothing writes
|
|
8783
|
-
// the run row's parent-link column any more (405-07 then deleted the column outright), so the
|
|
8784
|
-
// walk could only ever have read an empty list while reading as live cancellation policy.
|
|
8785
|
-
// Cancelling the run cancels the run.
|
|
8786
|
-
//
|
|
8787
|
-
// WHAT THIS DOES NOT TOUCH: `parallel` below. D-11 keeps the queue's parallel flag as CROSS-RUN
|
|
8788
|
-
// concurrency of INDEPENDENT jobs, which is what `findNextEligible` has always used it for. What
|
|
8789
|
-
// was deleted is parallel-SUITE semantics, not the queue's ability to run two unrelated jobs.
|
|
8790
|
-
//
|
|
8791
|
-
// HISTORICAL ROWS: a pre-402 suite run still lists its members on the read side; cancelling
|
|
8792
|
-
// such a run now leaves those member rows in whatever state their own process left them.
|
|
8546
|
+
// `parallel` is cross-run concurrency of independent jobs; a suite is one run row.
|
|
8793
8547
|
findNextEligible() {
|
|
8794
8548
|
if (this.activeCount >= this.concurrency) return void 0;
|
|
8795
8549
|
const pending = this.db.getPendingRuns();
|
|
@@ -8816,17 +8570,17 @@ var import_promises8 = require("node:fs/promises");
|
|
|
8816
8570
|
var import_node_path9 = require("node:path");
|
|
8817
8571
|
var import_agent_qa_ids3 = require("@vostride/agent-qa-ids");
|
|
8818
8572
|
var import_yaml8 = require("yaml");
|
|
8819
|
-
var
|
|
8573
|
+
var import_agent_qa_core11 = require("@vostride/agent-qa-core");
|
|
8820
8574
|
var SuiteFileManager = class {
|
|
8821
8575
|
constructor(workspace, testFileManager) {
|
|
8822
8576
|
this.workspace = workspace;
|
|
8823
8577
|
this.testFileManager = testFileManager;
|
|
8824
8578
|
}
|
|
8825
8579
|
testFileManager;
|
|
8826
|
-
//
|
|
8580
|
+
// Each list call reads the workspace itself; each file is written whole, one writer at a time.
|
|
8827
8581
|
underLock = createPathLock();
|
|
8828
8582
|
async list() {
|
|
8829
|
-
const records = await (0,
|
|
8583
|
+
const records = await (0, import_agent_qa_core11.discoverWorkspaceFiles)({ workspace: this.workspace, kind: "suite" });
|
|
8830
8584
|
const files = [];
|
|
8831
8585
|
for (const record of records) {
|
|
8832
8586
|
try {
|
|
@@ -8888,7 +8642,7 @@ var SuiteFileManager = class {
|
|
|
8888
8642
|
if (!trimmedPath) {
|
|
8889
8643
|
throw new Error("Suite path is required");
|
|
8890
8644
|
}
|
|
8891
|
-
const target = await (0,
|
|
8645
|
+
const target = await (0, import_agent_qa_core11.resolveWorkspaceFileTarget)({
|
|
8892
8646
|
workspace: this.workspace,
|
|
8893
8647
|
kind: "suite",
|
|
8894
8648
|
filePath: trimmedPath,
|
|
@@ -8912,7 +8666,7 @@ var SuiteFileManager = class {
|
|
|
8912
8666
|
async validate(content) {
|
|
8913
8667
|
try {
|
|
8914
8668
|
const parsed = (0, import_yaml8.parse)(content);
|
|
8915
|
-
const result =
|
|
8669
|
+
const result = import_agent_qa_core11.SuiteDefinitionSchema.safeParse(parsed);
|
|
8916
8670
|
if (!result.success) {
|
|
8917
8671
|
return {
|
|
8918
8672
|
valid: false,
|
|
@@ -8950,7 +8704,7 @@ var SuiteFileManager = class {
|
|
|
8950
8704
|
}
|
|
8951
8705
|
}
|
|
8952
8706
|
async resolveTarget(relativePath, requireExisting) {
|
|
8953
|
-
return (0,
|
|
8707
|
+
return (0, import_agent_qa_core11.resolveWorkspaceFileTarget)({
|
|
8954
8708
|
workspace: this.workspace,
|
|
8955
8709
|
kind: "suite",
|
|
8956
8710
|
filePath: relativePath,
|
|
@@ -8962,7 +8716,7 @@ var SuiteFileManager = class {
|
|
|
8962
8716
|
// src/execution/appium-manager.ts
|
|
8963
8717
|
var import_node_http = require("node:http");
|
|
8964
8718
|
var import_node_child_process2 = require("node:child_process");
|
|
8965
|
-
var
|
|
8719
|
+
var import_agent_qa_core12 = require("@vostride/agent-qa-core");
|
|
8966
8720
|
var import_tree_kill = __toESM(require("tree-kill"), 1);
|
|
8967
8721
|
var import_picocolors = __toESM(require("picocolors"), 1);
|
|
8968
8722
|
var APPIUM_PREFIX = import_picocolors.default.magenta("[appium]");
|
|
@@ -8983,7 +8737,7 @@ var AppiumManager = class {
|
|
|
8983
8737
|
this.startupTimeoutMs = opts?.startupTimeoutMs ?? 3e4;
|
|
8984
8738
|
this.pollIntervalMs = opts?.pollIntervalMs ?? 500;
|
|
8985
8739
|
this.logLevel = opts?.logLevel ?? "normal";
|
|
8986
|
-
this.appiumResolver = opts?.appiumResolver ??
|
|
8740
|
+
this.appiumResolver = opts?.appiumResolver ?? import_agent_qa_core12.resolveAppiumExecutable;
|
|
8987
8741
|
}
|
|
8988
8742
|
async acquire() {
|
|
8989
8743
|
await this.ensureRunning();
|
|
@@ -9086,23 +8840,23 @@ var AppiumManager = class {
|
|
|
9086
8840
|
this.managed = true;
|
|
9087
8841
|
child.on("error", (err) => {
|
|
9088
8842
|
if (err.code === "ENOENT") {
|
|
9089
|
-
console.error(`${APPIUM_PREFIX} Appium not found. ${(0,
|
|
8843
|
+
console.error(`${APPIUM_PREFIX} Appium not found. ${(0, import_agent_qa_core12.formatAppiumInstallGuidance)()}`);
|
|
9090
8844
|
} else {
|
|
9091
|
-
|
|
8845
|
+
(0, import_agent_qa_core12.printRunText)(`${APPIUM_PREFIX} Error: ${err.message}`, (text) => console.error(text));
|
|
9092
8846
|
}
|
|
9093
8847
|
});
|
|
9094
8848
|
child.stdout?.on("data", (chunk) => {
|
|
9095
8849
|
if (this.logLevel !== "debug") return;
|
|
9096
8850
|
const lines = chunk.toString().split("\n").filter(Boolean);
|
|
9097
8851
|
for (const line of lines) {
|
|
9098
|
-
|
|
8852
|
+
(0, import_agent_qa_core12.printRunText)(`${APPIUM_PREFIX} ${import_picocolors.default.dim(line)}`, (text) => console.log(text));
|
|
9099
8853
|
}
|
|
9100
8854
|
});
|
|
9101
8855
|
child.stderr?.on("data", (chunk) => {
|
|
9102
8856
|
if (this.logLevel !== "debug") return;
|
|
9103
8857
|
const lines = chunk.toString().split("\n").filter(Boolean);
|
|
9104
8858
|
for (const line of lines) {
|
|
9105
|
-
|
|
8859
|
+
(0, import_agent_qa_core12.printRunText)(`${APPIUM_PREFIX} ${import_picocolors.default.dim(line)}`, (text) => console.error(text));
|
|
9106
8860
|
}
|
|
9107
8861
|
});
|
|
9108
8862
|
const start = Date.now();
|
|
@@ -9184,10 +8938,7 @@ var SessionManager = class {
|
|
|
9184
8938
|
this.deps = deps;
|
|
9185
8939
|
}
|
|
9186
8940
|
sessions = /* @__PURE__ */ new Map();
|
|
9187
|
-
//
|
|
9188
|
-
// Monotonic and ephemeral: resets on server restart per CONTEXT D-30.
|
|
9189
|
-
// Never decremented on terminate — a re-open of a terminated entity yields
|
|
9190
|
-
// the next number so "Session #3" after "#2" ends does not confuse users.
|
|
8941
|
+
// Monotonic per entity and reset on restart, so a reopened entity gets the next number.
|
|
9191
8942
|
counters = /* @__PURE__ */ new Map();
|
|
9192
8943
|
async createSession(config, entity) {
|
|
9193
8944
|
if (!config.llmConfig) {
|
|
@@ -9244,7 +8995,8 @@ var SessionManager = class {
|
|
|
9244
8995
|
};
|
|
9245
8996
|
|
|
9246
8997
|
// src/live-editor/ws-handler.ts
|
|
9247
|
-
var
|
|
8998
|
+
var import_node_util4 = require("node:util");
|
|
8999
|
+
var import_agent_qa_core13 = require("@vostride/agent-qa-core");
|
|
9248
9000
|
|
|
9249
9001
|
// src/live-editor/types.ts
|
|
9250
9002
|
var LIVE_EDITOR_UNKNOWN_SESSION_CLOSE_CODE = 4404;
|
|
@@ -9431,12 +9183,14 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
|
|
|
9431
9183
|
try {
|
|
9432
9184
|
raw = await testFileManager.read(msg.path);
|
|
9433
9185
|
} catch (readErr) {
|
|
9434
|
-
|
|
9186
|
+
const line = `[ws-handler] execute-test: read failed for path=${msg.path}`;
|
|
9187
|
+
(0, import_agent_qa_core13.printRunText)((0, import_node_util4.format)(line, readErr), () => console.error(line, readErr));
|
|
9435
9188
|
throw new Error(`Cannot read test file "${msg.path}": ${readErr?.message ?? String(readErr)}`);
|
|
9436
9189
|
}
|
|
9437
|
-
const parsed = (0,
|
|
9190
|
+
const parsed = (0, import_agent_qa_core13.parseTestFile)(raw, msg.path);
|
|
9438
9191
|
if (parsed.errors.length > 0) {
|
|
9439
|
-
|
|
9192
|
+
const line = `[ws-handler] execute-test: parse errors for ${msg.path}`;
|
|
9193
|
+
(0, import_agent_qa_core13.printRunText)((0, import_node_util4.format)(line, parsed.errors), () => console.error(line, parsed.errors));
|
|
9440
9194
|
throw new Error(`Parse error in ${msg.path}: ${parsed.errors[0].message}`);
|
|
9441
9195
|
}
|
|
9442
9196
|
const testDef = parsed.tests[0];
|
|
@@ -9446,7 +9200,13 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
|
|
|
9446
9200
|
const stepInstructions = testDef.steps.map(
|
|
9447
9201
|
(s) => typeof s === "string" ? s : s.step
|
|
9448
9202
|
);
|
|
9449
|
-
|
|
9203
|
+
const stepCount = stepInstructions.length;
|
|
9204
|
+
const setupCount = (testDef.setup ?? []).length;
|
|
9205
|
+
const teardownCount = (testDef.teardown ?? []).length;
|
|
9206
|
+
(0, import_agent_qa_core13.printRunText)(
|
|
9207
|
+
`[ws-handler] execute-test: starting "${testDef.name}" (${stepCount} steps, setup=${setupCount}, teardown=${teardownCount})`,
|
|
9208
|
+
(text) => console.log(text)
|
|
9209
|
+
);
|
|
9450
9210
|
reply({
|
|
9451
9211
|
type: "test-start",
|
|
9452
9212
|
test: {
|
|
@@ -9468,7 +9228,11 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
|
|
|
9468
9228
|
teardown: testDef.teardown ?? []
|
|
9469
9229
|
}
|
|
9470
9230
|
);
|
|
9471
|
-
|
|
9231
|
+
const errorPart = result.error ? ` error=${result.error}` : "";
|
|
9232
|
+
(0, import_agent_qa_core13.printRunText)(
|
|
9233
|
+
`[ws-handler] execute-test: finished "${testDef.name}" status=${result.status} duration=${result.duration}ms${errorPart}`,
|
|
9234
|
+
(text) => console.log(text)
|
|
9235
|
+
);
|
|
9472
9236
|
reply({
|
|
9473
9237
|
type: "test-complete",
|
|
9474
9238
|
test: {
|
|
@@ -9481,11 +9245,9 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
|
|
|
9481
9245
|
});
|
|
9482
9246
|
reply({ type: "session-state", state: session.getState() });
|
|
9483
9247
|
} catch (err) {
|
|
9484
|
-
|
|
9485
|
-
|
|
9486
|
-
|
|
9487
|
-
code: err?.code
|
|
9488
|
-
});
|
|
9248
|
+
const line = `[ws-handler] execute-test: caught error for path=${msg.path}`;
|
|
9249
|
+
const detail = { message: err?.message, stack: err?.stack, code: err?.code };
|
|
9250
|
+
(0, import_agent_qa_core13.printRunText)((0, import_node_util4.format)(line, detail), () => console.error(line, detail));
|
|
9489
9251
|
reply({
|
|
9490
9252
|
type: "test-error",
|
|
9491
9253
|
test: {
|
|
@@ -9742,8 +9504,7 @@ function renderLiveEmbedPage(input) {
|
|
|
9742
9504
|
if (pending === null) return;
|
|
9743
9505
|
var header = pending;
|
|
9744
9506
|
pending = null;
|
|
9745
|
-
//
|
|
9746
|
-
// both paint in every host, not only in Chromium (428 RESEARCH A2).
|
|
9507
|
+
// No MIME type: the decoder sniffs, so JPEG and PNG frames paint in every host.
|
|
9747
9508
|
var blob = new Blob([event.data]);
|
|
9748
9509
|
createImageBitmap(blob).then(function (bitmap) {
|
|
9749
9510
|
if (canvas.width !== header.width || canvas.height !== header.height) {
|
|
@@ -9767,8 +9528,7 @@ function renderLiveEmbedPage(input) {
|
|
|
9767
9528
|
try { msg = JSON.parse(event.data); } catch (e) { return; }
|
|
9768
9529
|
|
|
9769
9530
|
if (msg.type === 'ready') {
|
|
9770
|
-
//
|
|
9771
|
-
// a transport guess (LIVE-04's prohibition). A snapshot paints on the same canvas (428 D-11).
|
|
9531
|
+
// The declared kind picks the painter, never a browser name, adapter or transport guess.
|
|
9772
9532
|
var kind = msg.stream ? msg.stream.kind : undefined;
|
|
9773
9533
|
if (kind === 'cdp-screencast' || kind === 'snapshot') {
|
|
9774
9534
|
setState('connecting', 'waiting for the first frame\u2026');
|
|
@@ -9815,8 +9575,7 @@ function renderLiveEmbedPage(input) {
|
|
|
9815
9575
|
report({ event: 'lost' });
|
|
9816
9576
|
});
|
|
9817
9577
|
|
|
9818
|
-
// A
|
|
9819
|
-
// so a renewal never costs the viewer the stream it is watching (D-02).
|
|
9578
|
+
// A re-minting host replaces the fragment; the socket renews in place and the stream continues.
|
|
9820
9579
|
window.addEventListener('hashchange', function () {
|
|
9821
9580
|
var next = location.hash.slice(1);
|
|
9822
9581
|
if (next === '' || socket.readyState !== 1) return;
|
|
@@ -10025,7 +9784,7 @@ function createFrameProxy(deps) {
|
|
|
10025
9784
|
}
|
|
10026
9785
|
|
|
10027
9786
|
// src/server/server.ts
|
|
10028
|
-
var
|
|
9787
|
+
var import_agent_qa_core14 = require("@vostride/agent-qa-core");
|
|
10029
9788
|
var MIME_TYPES = {
|
|
10030
9789
|
".html": "text/html; charset=utf-8",
|
|
10031
9790
|
".js": "application/javascript",
|
|
@@ -10044,9 +9803,10 @@ var MIME_TYPES = {
|
|
|
10044
9803
|
".webp": "image/webp"
|
|
10045
9804
|
};
|
|
10046
9805
|
var DashboardRemoteBindRefusedError = class extends Error {
|
|
10047
|
-
constructor(host) {
|
|
9806
|
+
constructor(host, port) {
|
|
9807
|
+
const p = String(port === 0 ? 3470 : port);
|
|
10048
9808
|
super(
|
|
10049
|
-
`The dashboard was asked to bind ${host}, which is not a loopback interface. Nothing this server serves \u2014 the run trigger, the queue enqueue, the run service, the artifact serving, the config reads \u2014 asks for a credential, so a non-loopback bind would publish an anonymous run trigger to the network. Leave services.dashboard.host unset (it defaults to ${DEFAULT_DASHBOARD_HOST})
|
|
9809
|
+
`The dashboard was asked to bind ${host}, which is not a loopback interface. Nothing this server serves \u2014 the run trigger, the queue enqueue, the run service, the artifact serving, the config reads \u2014 asks for a credential, so a non-loopback bind would publish an anonymous run trigger to the network. Leave services.dashboard.host unset (it defaults to ${DEFAULT_DASHBOARD_HOST}). To use the dashboard from another machine, forward its port with ssh -L ${p}:localhost:${p} <host> and open http://localhost:${p}/.`
|
|
10050
9810
|
);
|
|
10051
9811
|
this.host = host;
|
|
10052
9812
|
this.name = "DashboardRemoteBindRefusedError";
|
|
@@ -10057,8 +9817,8 @@ async function startServer(opts) {
|
|
|
10057
9817
|
const { db, port = 3470, uiDir, workspacePaths, configPath, llmConfig, authFetch, agentHost, executionBackend, runService, runServiceReadiness, liveView, liveEditorSubjects, postProcessingRelay } = opts;
|
|
10058
9818
|
const runServiceAttach = opts.runServiceAttach ?? runService?.attach;
|
|
10059
9819
|
const host = opts.host?.trim() || DEFAULT_DASHBOARD_HOST;
|
|
10060
|
-
if (!(0,
|
|
10061
|
-
throw new DashboardRemoteBindRefusedError(host);
|
|
9820
|
+
if (!(0, import_agent_qa_core14.isLoopbackHost)(host)) {
|
|
9821
|
+
throw new DashboardRemoteBindRefusedError(host, port);
|
|
10062
9822
|
}
|
|
10063
9823
|
const resolvedConfigPath = configPath ?? (0, import_node_path10.resolve)("agent-qa.config.yaml");
|
|
10064
9824
|
const configManager = new ConfigManager(resolvedConfigPath, opts.declaredSecretMask);
|
|
@@ -10093,7 +9853,7 @@ async function startServer(opts) {
|
|
|
10093
9853
|
failureSummary = "Test timed out -- process was killed";
|
|
10094
9854
|
} else if (details.refusedBeforeStart) {
|
|
10095
9855
|
dbStatus = "failed";
|
|
10096
|
-
refusalProse = details.
|
|
9856
|
+
refusalProse = details.reason;
|
|
10097
9857
|
failureSummary = refusalProse ? `Refused before the run started: ${refusalProse}`.slice(0, 300) : "Refused before the run started";
|
|
10098
9858
|
} else if (status === "completed") {
|
|
10099
9859
|
dbStatus = "failed";
|
|
@@ -10174,7 +9934,10 @@ async function startServer(opts) {
|
|
|
10174
9934
|
mobileRuns.set(run.id, platform);
|
|
10175
9935
|
} catch (err) {
|
|
10176
9936
|
const message = err instanceof Error ? err.message : String(err);
|
|
10177
|
-
|
|
9937
|
+
(0, import_agent_qa_core14.printRunText)(
|
|
9938
|
+
`Failed to start Appium for run ${run.id}: ${message}`,
|
|
9939
|
+
(text) => console.error(text)
|
|
9940
|
+
);
|
|
10178
9941
|
db.updateRun(run.id, {
|
|
10179
9942
|
status: "failed",
|
|
10180
9943
|
failureSummary: `Appium server failed to start: ${message}`,
|
|
@@ -10194,19 +9957,9 @@ async function startServer(opts) {
|
|
|
10194
9957
|
runId: run.id,
|
|
10195
9958
|
args,
|
|
10196
9959
|
source: meta.isSuite ? "suite" : "dashboard",
|
|
10197
|
-
// 2026-10-01 (432-14, D-14): no confirm scope is read off the row any more. The browser that
|
|
10198
|
-
// triggered a run is not asked anything, so nothing about it has to survive the wait.
|
|
10199
9960
|
attributes: run.attributes,
|
|
10200
|
-
//
|
|
10201
|
-
// `in-process-backend.ts:211` reads `opts.timeout ?? defaultTimeout` and validates AFTER, so
|
|
10202
|
-
// any non-nullish nonsense here skips the default and leaves the run with no reclamation
|
|
10203
|
-
// timer at all. An unusable value falls back to the backend's default rather than a number
|
|
10204
|
-
// this call invented.
|
|
9961
|
+
// The backend takes `opts.timeout ?? defaultTimeout`, so an unusable value falls back here.
|
|
10205
9962
|
timeout: sanitizeQueuedExecutionTimeout(meta.timeout),
|
|
10206
|
-
// THE LEASE ABOVE IS UNCHANGED; ONLY ITS DELIVERY IS. `acquireLease` still runs where it ran,
|
|
10207
|
-
// still keys on this run id and still owns the bundled Appium's lifecycle (design-of-record).
|
|
10208
|
-
// What used to ride `AGENT_QA_APPIUM_URL` in a child's environment now travels as an
|
|
10209
|
-
// argument, because there is no child to carry an environment.
|
|
10210
9963
|
...isMobile ? { appiumUrl: appiumManager.getUrl() } : {}
|
|
10211
9964
|
});
|
|
10212
9965
|
});
|
|
@@ -10217,16 +9970,14 @@ async function startServer(opts) {
|
|
|
10217
9970
|
appiumManager,
|
|
10218
9971
|
configManager,
|
|
10219
9972
|
configPath: resolvedConfigPath,
|
|
10220
|
-
//
|
|
9973
|
+
// Each created session registers here, which is what the frame socket attaches.
|
|
10221
9974
|
...liveEditorSubjects === void 0 ? {} : { liveEditorSubjects }
|
|
10222
9975
|
});
|
|
10223
9976
|
const servesLiveView = liveView !== void 0 && (runServiceAttach !== void 0 || liveEditorSubjects !== void 0);
|
|
10224
9977
|
const apiRouter = createRouter({
|
|
10225
9978
|
db,
|
|
10226
9979
|
workspacePaths,
|
|
10227
|
-
//
|
|
10228
|
-
// dependency is an ABSENT field, never a present `undefined`, so the router's own 503 arm is
|
|
10229
|
-
// what decides, in one place.
|
|
9980
|
+
// Absent dependencies are absent fields, so the router's own 503 arm decides in one place.
|
|
10230
9981
|
...executionBackend === void 0 ? {} : { executionBackend },
|
|
10231
9982
|
jobQueue,
|
|
10232
9983
|
testFileManager,
|
|
@@ -10237,27 +9988,13 @@ async function startServer(opts) {
|
|
|
10237
9988
|
llmConfig,
|
|
10238
9989
|
authFetch,
|
|
10239
9990
|
sessionManager,
|
|
10240
|
-
//
|
|
10241
|
-
// never a present `undefined` — the router's own 501 arm is what decides, in one place.
|
|
9991
|
+
// Absent dependencies are absent fields, so the router's own 501 arm decides in one place.
|
|
10242
9992
|
...agentHost === void 0 ? {} : { agentHost },
|
|
10243
|
-
// 402-05 (D-15): the same conditional-spread house rule. Absent means a legacy mount rather
|
|
10244
|
-
// than an unconfigured service, and the router's own 503 arm is what decides, in one place.
|
|
10245
9993
|
...runServiceReadiness === void 0 ? {} : { runServiceReadiness },
|
|
10246
|
-
//
|
|
10247
|
-
// composed again. That derivation is the "one session registry serves both doors" claim in its
|
|
10248
|
-
// structural form — there is no second wiring a composition root could forget or point
|
|
10249
|
-
// somewhere else, and the two doors cannot end up attached to different registries. Absent
|
|
10250
|
-
// `runService` means no registry exists at all, so the same conditional-spread house rule
|
|
10251
|
-
// applies and the router's own 503 arm decides.
|
|
9994
|
+
// Read off `runService` by default, so both doors attach to one session registry.
|
|
10252
9995
|
...runServiceAttach === void 0 ? {} : { runServiceAttach },
|
|
10253
|
-
// 403 D-03: the mint route's port, on the same conditional-spread house rule — absent means
|
|
10254
|
-
// this host serves no live view and the route's own 503 arm decides, in one place.
|
|
10255
9996
|
...liveView === void 0 ? {} : { mintLiveUrl: liveView.mintLiveUrl },
|
|
10256
|
-
// 428 D-07: the editor mint, on the same house rule — absent means the editor mint route's own
|
|
10257
|
-
// 503 decides.
|
|
10258
9997
|
...liveView?.mintEditorLiveUrl === void 0 ? {} : { mintEditorLiveUrl: liveView.mintEditorLiveUrl },
|
|
10259
|
-
// 404-05 (D-16): the same conditional-spread house rule — absent means this host serves no
|
|
10260
|
-
// post-processing push channel and the stream arm's own 503 decides, in one place.
|
|
10261
9998
|
...postProcessingRelay === void 0 ? {} : { postProcessingRelay }
|
|
10262
9999
|
});
|
|
10263
10000
|
const runServiceRouter = runService === void 0 ? void 0 : createRunServiceRoutes(runService);
|
|
@@ -10277,13 +10014,15 @@ async function startServer(opts) {
|
|
|
10277
10014
|
apiRouter(req, res);
|
|
10278
10015
|
return;
|
|
10279
10016
|
}
|
|
10017
|
+
if (refuseNonLoopbackHost(req) !== void 0) {
|
|
10018
|
+
const address = server.address();
|
|
10019
|
+
const boundPort = typeof address === "object" && address !== null ? address.port : port;
|
|
10020
|
+
refuseForeignHostPage(res, boundPort);
|
|
10021
|
+
return;
|
|
10022
|
+
}
|
|
10280
10023
|
if (req.method === "OPTIONS") {
|
|
10281
|
-
res.writeHead(
|
|
10282
|
-
|
|
10283
|
-
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
10284
|
-
"Access-Control-Allow-Headers": "Content-Type"
|
|
10285
|
-
});
|
|
10286
|
-
res.end();
|
|
10024
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
10025
|
+
res.end("Not Found");
|
|
10287
10026
|
return;
|
|
10288
10027
|
}
|
|
10289
10028
|
if (req.method === "GET" && path.startsWith(LIVE_EMBED_PATH_PREFIX)) {
|
|
@@ -10327,8 +10066,7 @@ async function startServer(opts) {
|
|
|
10327
10066
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
10328
10067
|
res.writeHead(200, {
|
|
10329
10068
|
"Content-Type": contentType,
|
|
10330
|
-
"Content-Length": content.length
|
|
10331
|
-
"Access-Control-Allow-Origin": "*"
|
|
10069
|
+
"Content-Length": content.length
|
|
10332
10070
|
});
|
|
10333
10071
|
res.end(content);
|
|
10334
10072
|
return;
|
|
@@ -10340,8 +10078,7 @@ async function startServer(opts) {
|
|
|
10340
10078
|
const content = await (0, import_promises9.readFile)(indexPath);
|
|
10341
10079
|
res.writeHead(200, {
|
|
10342
10080
|
"Content-Type": "text/html",
|
|
10343
|
-
"Content-Length": content.length
|
|
10344
|
-
"Access-Control-Allow-Origin": "*"
|
|
10081
|
+
"Content-Length": content.length
|
|
10345
10082
|
});
|
|
10346
10083
|
res.end(content);
|
|
10347
10084
|
return;
|
|
@@ -10426,7 +10163,8 @@ async function startServer(opts) {
|
|
|
10426
10163
|
session.clearIdleTimer();
|
|
10427
10164
|
const handler = createSessionHandler(session, () => {
|
|
10428
10165
|
void sessionManager.terminateSession(sessionId).catch((error) => {
|
|
10429
|
-
|
|
10166
|
+
const line = `[live-editor] could not end session ${sessionId}`;
|
|
10167
|
+
(0, import_agent_qa_core14.printRunText)((0, import_node_util5.format)(line, error), () => console.error(line, error));
|
|
10430
10168
|
});
|
|
10431
10169
|
}, (event) => {
|
|
10432
10170
|
db.insertTokenEvent(event);
|
|
@@ -10459,7 +10197,7 @@ async function startServer(opts) {
|
|
|
10459
10197
|
server.listen(port, host, () => {
|
|
10460
10198
|
server.off("error", onError);
|
|
10461
10199
|
console.log(
|
|
10462
|
-
(0,
|
|
10200
|
+
(0, import_agent_qa_core14.isLoopbackHost)(host) ? `Dashboard running at http://localhost:${port}` : `Dashboard running at http://${host.includes(":") ? `[${host}]` : host}:${port}`
|
|
10463
10201
|
);
|
|
10464
10202
|
resolve5({
|
|
10465
10203
|
server,
|