@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.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// src/server/server.ts
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
|
-
import { readFile as readFile8, stat as
|
|
5
|
-
import { join as
|
|
4
|
+
import { readFile as readFile8, stat as stat6 } from "node:fs/promises";
|
|
5
|
+
import { join as join4, extname, resolve as resolve4 } from "node:path";
|
|
6
6
|
import { cpus as cpus2 } from "node:os";
|
|
7
|
+
import { format as format3 } from "node:util";
|
|
7
8
|
|
|
8
9
|
// src/db/run-owner-host.ts
|
|
9
10
|
import { spawnSync } from "node:child_process";
|
|
@@ -104,8 +105,8 @@ var ownedElsewhere = (pid) => `This run belongs to another agent-qa process (pid
|
|
|
104
105
|
|
|
105
106
|
// src/server/routes.ts
|
|
106
107
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
107
|
-
import { readFile as readFile6, writeFile as writeFile4
|
|
108
|
-
import {
|
|
108
|
+
import { readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
|
|
109
|
+
import { isAbsolute as isAbsolute2, basename as basename5, resolve as resolve3, dirname as dirname7 } from "node:path";
|
|
109
110
|
|
|
110
111
|
// src/db/run-status-class.ts
|
|
111
112
|
var CLASS_OF = {
|
|
@@ -302,7 +303,7 @@ var TestFileManager = class {
|
|
|
302
303
|
constructor(workspace) {
|
|
303
304
|
this.workspace = workspace;
|
|
304
305
|
}
|
|
305
|
-
//
|
|
306
|
+
// Each list call reads the workspace itself; each file is written whole, one writer at a time.
|
|
306
307
|
underLock = createPathLock();
|
|
307
308
|
async list() {
|
|
308
309
|
const records = await discoverWorkspaceFiles({ workspace: this.workspace, kind: "test" });
|
|
@@ -479,8 +480,7 @@ var ConfigManager = class _ConfigManager {
|
|
|
479
480
|
this.configPath = configPath;
|
|
480
481
|
this.mask = mask;
|
|
481
482
|
}
|
|
482
|
-
//
|
|
483
|
-
// add one. A missing file holds none.
|
|
483
|
+
// A whole document may keep a marker the file already holds, as often, but never add one.
|
|
484
484
|
static addsShownMarker(current, next) {
|
|
485
485
|
const held = shownMarkerCounts(current);
|
|
486
486
|
for (const [marker, count] of shownMarkerCounts(next)) {
|
|
@@ -488,13 +488,13 @@ var ConfigManager = class _ConfigManager {
|
|
|
488
488
|
}
|
|
489
489
|
return false;
|
|
490
490
|
}
|
|
491
|
-
//
|
|
491
|
+
// A half-edited file is named, never served as the partial config yaml recovers.
|
|
492
492
|
async readWithRevision() {
|
|
493
493
|
const read = await this.readParsed();
|
|
494
494
|
if (read.kind !== "ok") return read;
|
|
495
495
|
return { kind: "ok", config: read.config, raw: read.raw, revision: read.revision };
|
|
496
496
|
}
|
|
497
|
-
//
|
|
497
|
+
// The payload and the file view pass through the declared mask and nothing else.
|
|
498
498
|
async readMasked() {
|
|
499
499
|
const read = await this.readParsed();
|
|
500
500
|
if (read.kind === "absent") {
|
|
@@ -538,8 +538,7 @@ var ConfigManager = class _ConfigManager {
|
|
|
538
538
|
revision
|
|
539
539
|
};
|
|
540
540
|
}
|
|
541
|
-
//
|
|
542
|
-
// read the same bytes and drop each other's field.
|
|
541
|
+
// One transaction per request, serialised so overlapping autosaves never drop each other's field.
|
|
543
542
|
async applyChanges(changes, options = {}) {
|
|
544
543
|
const marked = changeWithShownMarker(changes);
|
|
545
544
|
if (marked !== void 0) {
|
|
@@ -554,13 +553,11 @@ var ConfigManager = class _ConfigManager {
|
|
|
554
553
|
}
|
|
555
554
|
return this.serialized(() => this.applyChangesOnce(changes, options.capturePriors === true));
|
|
556
555
|
}
|
|
557
|
-
//
|
|
558
|
-
// that no longer holds what the change left there was written since, and is never overwritten.
|
|
556
|
+
// An Undo restores newest first; a field written after the change is never overwritten.
|
|
559
557
|
async restorePriors(priors) {
|
|
560
558
|
return this.serialized(() => this.restorePriorsOnce(priors));
|
|
561
559
|
}
|
|
562
|
-
//
|
|
563
|
-
// bytes it was built from, read again in this path's turn. Anything else is somebody's edit.
|
|
560
|
+
// A generated whole-file change publishes only over the bytes it is built from, re-read in turn.
|
|
564
561
|
async replaceDocument(expectedRevision, content) {
|
|
565
562
|
return this.serialized(async () => {
|
|
566
563
|
let current;
|
|
@@ -687,8 +684,7 @@ var ConfigManager = class _ConfigManager {
|
|
|
687
684
|
await this.writeAtomic(next);
|
|
688
685
|
return capturePriors ? { kind: "ok", revision: revisionOf(next), baseRevision, priors } : { kind: "ok", revision: revisionOf(next), baseRevision };
|
|
689
686
|
}
|
|
690
|
-
//
|
|
691
|
-
// (chmod, because writeFile's mode is filtered by the umask), and replaces it in one rename.
|
|
687
|
+
// The temp file sits beside the real one and is chmodded, as the umask filters `writeFile` modes.
|
|
692
688
|
async writeAtomic(content) {
|
|
693
689
|
const target = await this.writeTarget();
|
|
694
690
|
const directory = dirname3(target);
|
|
@@ -1892,6 +1888,12 @@ var HookRegistryManager = class {
|
|
|
1892
1888
|
|
|
1893
1889
|
// src/server/body-parser.ts
|
|
1894
1890
|
var DEFAULT_MAX_BYTES = 1024 * 1024;
|
|
1891
|
+
function carriesNonJsonBody(req) {
|
|
1892
|
+
const hasBody = req.headers["transfer-encoding"] !== void 0 || Number(req.headers["content-length"] ?? "0") > 0;
|
|
1893
|
+
if (!hasBody) return false;
|
|
1894
|
+
const type = req.headers["content-type"];
|
|
1895
|
+
return type === void 0 || type.split(";", 1)[0].trim().toLowerCase() !== "application/json";
|
|
1896
|
+
}
|
|
1895
1897
|
function readJsonBody(req, maxBytes = DEFAULT_MAX_BYTES) {
|
|
1896
1898
|
return new Promise((resolve5, reject) => {
|
|
1897
1899
|
const chunks = [];
|
|
@@ -1926,12 +1928,16 @@ function readJsonBody(req, maxBytes = DEFAULT_MAX_BYTES) {
|
|
|
1926
1928
|
// src/live-editor/live-session.ts
|
|
1927
1929
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
1928
1930
|
import { dirname as dirname6, resolve as resolvePath2 } from "node:path";
|
|
1931
|
+
import { format } from "node:util";
|
|
1929
1932
|
import {
|
|
1930
1933
|
AUTH_STATE_SCHEMA_VERSION,
|
|
1931
1934
|
MobileSetupError as MobileSetupError2,
|
|
1932
1935
|
PlannerConfigSchema,
|
|
1933
1936
|
SuiteDefinitionSchema,
|
|
1934
1937
|
TestDefinitionSchema,
|
|
1938
|
+
TimeoutConfigSchema,
|
|
1939
|
+
getAgentQaVersion,
|
|
1940
|
+
printRunText,
|
|
1935
1941
|
readAuthStateMetadata,
|
|
1936
1942
|
resolveAuthStatePaths,
|
|
1937
1943
|
writeAuthStateFiles
|
|
@@ -1960,7 +1966,7 @@ import {
|
|
|
1960
1966
|
resolveVisionGate,
|
|
1961
1967
|
visionFromConfig
|
|
1962
1968
|
} from "@vostride/agent-qa-model";
|
|
1963
|
-
import {
|
|
1969
|
+
import { resolvePlannerLimits } from "@vostride/agent-qa-schema";
|
|
1964
1970
|
import { parse as parseYaml5 } from "yaml";
|
|
1965
1971
|
|
|
1966
1972
|
// src/llm-model-mapping.ts
|
|
@@ -2039,6 +2045,7 @@ import { dirname as dirname5, join as join3 } from "node:path";
|
|
|
2039
2045
|
import { parse as parseYaml4 } from "yaml";
|
|
2040
2046
|
import {
|
|
2041
2047
|
MobileSetupError,
|
|
2048
|
+
refuseRetiredLocalBindings,
|
|
2042
2049
|
resolveMobileRunConfig
|
|
2043
2050
|
} from "@vostride/agent-qa-core";
|
|
2044
2051
|
async function loadLocalBindings(configPath) {
|
|
@@ -2046,12 +2053,10 @@ async function loadLocalBindings(configPath) {
|
|
|
2046
2053
|
try {
|
|
2047
2054
|
const content = await readFile5(filePath, "utf-8");
|
|
2048
2055
|
const parsed = parseYaml4(content);
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
filePath
|
|
2054
|
-
} : { filePath };
|
|
2056
|
+
refuseRetiredLocalBindings(parsed, filePath);
|
|
2057
|
+
if (!parsed || typeof parsed !== "object") return { filePath };
|
|
2058
|
+
const bindings = parsed;
|
|
2059
|
+
return { devices: bindings.devices, apps: bindings.apps, filePath };
|
|
2055
2060
|
} catch (err) {
|
|
2056
2061
|
if (err.code === "ENOENT") return null;
|
|
2057
2062
|
throw err;
|
|
@@ -2141,44 +2146,15 @@ async function prepareMobileLiveSession(input) {
|
|
|
2141
2146
|
appActivity: resolved.app.appActivity,
|
|
2142
2147
|
deepLinkAppId: resolved.app.deepLinkAppId,
|
|
2143
2148
|
appPath: resolved.app.install?.path,
|
|
2144
|
-
browserstackApp: resolved.app.install?.browserstack,
|
|
2145
2149
|
appState: resolved.appState
|
|
2146
2150
|
}
|
|
2147
2151
|
};
|
|
2148
2152
|
}
|
|
2149
2153
|
|
|
2150
2154
|
// src/live-editor/live-session.ts
|
|
2151
|
-
var LiveProductionRefusedError = class extends Error {
|
|
2152
|
-
productionTargetNames;
|
|
2153
|
-
constructor(productionTargetNames) {
|
|
2154
|
-
super(buildProductionRefusal(productionTargetNames));
|
|
2155
|
-
this.name = "LiveProductionRefusedError";
|
|
2156
|
-
this.productionTargetNames = distinctInFirstOccurrenceOrder(productionTargetNames);
|
|
2157
|
-
}
|
|
2158
|
-
};
|
|
2159
2155
|
function isConfigRecord(value) {
|
|
2160
2156
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2161
2157
|
}
|
|
2162
|
-
function liveTargetName(targetName) {
|
|
2163
|
-
if (typeof targetName !== "string") return void 0;
|
|
2164
|
-
const trimmed = targetName.trim();
|
|
2165
|
-
return trimmed.length > 0 ? trimmed : void 0;
|
|
2166
|
-
}
|
|
2167
|
-
async function resolveLiveTargetProduction(configManager, targetName) {
|
|
2168
|
-
const name = liveTargetName(targetName);
|
|
2169
|
-
if (name === void 0 || configManager === void 0) {
|
|
2170
|
-
return { productionTargetNames: [], allowProduction: false };
|
|
2171
|
-
}
|
|
2172
|
-
const config = await readConfigValues(configManager);
|
|
2173
|
-
const registry = isConfigRecord(config.registry) ? config.registry : void 0;
|
|
2174
|
-
const targets = registry !== void 0 && isConfigRecord(registry.targets) ? registry.targets : void 0;
|
|
2175
|
-
const declared = targets !== void 0 && Object.hasOwn(targets, name) ? targets[name] : void 0;
|
|
2176
|
-
const production = isConfigRecord(declared) && declared.environment === "production";
|
|
2177
|
-
return {
|
|
2178
|
-
productionTargetNames: production ? [name] : [],
|
|
2179
|
-
allowProduction: config.allowProduction === true
|
|
2180
|
-
};
|
|
2181
|
-
}
|
|
2182
2158
|
async function readEntityUse(entity, readers) {
|
|
2183
2159
|
if (entity === void 0) return void 0;
|
|
2184
2160
|
const find = entity.type === "suite" ? readers.findSuite : readers.findTest;
|
|
@@ -2198,6 +2174,12 @@ var LivePlannerLimitsRefusedError = class extends Error {
|
|
|
2198
2174
|
this.name = "LivePlannerLimitsRefusedError";
|
|
2199
2175
|
}
|
|
2200
2176
|
};
|
|
2177
|
+
var LiveNavigationTimeoutRefusedError = class extends Error {
|
|
2178
|
+
constructor(message) {
|
|
2179
|
+
super(message);
|
|
2180
|
+
this.name = "LiveNavigationTimeoutRefusedError";
|
|
2181
|
+
}
|
|
2182
|
+
};
|
|
2201
2183
|
var LIVE_LIMIT_LEAVES = ["maxActions", "maxPlanningTurns"];
|
|
2202
2184
|
function shownLimitValue(value) {
|
|
2203
2185
|
return typeof value === "number" ? String(value) : JSON.stringify(value) ?? String(value);
|
|
@@ -2246,9 +2228,30 @@ async function resolveLivePlannerLimits(configManager, entities, readers) {
|
|
|
2246
2228
|
}
|
|
2247
2229
|
return resolvePlannerLimits(config.use, ...uses);
|
|
2248
2230
|
}
|
|
2249
|
-
function
|
|
2250
|
-
|
|
2251
|
-
|
|
2231
|
+
async function resolveLiveNavigationTimeoutMs(configManager, entities, readers) {
|
|
2232
|
+
const config = configManager === void 0 ? {} : await readConfigValues(configManager);
|
|
2233
|
+
const layers = [
|
|
2234
|
+
{ layer: "project config", use: config.use }
|
|
2235
|
+
];
|
|
2236
|
+
for (const entity of [].concat(entities ?? [])) {
|
|
2237
|
+
const use = await readEntityUse(entity, readers);
|
|
2238
|
+
layers.push({ layer: `${entity.type} file ${entity.id}`, use });
|
|
2239
|
+
}
|
|
2240
|
+
let navigation;
|
|
2241
|
+
for (const { layer, use } of layers) {
|
|
2242
|
+
if (!isConfigRecord(use) || !isConfigRecord(use.timeout)) continue;
|
|
2243
|
+
if (!Object.hasOwn(use.timeout, "navigation")) continue;
|
|
2244
|
+
const value = use.timeout.navigation;
|
|
2245
|
+
const parsed = TimeoutConfigSchema.shape.navigation.safeParse(value);
|
|
2246
|
+
if (!parsed.success) {
|
|
2247
|
+
const issue = parsed.error.issues[0]?.message ?? "invalid value";
|
|
2248
|
+
throw new LiveNavigationTimeoutRefusedError(
|
|
2249
|
+
`${layer} sets use.timeout.navigation to ${shownLimitValue(value)}: ${issue}. The CLI refuses this file too.`
|
|
2250
|
+
);
|
|
2251
|
+
}
|
|
2252
|
+
navigation = parsed.data;
|
|
2253
|
+
}
|
|
2254
|
+
return navigation;
|
|
2252
2255
|
}
|
|
2253
2256
|
var IDLE_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
2254
2257
|
var LIVE_HUB_CLOSE_LIMIT_MS = 2e3;
|
|
@@ -2384,8 +2387,7 @@ var LiveSession = class {
|
|
|
2384
2387
|
planner = null;
|
|
2385
2388
|
verifier = null;
|
|
2386
2389
|
modelProfiles = null;
|
|
2387
|
-
//
|
|
2388
|
-
// rooted at the session's `.agent-qa` runtime dir (the FsStorageStateStore rooting idiom).
|
|
2390
|
+
// Built lazily on first use, rooted at the session's `.agent-qa` runtime dir.
|
|
2389
2391
|
storageStateStore = null;
|
|
2390
2392
|
model = null;
|
|
2391
2393
|
variableStore = null;
|
|
@@ -2413,15 +2415,14 @@ var LiveSession = class {
|
|
|
2413
2415
|
mobileAppiumLease = null;
|
|
2414
2416
|
secretStore;
|
|
2415
2417
|
secretRedactor;
|
|
2416
|
-
//
|
|
2417
|
-
// setup hooks failed has no adapter and therefore no hub; its subject declares `none`.
|
|
2418
|
+
// One hub per session at handle 0, created once the adapter exists; no adapter means no hub.
|
|
2418
2419
|
liveHub = null;
|
|
2419
2420
|
attachment = null;
|
|
2420
2421
|
resolveEnded = () => {
|
|
2421
2422
|
};
|
|
2422
2423
|
ended;
|
|
2423
2424
|
cleanupRun = null;
|
|
2424
|
-
//
|
|
2425
|
+
// Which control socket is the current one; only its close may detach the session.
|
|
2425
2426
|
attachmentGeneration = 0;
|
|
2426
2427
|
releaseAttachment = null;
|
|
2427
2428
|
get modelName() {
|
|
@@ -2443,11 +2444,7 @@ var LiveSession = class {
|
|
|
2443
2444
|
hub.register(0, registration?.capability, registration?.fallback);
|
|
2444
2445
|
this.liveHub = hub;
|
|
2445
2446
|
}
|
|
2446
|
-
|
|
2447
|
-
* Make a control socket the current one. `release` is how the session lets go of it later: the
|
|
2448
|
-
* previous attachment's is called with `superseded` here (428 WR-01), so one socket drives the
|
|
2449
|
-
* session at a time.
|
|
2450
|
-
*/
|
|
2447
|
+
// The previous attachment's `release` gets `superseded`, so one socket drives the session.
|
|
2451
2448
|
beginAttachment(release) {
|
|
2452
2449
|
const previous = this.releaseAttachment;
|
|
2453
2450
|
this.attachmentGeneration += 1;
|
|
@@ -2466,11 +2463,7 @@ var LiveSession = class {
|
|
|
2466
2463
|
}
|
|
2467
2464
|
this.pendingMessages = [];
|
|
2468
2465
|
}
|
|
2469
|
-
|
|
2470
|
-
* A command's reply, for whichever control socket is attached now (428 CR-01). A command can
|
|
2471
|
-
* outlive the socket that issued it, and its outcome belongs to the page, not to that socket:
|
|
2472
|
-
* with none attached it is held and flushed to the next one.
|
|
2473
|
-
*/
|
|
2466
|
+
// A reply can outlive its socket, so it goes to the current socket or waits for the next one.
|
|
2474
2467
|
deliver(message) {
|
|
2475
2468
|
if (this.messageSink) {
|
|
2476
2469
|
this.messageSink(message);
|
|
@@ -2581,7 +2574,7 @@ var LiveSession = class {
|
|
|
2581
2574
|
continue;
|
|
2582
2575
|
}
|
|
2583
2576
|
const result = await runHooks([hookDef], {
|
|
2584
|
-
executor: new DockerHookExecutor(),
|
|
2577
|
+
executor: new DockerHookExecutor({ version: getAgentQaVersion() }),
|
|
2585
2578
|
env: this.getHookEnvVars(),
|
|
2586
2579
|
secretStore: this.secretStore,
|
|
2587
2580
|
secretRedactor: this.secretRedactor
|
|
@@ -2655,7 +2648,6 @@ var LiveSession = class {
|
|
|
2655
2648
|
device: config.device ? {
|
|
2656
2649
|
name: config.device.name,
|
|
2657
2650
|
platform,
|
|
2658
|
-
transport: "local",
|
|
2659
2651
|
match: config.device
|
|
2660
2652
|
} : void 0
|
|
2661
2653
|
};
|
|
@@ -2720,15 +2712,6 @@ var LiveSession = class {
|
|
|
2720
2712
|
}
|
|
2721
2713
|
}
|
|
2722
2714
|
async initialize(config) {
|
|
2723
|
-
const found = await resolveLiveTargetProduction(this.deps.configManager, config.targetName);
|
|
2724
|
-
const refusal = liveProductionRefusal({
|
|
2725
|
-
productionTargetNames: distinctInFirstOccurrenceOrder([
|
|
2726
|
-
...config.production?.productionTargetNames ?? [],
|
|
2727
|
-
...found.productionTargetNames
|
|
2728
|
-
]),
|
|
2729
|
-
allowProduction: found.allowProduction
|
|
2730
|
-
});
|
|
2731
|
-
if (refusal !== void 0) throw refusal;
|
|
2732
2715
|
this.platform = config.platform;
|
|
2733
2716
|
this.targetName = typeof config.targetName === "string" && config.targetName.trim().length > 0 ? config.targetName.trim() : null;
|
|
2734
2717
|
this._modelName = config.llmConfig.model ?? "unknown";
|
|
@@ -2763,7 +2746,11 @@ var LiveSession = class {
|
|
|
2763
2746
|
} else {
|
|
2764
2747
|
const { WebPlatformAdapter } = await import("@vostride/agent-qa-web");
|
|
2765
2748
|
const adapter = new WebPlatformAdapter();
|
|
2766
|
-
await adapter.setup({
|
|
2749
|
+
await adapter.setup({
|
|
2750
|
+
platform: "web",
|
|
2751
|
+
browser: { name: "chromium", headless: config.headless ?? true },
|
|
2752
|
+
...config.navigationTimeoutMs === void 0 ? {} : { timeouts: { navigation: config.navigationTimeoutMs } }
|
|
2753
|
+
});
|
|
2767
2754
|
this.adapter = adapter;
|
|
2768
2755
|
if (config.url) {
|
|
2769
2756
|
const resolvedUrl = this.resolveSecrets(config.url);
|
|
@@ -2825,7 +2812,7 @@ var LiveSession = class {
|
|
|
2825
2812
|
}
|
|
2826
2813
|
const startedAt = performance.now();
|
|
2827
2814
|
try {
|
|
2828
|
-
const result = await new DockerHookExecutor().execute(hookDef, {
|
|
2815
|
+
const result = await new DockerHookExecutor({ version: getAgentQaVersion() }).execute(hookDef, {
|
|
2829
2816
|
env: this.getHookEnvVars(),
|
|
2830
2817
|
secretEnv: resolveSecretEnv(this.secretStore),
|
|
2831
2818
|
secretRedactor: this.secretRedactor
|
|
@@ -2992,12 +2979,7 @@ var LiveSession = class {
|
|
|
2992
2979
|
};
|
|
2993
2980
|
return this.redact(result);
|
|
2994
2981
|
}
|
|
2995
|
-
//
|
|
2996
|
-
// lifecycle (executeStepCommand for single steps, executeTestCommand for
|
|
2997
|
-
// the per-test loop that runs many steps under one outer gate).
|
|
2998
|
-
// D-14 (Phase 360): lazily construct the run-scoped storage-state store, rooted at the session's
|
|
2999
|
-
// `.agent-qa` runtime dir. All fs writes live in runner-local (validate:arch storageNeutral);
|
|
3000
|
-
// the loop records only { name, fingerprint }. Absent configPath falls back to the cwd root.
|
|
2982
|
+
// Lazily built and rooted at the session's `.agent-qa` dir; fs writes stay in runner-local.
|
|
3001
2983
|
async getStorageStateStore() {
|
|
3002
2984
|
let store = this.storageStateStore;
|
|
3003
2985
|
if (!store) {
|
|
@@ -3136,7 +3118,9 @@ ${errorLines.join("\n")}`;
|
|
|
3136
3118
|
testLimits = this.plannerLimitsForTest !== void 0 && testDraft.testId.length > 0 ? await this.plannerLimitsForTest(testDraft.testId) : this.plannerLimits ?? resolvePlannerLimits();
|
|
3137
3119
|
} catch (error) {
|
|
3138
3120
|
if (!(error instanceof LivePlannerLimitsRefusedError)) throw error;
|
|
3139
|
-
|
|
3121
|
+
const line = `[live-session] executeTestCommand: ${error.message}`;
|
|
3122
|
+
const detail = { testName: testDraft.testName };
|
|
3123
|
+
printRunText(format(line, detail), () => console.warn(line, detail));
|
|
3140
3124
|
return this.redact({
|
|
3141
3125
|
status: "failed",
|
|
3142
3126
|
duration: performance.now() - start,
|
|
@@ -3156,11 +3140,13 @@ ${errorLines.join("\n")}`;
|
|
|
3156
3140
|
if (!setupResult.allPassed) {
|
|
3157
3141
|
const failedHook = setupHookPayloads.find((h) => h.status === "failed");
|
|
3158
3142
|
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";
|
|
3159
|
-
|
|
3143
|
+
const line = `[live-session] executeTestCommand: ${detail}`;
|
|
3144
|
+
const context = {
|
|
3160
3145
|
testName: testDraft.testName,
|
|
3161
3146
|
setupHooks: testDraft.setup,
|
|
3162
3147
|
failedHook: failedHook ? { name: failedHook.hookName, stderr: failedHook.stderr, error: failedHook.error } : null
|
|
3163
|
-
}
|
|
3148
|
+
};
|
|
3149
|
+
printRunText(format(line, context), () => console.warn(line, context));
|
|
3164
3150
|
return {
|
|
3165
3151
|
status: "failed",
|
|
3166
3152
|
duration: performance.now() - start,
|
|
@@ -3238,7 +3224,9 @@ ${errorLines.join("\n")}`;
|
|
|
3238
3224
|
topLevelError = failedHook ? `Teardown hook "${failedHook.hookName}" failed${failedHook.error ? `: ${failedHook.error}` : ""}` : "Teardown hook failed";
|
|
3239
3225
|
}
|
|
3240
3226
|
if (topLevelError) {
|
|
3241
|
-
|
|
3227
|
+
const line = `[live-session] executeTestCommand: ${topLevelError}`;
|
|
3228
|
+
const detail = { testName: testDraft.testName };
|
|
3229
|
+
printRunText(format(line, detail), () => console.warn(line, detail));
|
|
3242
3230
|
}
|
|
3243
3231
|
return this.redact({
|
|
3244
3232
|
status: finalStatus,
|
|
@@ -3254,10 +3242,7 @@ ${errorLines.join("\n")}`;
|
|
|
3254
3242
|
this.status = "idle";
|
|
3255
3243
|
}
|
|
3256
3244
|
}
|
|
3257
|
-
//
|
|
3258
|
-
// hook payloads (hook-complete) for inclusion in the LiveTestResultPayload.
|
|
3259
|
-
// The capture sits beside the message sink rather than wrapping it, so a
|
|
3260
|
-
// control socket that reattaches mid-hook keeps its sink (428 CR-01).
|
|
3245
|
+
// Captures dispatched hook payloads beside the message sink, so a reattached socket keeps it.
|
|
3261
3246
|
async runLifecycleHooksCapturing(phase, hookIds, sink, owner = { scope: "suite" }) {
|
|
3262
3247
|
this.hookCaptures.add(sink);
|
|
3263
3248
|
try {
|
|
@@ -3501,6 +3486,21 @@ function refuseUnparseableRequest(res) {
|
|
|
3501
3486
|
});
|
|
3502
3487
|
res.end(body);
|
|
3503
3488
|
}
|
|
3489
|
+
function refuseForeignHostPage(res, port) {
|
|
3490
|
+
if (res.headersSent) {
|
|
3491
|
+
res.destroy();
|
|
3492
|
+
return;
|
|
3493
|
+
}
|
|
3494
|
+
const p = String(port);
|
|
3495
|
+
const body = `The agent-qa dashboard answers only at localhost, 127.0.0.1 or [::1].
|
|
3496
|
+
Open http://localhost:${p}/ on the machine it runs on, or forward the port with ssh -L ${p}:localhost:${p} <host>.
|
|
3497
|
+
`;
|
|
3498
|
+
res.writeHead(403, {
|
|
3499
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
3500
|
+
"Content-Length": Buffer.byteLength(body)
|
|
3501
|
+
});
|
|
3502
|
+
res.end(body);
|
|
3503
|
+
}
|
|
3504
3504
|
function backstopServerError(res, req, error) {
|
|
3505
3505
|
const kind = error instanceof Error ? error.constructor.name : typeof error;
|
|
3506
3506
|
console.error(
|
|
@@ -3606,10 +3606,6 @@ function respondJson(res, status, data) {
|
|
|
3606
3606
|
res.end(body);
|
|
3607
3607
|
}
|
|
3608
3608
|
function serveRunEvidenceRead(req, res, deps) {
|
|
3609
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req)) {
|
|
3610
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
3611
|
-
return;
|
|
3612
|
-
}
|
|
3613
3609
|
const query = new URL(req.url ?? "/", "http://localhost").searchParams;
|
|
3614
3610
|
const entries = [["runId", deps.runId]];
|
|
3615
3611
|
const seen = /* @__PURE__ */ new Set(["runId"]);
|
|
@@ -3651,10 +3647,6 @@ function respondFeedbackRead(res, deps, functionId, input, parse) {
|
|
|
3651
3647
|
});
|
|
3652
3648
|
}
|
|
3653
3649
|
function serveRunMarksHistoryRead(req, res, deps) {
|
|
3654
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
|
|
3655
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
3656
|
-
return;
|
|
3657
|
-
}
|
|
3658
3650
|
if (new URL(req.url ?? "/", "http://localhost").search !== "") {
|
|
3659
3651
|
respondJson(res, 400, { error: "invalid_marks_input" });
|
|
3660
3652
|
return;
|
|
@@ -3673,10 +3665,6 @@ function serveRunMarksHistoryRead(req, res, deps) {
|
|
|
3673
3665
|
);
|
|
3674
3666
|
}
|
|
3675
3667
|
function serveRunMarkPreviewRead(req, res, deps) {
|
|
3676
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
|
|
3677
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
3678
|
-
return;
|
|
3679
|
-
}
|
|
3680
3668
|
void readJsonBody(req).then((body) => {
|
|
3681
3669
|
const parsed = RunsMarkInputSchema.safeParse(body);
|
|
3682
3670
|
if (!parsed.success) {
|
|
@@ -3699,10 +3687,6 @@ function serveRunMarkPreviewRead(req, res, deps) {
|
|
|
3699
3687
|
});
|
|
3700
3688
|
}
|
|
3701
3689
|
function serveRunDetailRead(req, res, deps) {
|
|
3702
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
|
|
3703
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
3704
|
-
return;
|
|
3705
|
-
}
|
|
3706
3690
|
void readRunDetail(deps.host, deps.runId).then(
|
|
3707
3691
|
(outcome) => {
|
|
3708
3692
|
if (outcome.kind === "refused") {
|
|
@@ -3854,10 +3838,6 @@ async function readRunAnalysis(host, runId) {
|
|
|
3854
3838
|
}
|
|
3855
3839
|
}
|
|
3856
3840
|
function serveRunAnalysisRead(req, res, deps) {
|
|
3857
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
|
|
3858
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
3859
|
-
return;
|
|
3860
|
-
}
|
|
3861
3841
|
void readRunAnalysis(deps.host, deps.runId).then(
|
|
3862
3842
|
(outcome) => {
|
|
3863
3843
|
if (outcome.kind === "refused") {
|
|
@@ -4081,10 +4061,7 @@ function serveRunEventStream(req, res, deps) {
|
|
|
4081
4061
|
else pending.push(frame);
|
|
4082
4062
|
};
|
|
4083
4063
|
const frameOf = (event) => (
|
|
4084
|
-
//
|
|
4085
|
-
// data IS the envelope's own JSON. No projection, no rename, no second vocabulary. The filter
|
|
4086
|
-
// DROPS envelopes; it never edits one, so a filtered stream's frames are byte-identical to the
|
|
4087
|
-
// firehose's — with gaps in the ids, which is what seq-as-id means once a filter exists.
|
|
4064
|
+
// The filter drops envelopes and never edits one, so filtered frames match the firehose bytes.
|
|
4088
4065
|
`id: ${String(event.seq)}
|
|
4089
4066
|
event: ${event.type}
|
|
4090
4067
|
data: ${JSON.stringify(event)}
|
|
@@ -4146,12 +4123,6 @@ data: ${JSON.stringify(event)}
|
|
|
4146
4123
|
deps.openStreams?.add(teardown);
|
|
4147
4124
|
req.on("close", teardown);
|
|
4148
4125
|
}
|
|
4149
|
-
function runDenialOf(error) {
|
|
4150
|
-
if (typeof error !== "object" || error === null) return void 0;
|
|
4151
|
-
const neutralReason = error.neutralReason;
|
|
4152
|
-
if (typeof neutralReason !== "string" || neutralReason.trim().length === 0) return void 0;
|
|
4153
|
-
return { error: "run_denied", neutralReason };
|
|
4154
|
-
}
|
|
4155
4126
|
function createRunServiceRoutes(ports) {
|
|
4156
4127
|
const openStreams = /* @__PURE__ */ new Set();
|
|
4157
4128
|
function handleStart(req, res) {
|
|
@@ -4182,12 +4153,7 @@ function createRunServiceRoutes(ports) {
|
|
|
4182
4153
|
ack = await ports.invokeRunStart(
|
|
4183
4154
|
parsed.data.patterns === void 0 ? {} : { patterns: parsed.data.patterns }
|
|
4184
4155
|
);
|
|
4185
|
-
} catch
|
|
4186
|
-
const denial = runDenialOf(error);
|
|
4187
|
-
if (denial !== void 0) {
|
|
4188
|
-
respondJson2(res, 403, denial);
|
|
4189
|
-
return;
|
|
4190
|
-
}
|
|
4156
|
+
} catch {
|
|
4191
4157
|
refuse(res, 500, "run_start_failed");
|
|
4192
4158
|
return;
|
|
4193
4159
|
}
|
|
@@ -4267,6 +4233,10 @@ function createRunServiceRoutes(ports) {
|
|
|
4267
4233
|
refuse(res, 403, "cross_site_refused");
|
|
4268
4234
|
return;
|
|
4269
4235
|
}
|
|
4236
|
+
if (carriesNonJsonBody(req)) {
|
|
4237
|
+
refuse(res, 415, "unsupported_media_type");
|
|
4238
|
+
return;
|
|
4239
|
+
}
|
|
4270
4240
|
const matched = matchRunServiceRoute(req.method, path);
|
|
4271
4241
|
if (matched === void 0) {
|
|
4272
4242
|
refuse(res, 404, "unknown_route");
|
|
@@ -4534,8 +4504,7 @@ data: ${JSON.stringify(again.body)}
|
|
|
4534
4504
|
// A failed re-read is not a reason to close a live channel: the next envelope will trigger
|
|
4535
4505
|
// another, and the page's own poll is the floor underneath both.
|
|
4536
4506
|
() => void 0
|
|
4537
|
-
//
|
|
4538
|
-
// failure arm never sees its own fulfilment arm's throw (UF-01, on the nested chain).
|
|
4507
|
+
// A throw from the arm above is a dead socket, and `.then`'s failure arm never sees it.
|
|
4539
4508
|
).catch(failStream);
|
|
4540
4509
|
});
|
|
4541
4510
|
heartbeat = setInterval(() => {
|
|
@@ -4574,10 +4543,6 @@ async function readRunKnowledge(host, runId) {
|
|
|
4574
4543
|
}
|
|
4575
4544
|
}
|
|
4576
4545
|
function serveRunKnowledgeRead(req, res, deps) {
|
|
4577
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
|
|
4578
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
4579
|
-
return;
|
|
4580
|
-
}
|
|
4581
4546
|
void readRunKnowledge(deps.host, deps.runId).then(
|
|
4582
4547
|
(outcome) => {
|
|
4583
4548
|
if (outcome.kind === "refused") {
|
|
@@ -4620,10 +4585,6 @@ async function markRun(host, runId, body) {
|
|
|
4620
4585
|
}
|
|
4621
4586
|
}
|
|
4622
4587
|
function registerRunMarkRoute(req, res, deps) {
|
|
4623
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
|
|
4624
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
4625
|
-
return;
|
|
4626
|
-
}
|
|
4627
4588
|
void readJsonBody(req).then(async (body) => {
|
|
4628
4589
|
const outcome = await markRun(deps.host, deps.runId, body);
|
|
4629
4590
|
if (outcome.kind === "refused") {
|
|
@@ -4646,10 +4607,6 @@ import {
|
|
|
4646
4607
|
isPageWritableConfigPath
|
|
4647
4608
|
} from "@vostride/agent-qa-schema";
|
|
4648
4609
|
function registerConfigWriteRoute(req, res, deps) {
|
|
4649
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
4650
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
4651
|
-
return;
|
|
4652
|
-
}
|
|
4653
4610
|
void readJsonBody(req).then(async (body) => {
|
|
4654
4611
|
const parsed = ConfigWriteRequestSchema.safeParse(body);
|
|
4655
4612
|
if (!parsed.success) {
|
|
@@ -4707,10 +4664,6 @@ function registerConfigUndoRoute(req, res, deps) {
|
|
|
4707
4664
|
respondJson(res, 405, { error: "method_not_allowed" });
|
|
4708
4665
|
return;
|
|
4709
4666
|
}
|
|
4710
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
4711
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
4712
|
-
return;
|
|
4713
|
-
}
|
|
4714
4667
|
void readJsonBody(req).then(async (body) => {
|
|
4715
4668
|
const parsed = ConfigUndoRequestSchema.safeParse(body);
|
|
4716
4669
|
if (!parsed.success) {
|
|
@@ -4953,10 +4906,6 @@ async function connectName(request2, deps) {
|
|
|
4953
4906
|
});
|
|
4954
4907
|
}
|
|
4955
4908
|
function registerLlmConnectRoute(req, res, deps) {
|
|
4956
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
4957
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
4958
|
-
return;
|
|
4959
|
-
}
|
|
4960
4909
|
void readJsonBody(req).then(async (body) => {
|
|
4961
4910
|
const answer = await connectLlm(body, deps);
|
|
4962
4911
|
respondJson(res, answer.status, answer.body);
|
|
@@ -4968,9 +4917,9 @@ function registerLlmConnectRoute(req, res, deps) {
|
|
|
4968
4917
|
}
|
|
4969
4918
|
|
|
4970
4919
|
// src/server/routes.ts
|
|
4971
|
-
import { AuthStateNameSchema, buildInternalRunAttributes, mergeRunAttributes, readAuth as readAuth2, writeAuth as writeAuth2, removeAuth as removeAuth2, getAgentQaVersion, getAgentQaUpdateStatus, getLLMAuthProviderPlugin as getLLMAuthProviderPlugin2, listAuthStateMetadata, listLLMAuthProviderPlugins, ModelConfigSchema as ModelConfigSchema2, NameSlugSchema as NameSlugSchema2, TimeoutConfigSchema, AgentQaConfigSchema as AgentQaConfigSchema3, TestDefinitionSchema as TestDefinitionSchema2, SuiteDefinitionSchema as SuiteDefinitionSchema2, parseEnvFile, serializeEnvFile,
|
|
4920
|
+
import { AuthStateNameSchema, buildInternalRunAttributes, DEFAULT_AGENT_QA_CACHE_DIR, mergeRunAttributes, readAuth as readAuth2, writeAuth as writeAuth2, removeAuth as removeAuth2, getAgentQaVersion as getAgentQaVersion2, getAgentQaUpdateStatus, getLLMAuthProviderPlugin as getLLMAuthProviderPlugin2, listAuthStateMetadata, listLLMAuthProviderPlugins, ModelConfigSchema as ModelConfigSchema2, NameSlugSchema as NameSlugSchema2, TimeoutConfigSchema as TimeoutConfigSchema2, AgentQaConfigSchema as AgentQaConfigSchema3, TestDefinitionSchema as TestDefinitionSchema2, SuiteDefinitionSchema as SuiteDefinitionSchema2, parseEnvFile, serializeEnvFile, hookRunnerImage, validateUserRunAttributes, discoverWorkspaceFiles as discoverWorkspaceFiles3, isWorkspacePathMatch, resolveWorkspaceFileTarget as resolveWorkspaceFileTarget2, SizeString } from "@vostride/agent-qa-core";
|
|
4972
4921
|
import { SecretStore, SecretRedactor } from "@vostride/agent-qa-runner";
|
|
4973
|
-
import { DockerHookExecutor as DockerHookExecutor2 } from "@vostride/agent-qa-runner-local";
|
|
4922
|
+
import { DockerHookExecutor as DockerHookExecutor2, purgeActionCache } from "@vostride/agent-qa-runner-local";
|
|
4974
4923
|
import { createModel as createModel3, fetchLiveModels, getModelCatalog, listProviders, mergeCatalog, resolveVisionConfig } from "@vostride/agent-qa-model";
|
|
4975
4924
|
import { parse as parseYaml6 } from "yaml";
|
|
4976
4925
|
var LLM_PROVIDER_MODES = /* @__PURE__ */ new Set([
|
|
@@ -5278,25 +5227,10 @@ function llmConnectionTestTimeoutMs(config) {
|
|
|
5278
5227
|
return REMOTE_LLM_CONNECTION_TEST_TIMEOUT_MS;
|
|
5279
5228
|
}
|
|
5280
5229
|
function json(res, data, status = 200) {
|
|
5281
|
-
const body = JSON.stringify(data);
|
|
5282
|
-
res.writeHead(status, {
|
|
5283
|
-
"Content-Type": "application/json",
|
|
5284
|
-
"Access-Control-Allow-Origin": "*",
|
|
5285
|
-
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
5286
|
-
"Access-Control-Allow-Headers": "Content-Type"
|
|
5287
|
-
});
|
|
5288
|
-
res.end(body);
|
|
5289
|
-
}
|
|
5290
|
-
function jsonNoCors(res, data, status = 200) {
|
|
5291
5230
|
const body = JSON.stringify(data);
|
|
5292
5231
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
5293
5232
|
res.end(body);
|
|
5294
5233
|
}
|
|
5295
|
-
function refuseCrossSiteDisplayRead(req, res) {
|
|
5296
|
-
if (refuseCrossSiteRequest(req) === void 0) return false;
|
|
5297
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
5298
|
-
return true;
|
|
5299
|
-
}
|
|
5300
5234
|
var INSIGHTS_KNOWLEDGE_RUN_CAP = 200;
|
|
5301
5235
|
var INSIGHTS_KNOWLEDGE_CONCURRENCY = 4;
|
|
5302
5236
|
async function isPostProcessingSkipped(host, runId) {
|
|
@@ -5360,14 +5294,6 @@ async function readInsightsKnowledgeChanges(host, db, window) {
|
|
|
5360
5294
|
function notFound(res, message = "Not found") {
|
|
5361
5295
|
json(res, { error: message }, 404);
|
|
5362
5296
|
}
|
|
5363
|
-
function cors(res) {
|
|
5364
|
-
res.writeHead(204, {
|
|
5365
|
-
"Access-Control-Allow-Origin": "*",
|
|
5366
|
-
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
5367
|
-
"Access-Control-Allow-Headers": "Content-Type"
|
|
5368
|
-
});
|
|
5369
|
-
res.end();
|
|
5370
|
-
}
|
|
5371
5297
|
function isPlainRecord(value) {
|
|
5372
5298
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5373
5299
|
}
|
|
@@ -5420,7 +5346,7 @@ function normalizeRunFilterValue(value) {
|
|
|
5420
5346
|
function buildRunRequestAttributes(input) {
|
|
5421
5347
|
const internal = buildInternalRunAttributes({
|
|
5422
5348
|
trigger: input.trigger,
|
|
5423
|
-
runner:
|
|
5349
|
+
runner: "local"
|
|
5424
5350
|
});
|
|
5425
5351
|
const user = validateUserRunAttributes(input.userAttributes, "run attributes");
|
|
5426
5352
|
return mergeRunAttributes(internal, user);
|
|
@@ -5555,7 +5481,7 @@ function parseTimeoutConfigTestTimeoutMs(timeoutConfig) {
|
|
|
5555
5481
|
return transformedTimeout;
|
|
5556
5482
|
}
|
|
5557
5483
|
}
|
|
5558
|
-
const parsed =
|
|
5484
|
+
const parsed = TimeoutConfigSchema2.partial().safeParse(timeoutConfig);
|
|
5559
5485
|
if (!parsed.success) return void 0;
|
|
5560
5486
|
const testTimeout = parsed.data.test;
|
|
5561
5487
|
return typeof testTimeout === "number" && Number.isFinite(testTimeout) && testTimeout > 0 ? testTimeout : void 0;
|
|
@@ -5865,9 +5791,7 @@ var RESPONSE_SAFE_RUN_METADATA_KEYS = /* @__PURE__ */ new Set([
|
|
|
5865
5791
|
"timeoutSource",
|
|
5866
5792
|
"timeoutBaseMs",
|
|
5867
5793
|
"timeoutBufferMs",
|
|
5868
|
-
"suiteIndex"
|
|
5869
|
-
"farmSessionUrl",
|
|
5870
|
-
"runDevice"
|
|
5794
|
+
"suiteIndex"
|
|
5871
5795
|
]);
|
|
5872
5796
|
function sanitizeRunMetadataForResponse(metadata) {
|
|
5873
5797
|
if (metadata === null || metadata === void 0) return metadata ?? null;
|
|
@@ -5897,17 +5821,7 @@ var RESPONSE_SAFE_RUN_READ_METHODS = [
|
|
|
5897
5821
|
"getRunsByTest"
|
|
5898
5822
|
];
|
|
5899
5823
|
var RUN_READS_WITHOUT_ROWS = /* @__PURE__ */ new Set([
|
|
5900
|
-
//
|
|
5901
|
-
//
|
|
5902
|
-
// The five WRITES return nothing; they are here for the same reason `insertRun` is.
|
|
5903
|
-
//
|
|
5904
|
-
// `readRunDetailRows` IS A READ AND IS STILL HERE, and that is the judgement this set exists to
|
|
5905
|
-
// make explicit rather than to hide. It returns members, steps and sub-actions — the bus
|
|
5906
|
-
// payloads as the subscriber MASKED them (T-363-05) — and NO `RunRow` and no
|
|
5907
|
-
// `RunArtifactRow`. It therefore carries none of the run metadata the 401-23 scrub law exists
|
|
5908
|
-
// to strip, so wrapping it would be wrapping a shape that has nothing to strip. This router
|
|
5909
|
-
// does not call it either: the detail arm dispatches through the injected host, which is the
|
|
5910
|
-
// whole of D-03.
|
|
5824
|
+
// `readRunDetailRows` returns masked bus payloads and no run row, so it has no metadata to strip.
|
|
5911
5825
|
"deleteMemberProjection",
|
|
5912
5826
|
"incrementRunMemberAttempts",
|
|
5913
5827
|
"insertRunStep",
|
|
@@ -5918,23 +5832,23 @@ var RUN_READS_WITHOUT_ROWS = /* @__PURE__ */ new Set([
|
|
|
5918
5832
|
"finalizeRunArtifact",
|
|
5919
5833
|
"getRunsBySuiteIdCount",
|
|
5920
5834
|
"getRunsByTestCount",
|
|
5921
|
-
//
|
|
5835
|
+
// Run ids and a count, no row; each id is dispatched through the agent host.
|
|
5922
5836
|
"listInsightsWindowRunIds",
|
|
5923
5837
|
"insertPendingRun",
|
|
5924
5838
|
"insertRun",
|
|
5925
5839
|
"insertRunArtifact",
|
|
5926
5840
|
"listRunAttributeKeys",
|
|
5927
5841
|
"listRunAttributeValues",
|
|
5928
|
-
//
|
|
5842
|
+
// An owner (pid, incarnation, host), no run metadata.
|
|
5929
5843
|
"readRunOwner",
|
|
5930
|
-
//
|
|
5844
|
+
// Ids and owners only, no run metadata.
|
|
5931
5845
|
"listRunningRunOwners",
|
|
5932
|
-
//
|
|
5846
|
+
// Compare-and-set writes that return a boolean.
|
|
5933
5847
|
"finalizeOrphanRun",
|
|
5934
5848
|
"finalizeOwnRun",
|
|
5935
|
-
//
|
|
5849
|
+
// A compare-and-set write that returns a boolean, no run metadata.
|
|
5936
5850
|
"closeOwnRun",
|
|
5937
|
-
//
|
|
5851
|
+
// A compare-and-set write that returns a boolean, no run metadata.
|
|
5938
5852
|
"claimRunningRow",
|
|
5939
5853
|
"stageRunArtifact",
|
|
5940
5854
|
"updateRun",
|
|
@@ -6005,10 +5919,7 @@ function withResponseSafeRunReads(db) {
|
|
|
6005
5919
|
// `sanitizeArtifactForResponse`; a caller reaching for the bare row has no such step, so the
|
|
6006
5920
|
// projection is its only choke point. Same allowlist, so the two cannot drift.
|
|
6007
5921
|
getRunArtifact: { value: (runId) => scrubArtifact(db.getRunArtifact(runId)) },
|
|
6008
|
-
//
|
|
6009
|
-
// parent, whose `run` rows this override scrubbed; nothing has written a child row since
|
|
6010
|
-
// 402-09, and the shape went with the column. `getRunArtifact` above remains the choke point
|
|
6011
|
-
// for the payload itself, and this wrapper delegates to it.
|
|
5922
|
+
// One artifact; `getRunArtifact` is the choke point for its payload, and this delegates to it.
|
|
6012
5923
|
getRunArtifactBundle: {
|
|
6013
5924
|
value: (runId) => ({ artifact: scrubArtifact(db.getRunArtifactBundle(runId).artifact) })
|
|
6014
5925
|
}
|
|
@@ -6156,10 +6067,18 @@ function createRouter(dbOrDeps) {
|
|
|
6156
6067
|
json(res, { error: "Bad Request" }, 400);
|
|
6157
6068
|
return;
|
|
6158
6069
|
}
|
|
6070
|
+
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
6071
|
+
json(res, { error: "cross_site_refused" }, 403);
|
|
6072
|
+
return;
|
|
6073
|
+
}
|
|
6074
|
+
if (carriesNonJsonBody(req)) {
|
|
6075
|
+
json(res, { error: "unsupported_media_type" }, 415);
|
|
6076
|
+
return;
|
|
6077
|
+
}
|
|
6159
6078
|
const path = url.pathname;
|
|
6160
6079
|
if (path === "/api/config" && req.method === "PATCH") {
|
|
6161
6080
|
if (!configManager) {
|
|
6162
|
-
|
|
6081
|
+
json(res, { error: "config_unavailable" }, 503);
|
|
6163
6082
|
return;
|
|
6164
6083
|
}
|
|
6165
6084
|
registerConfigWriteRoute(req, res, { configManager, undoStore });
|
|
@@ -6179,7 +6098,7 @@ function createRouter(dbOrDeps) {
|
|
|
6179
6098
|
}
|
|
6180
6099
|
if (path === "/api/config/revision" && req.method === "GET") {
|
|
6181
6100
|
if (!configManager) {
|
|
6182
|
-
|
|
6101
|
+
json(res, { error: "config_unavailable" }, 503);
|
|
6183
6102
|
return;
|
|
6184
6103
|
}
|
|
6185
6104
|
registerConfigRevisionRoute(req, res, { configManager });
|
|
@@ -6187,7 +6106,7 @@ function createRouter(dbOrDeps) {
|
|
|
6187
6106
|
}
|
|
6188
6107
|
if (path === "/api/llm/connect" && req.method === "POST") {
|
|
6189
6108
|
if (!configManager) {
|
|
6190
|
-
|
|
6109
|
+
json(res, { error: "config_unavailable" }, 503);
|
|
6191
6110
|
return;
|
|
6192
6111
|
}
|
|
6193
6112
|
registerLlmConnectRoute(req, res, {
|
|
@@ -6199,16 +6118,6 @@ function createRouter(dbOrDeps) {
|
|
|
6199
6118
|
});
|
|
6200
6119
|
return;
|
|
6201
6120
|
}
|
|
6202
|
-
if (path === "/api/runs/trigger" || path === "/api/queue/enqueue") {
|
|
6203
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
6204
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
6205
|
-
return;
|
|
6206
|
-
}
|
|
6207
|
-
}
|
|
6208
|
-
if (req.method === "OPTIONS") {
|
|
6209
|
-
cors(res);
|
|
6210
|
-
return;
|
|
6211
|
-
}
|
|
6212
6121
|
if (path === "/api/auth-states" && req.method === "GET") {
|
|
6213
6122
|
if (!configManager || !deps.configPath) {
|
|
6214
6123
|
json(res, { error: "Auth-state metadata not available" }, 503);
|
|
@@ -6232,11 +6141,10 @@ function createRouter(dbOrDeps) {
|
|
|
6232
6141
|
return;
|
|
6233
6142
|
}
|
|
6234
6143
|
if (path === "/api/runs" && req.method === "GET") {
|
|
6235
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
6236
6144
|
const statuses = url.searchParams.getAll("status");
|
|
6237
6145
|
const unknownStatus = statuses.find((word) => !isRunStatusWord(word));
|
|
6238
6146
|
if (unknownStatus !== void 0) {
|
|
6239
|
-
|
|
6147
|
+
json(res, { error: `Unknown run status: ${unknownStatus}.` }, 400);
|
|
6240
6148
|
return;
|
|
6241
6149
|
}
|
|
6242
6150
|
const name = url.searchParams.get("name") ?? void 0;
|
|
@@ -6268,12 +6176,12 @@ function createRouter(dbOrDeps) {
|
|
|
6268
6176
|
)).sort((a, b) => a.localeCompare(b));
|
|
6269
6177
|
const filtered = platformFilteredRuns.filter((run) => matchesTargetFilter(run, target));
|
|
6270
6178
|
const runs = filtered.slice(offset, offset + limit);
|
|
6271
|
-
|
|
6179
|
+
json(res, {
|
|
6272
6180
|
runs,
|
|
6273
6181
|
total: filtered.length,
|
|
6274
6182
|
targets: targetOptions
|
|
6275
6183
|
});
|
|
6276
|
-
}).catch(() =>
|
|
6184
|
+
}).catch(() => json(res, { error: "Failed to list runs" }, 500));
|
|
6277
6185
|
return;
|
|
6278
6186
|
}
|
|
6279
6187
|
if (path === "/api/runs/attributes/keys" && req.method === "GET") {
|
|
@@ -6311,7 +6219,6 @@ function createRouter(dbOrDeps) {
|
|
|
6311
6219
|
}
|
|
6312
6220
|
const runArtifactMatch = path.match(/^\/api\/runs\/([^/]+)\/artifact$/);
|
|
6313
6221
|
if (runArtifactMatch && req.method === "GET") {
|
|
6314
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
6315
6222
|
const runId = decodeURIComponent(runArtifactMatch[1]);
|
|
6316
6223
|
const run = db.getRun(runId);
|
|
6317
6224
|
if (!run) {
|
|
@@ -6319,7 +6226,7 @@ function createRouter(dbOrDeps) {
|
|
|
6319
6226
|
return;
|
|
6320
6227
|
}
|
|
6321
6228
|
const bundle = db.getRunArtifactBundle(runId);
|
|
6322
|
-
|
|
6229
|
+
json(res, {
|
|
6323
6230
|
run,
|
|
6324
6231
|
artifact: sanitizeArtifactForResponse(bundle.artifact),
|
|
6325
6232
|
missingSections: getArtifactMissingSections(bundle.artifact)
|
|
@@ -6327,7 +6234,6 @@ function createRouter(dbOrDeps) {
|
|
|
6327
6234
|
return;
|
|
6328
6235
|
}
|
|
6329
6236
|
if (path.startsWith("/api/runs/") && path.endsWith("/execution-logs") && req.method === "GET") {
|
|
6330
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
6331
6237
|
const segments = path.split("/");
|
|
6332
6238
|
const id = segments[3];
|
|
6333
6239
|
const run = db.getRun(id);
|
|
@@ -6338,37 +6244,37 @@ function createRouter(dbOrDeps) {
|
|
|
6338
6244
|
const stepId = url.searchParams.get("stepId") ?? void 0;
|
|
6339
6245
|
const type = url.searchParams.get("type") ?? void 0;
|
|
6340
6246
|
const logs = db.getExecutionLogs({ runId: id, stepId, type });
|
|
6341
|
-
|
|
6247
|
+
json(res, { logs });
|
|
6342
6248
|
return;
|
|
6343
6249
|
}
|
|
6344
6250
|
if (path.startsWith("/api/runs/") && path.endsWith("/mark") && req.method === "POST") {
|
|
6345
6251
|
if (deps.agentHost === void 0) {
|
|
6346
|
-
|
|
6252
|
+
json(res, { error: "agent_host_unavailable" }, 503);
|
|
6347
6253
|
return;
|
|
6348
6254
|
}
|
|
6349
6255
|
let runId;
|
|
6350
6256
|
try {
|
|
6351
6257
|
runId = decodeURIComponent(path.slice("/api/runs/".length, -"/mark".length));
|
|
6352
6258
|
} catch {
|
|
6353
|
-
|
|
6259
|
+
json(res, { error: "invalid_mark_input" }, 400);
|
|
6354
6260
|
return;
|
|
6355
6261
|
}
|
|
6356
|
-
registerRunMarkRoute(req, res, { host: deps.agentHost, runId
|
|
6262
|
+
registerRunMarkRoute(req, res, { host: deps.agentHost, runId });
|
|
6357
6263
|
return;
|
|
6358
6264
|
}
|
|
6359
6265
|
if (req.method === "POST" && /^\/api\/runs\/[^/]+\/mark-preview$/.test(path)) {
|
|
6360
6266
|
if (deps.agentHost === void 0) {
|
|
6361
|
-
|
|
6267
|
+
json(res, { error: "agent_host_unavailable" }, 503);
|
|
6362
6268
|
return;
|
|
6363
6269
|
}
|
|
6364
6270
|
let runId;
|
|
6365
6271
|
try {
|
|
6366
6272
|
runId = decodeURIComponent(path.slice("/api/runs/".length, -"/mark-preview".length));
|
|
6367
6273
|
} catch {
|
|
6368
|
-
|
|
6274
|
+
json(res, { error: "invalid_run_id" }, 400);
|
|
6369
6275
|
return;
|
|
6370
6276
|
}
|
|
6371
|
-
serveRunMarkPreviewRead(req, res, { runId, host: deps.agentHost
|
|
6277
|
+
serveRunMarkPreviewRead(req, res, { runId, host: deps.agentHost });
|
|
6372
6278
|
return;
|
|
6373
6279
|
}
|
|
6374
6280
|
if (path.startsWith("/api/runs/") && path.endsWith("/cancel") && req.method === "POST") {
|
|
@@ -6447,44 +6353,39 @@ function createRouter(dbOrDeps) {
|
|
|
6447
6353
|
return;
|
|
6448
6354
|
}
|
|
6449
6355
|
if (req.method === "GET" && /^\/api\/runs\/[^/]+\/live-url$/.test(path)) {
|
|
6450
|
-
const crossSite = refuseCrossSiteRequest(req);
|
|
6451
|
-
if (crossSite !== void 0) {
|
|
6452
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
6453
|
-
return;
|
|
6454
|
-
}
|
|
6455
6356
|
if (!deps.mintLiveUrl) {
|
|
6456
|
-
|
|
6357
|
+
json(res, { error: "live_view_unavailable" }, 503);
|
|
6457
6358
|
return;
|
|
6458
6359
|
}
|
|
6459
6360
|
let liveUrlRunId;
|
|
6460
6361
|
try {
|
|
6461
6362
|
liveUrlRunId = decodeURIComponent(path.slice("/api/runs/".length, -"/live-url".length));
|
|
6462
6363
|
} catch {
|
|
6463
|
-
|
|
6364
|
+
json(res, { error: "unknown_run" }, 404);
|
|
6464
6365
|
return;
|
|
6465
6366
|
}
|
|
6466
6367
|
const rawHandle = handleParamOf(req);
|
|
6467
6368
|
const parsedHandle = parseHandleParam2(rawHandle);
|
|
6468
6369
|
if (parsedHandle.kind === "invalid") {
|
|
6469
|
-
|
|
6370
|
+
json(res, { error: "invalid_handle" }, 400);
|
|
6470
6371
|
return;
|
|
6471
6372
|
}
|
|
6472
6373
|
const handle = parsedHandle.kind === "absent" ? 0 : parsedHandle.index;
|
|
6473
6374
|
void deps.mintLiveUrl(liveUrlRunId, handle).then(
|
|
6474
6375
|
(result) => {
|
|
6475
|
-
|
|
6376
|
+
json(res, result);
|
|
6476
6377
|
},
|
|
6477
6378
|
(error) => {
|
|
6478
6379
|
const name = error instanceof Error ? error.name : "";
|
|
6479
6380
|
if (name === "UnknownRunError") {
|
|
6480
|
-
|
|
6381
|
+
json(res, { error: "unknown_run" }, 404);
|
|
6481
6382
|
return;
|
|
6482
6383
|
}
|
|
6483
6384
|
if (name === "LiveViewUnavailableError") {
|
|
6484
|
-
|
|
6385
|
+
json(res, { error: "live_view_unavailable" }, 503);
|
|
6485
6386
|
return;
|
|
6486
6387
|
}
|
|
6487
|
-
|
|
6388
|
+
json(res, { error: "live_url_mint_failed" }, 500);
|
|
6488
6389
|
}
|
|
6489
6390
|
);
|
|
6490
6391
|
return;
|
|
@@ -6513,32 +6414,32 @@ function createRouter(dbOrDeps) {
|
|
|
6513
6414
|
}
|
|
6514
6415
|
if (req.method === "GET" && /^\/api\/runs\/[^/]+\/evidence$/.test(path)) {
|
|
6515
6416
|
if (!deps.agentHost) {
|
|
6516
|
-
|
|
6417
|
+
json(res, { error: "agent_host_unavailable" }, 503);
|
|
6517
6418
|
return;
|
|
6518
6419
|
}
|
|
6519
6420
|
let runId;
|
|
6520
6421
|
try {
|
|
6521
6422
|
runId = decodeURIComponent(path.slice("/api/runs/".length, -"/evidence".length));
|
|
6522
6423
|
} catch {
|
|
6523
|
-
|
|
6424
|
+
json(res, { error: "invalid_run_id" }, 400);
|
|
6524
6425
|
return;
|
|
6525
6426
|
}
|
|
6526
|
-
serveRunEvidenceRead(req, res, { runId, host: deps.agentHost
|
|
6427
|
+
serveRunEvidenceRead(req, res, { runId, host: deps.agentHost });
|
|
6527
6428
|
return;
|
|
6528
6429
|
}
|
|
6529
6430
|
if (req.method === "GET" && /^\/api\/runs\/[^/]+\/marks$/.test(path)) {
|
|
6530
6431
|
if (!deps.agentHost) {
|
|
6531
|
-
|
|
6432
|
+
json(res, { error: "agent_host_unavailable" }, 503);
|
|
6532
6433
|
return;
|
|
6533
6434
|
}
|
|
6534
6435
|
let runId;
|
|
6535
6436
|
try {
|
|
6536
6437
|
runId = decodeURIComponent(path.slice("/api/runs/".length, -"/marks".length));
|
|
6537
6438
|
} catch {
|
|
6538
|
-
|
|
6439
|
+
json(res, { error: "invalid_run_id" }, 400);
|
|
6539
6440
|
return;
|
|
6540
6441
|
}
|
|
6541
|
-
serveRunMarksHistoryRead(req, res, { runId, host: deps.agentHost
|
|
6442
|
+
serveRunMarksHistoryRead(req, res, { runId, host: deps.agentHost });
|
|
6542
6443
|
return;
|
|
6543
6444
|
}
|
|
6544
6445
|
if (req.method === "GET" && /^\/api\/runs\/[^/]+\/detail$/.test(path)) {
|
|
@@ -6553,7 +6454,7 @@ function createRouter(dbOrDeps) {
|
|
|
6553
6454
|
json(res, { error: "invalid_run_id" }, 400);
|
|
6554
6455
|
return;
|
|
6555
6456
|
}
|
|
6556
|
-
serveRunDetailRead(req, res, { runId: detailRunId, host: deps.agentHost
|
|
6457
|
+
serveRunDetailRead(req, res, { runId: detailRunId, host: deps.agentHost });
|
|
6557
6458
|
return;
|
|
6558
6459
|
}
|
|
6559
6460
|
if (req.method === "GET" && /^\/api\/runs\/[^/]+\/analysis$/.test(path)) {
|
|
@@ -6568,7 +6469,7 @@ function createRouter(dbOrDeps) {
|
|
|
6568
6469
|
json(res, { error: "invalid_run_id" }, 400);
|
|
6569
6470
|
return;
|
|
6570
6471
|
}
|
|
6571
|
-
serveRunAnalysisRead(req, res, { runId: analysisRunId, host: deps.agentHost
|
|
6472
|
+
serveRunAnalysisRead(req, res, { runId: analysisRunId, host: deps.agentHost });
|
|
6572
6473
|
return;
|
|
6573
6474
|
}
|
|
6574
6475
|
if (req.method === "GET" && /^\/api\/runs\/[^/]+\/knowledge$/.test(path)) {
|
|
@@ -6585,8 +6486,7 @@ function createRouter(dbOrDeps) {
|
|
|
6585
6486
|
}
|
|
6586
6487
|
serveRunKnowledgeRead(req, res, {
|
|
6587
6488
|
runId: knowledgeRunId,
|
|
6588
|
-
host: deps.agentHost
|
|
6589
|
-
boundary: "human"
|
|
6489
|
+
host: deps.agentHost
|
|
6590
6490
|
});
|
|
6591
6491
|
return;
|
|
6592
6492
|
}
|
|
@@ -6636,14 +6536,13 @@ function createRouter(dbOrDeps) {
|
|
|
6636
6536
|
notFound(res);
|
|
6637
6537
|
return;
|
|
6638
6538
|
}
|
|
6639
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
6640
6539
|
const id = segments[3];
|
|
6641
6540
|
const run = db.getRun(id);
|
|
6642
6541
|
if (!run) {
|
|
6643
6542
|
notFound(res, "Run not found");
|
|
6644
6543
|
return;
|
|
6645
6544
|
}
|
|
6646
|
-
|
|
6545
|
+
json(res, { run });
|
|
6647
6546
|
return;
|
|
6648
6547
|
}
|
|
6649
6548
|
if (path === "/api/stats/costs" && req.method === "GET") {
|
|
@@ -6654,19 +6553,18 @@ function createRouter(dbOrDeps) {
|
|
|
6654
6553
|
return;
|
|
6655
6554
|
}
|
|
6656
6555
|
if (path === "/api/insights/knowledge-changes" && req.method === "GET") {
|
|
6657
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
6658
6556
|
if (!deps.agentHost) {
|
|
6659
|
-
|
|
6557
|
+
json(res, { status: "unavailable", reason: "agent_host_unavailable" }, 503);
|
|
6660
6558
|
return;
|
|
6661
6559
|
}
|
|
6662
6560
|
const from = url.searchParams.get("from") ?? void 0;
|
|
6663
6561
|
const to = url.searchParams.get("to") ?? void 0;
|
|
6664
6562
|
void readInsightsKnowledgeChanges(deps.agentHost, db, { from, to }).then(
|
|
6665
6563
|
(body) => {
|
|
6666
|
-
|
|
6564
|
+
json(res, body);
|
|
6667
6565
|
},
|
|
6668
6566
|
() => {
|
|
6669
|
-
|
|
6567
|
+
json(res, { status: "unavailable", reason: "read_failed" }, 500);
|
|
6670
6568
|
}
|
|
6671
6569
|
);
|
|
6672
6570
|
return;
|
|
@@ -6732,10 +6630,7 @@ function createRouter(dbOrDeps) {
|
|
|
6732
6630
|
serveRunEventStream(req, res, {
|
|
6733
6631
|
runId,
|
|
6734
6632
|
handleParam: handleParamOf(req),
|
|
6735
|
-
//
|
|
6736
|
-
// it from. `url.searchParams.get('lastEventId')` would resolve a repeated parameter to its
|
|
6737
|
-
// first value — the one answer `lastEventIdParamOf` exists to refuse — and a second
|
|
6738
|
-
// header/query precedence rule here would be a second contract for where a client resumes.
|
|
6633
|
+
// Read from the same pair as the authed door; `searchParams.get` takes a repeated first.
|
|
6739
6634
|
resume: resumePointOf(req.headers["last-event-id"], lastEventIdParamOf(req)),
|
|
6740
6635
|
attach: deps.runServiceAttach
|
|
6741
6636
|
});
|
|
@@ -6777,7 +6672,6 @@ function createRouter(dbOrDeps) {
|
|
|
6777
6672
|
try {
|
|
6778
6673
|
attributes = buildRunRequestAttributes({
|
|
6779
6674
|
trigger: "api",
|
|
6780
|
-
runner: "local",
|
|
6781
6675
|
userAttributes: body.attributes
|
|
6782
6676
|
});
|
|
6783
6677
|
} catch (err) {
|
|
@@ -6785,25 +6679,10 @@ function createRouter(dbOrDeps) {
|
|
|
6785
6679
|
return;
|
|
6786
6680
|
}
|
|
6787
6681
|
const args = normalizedFileTarget ? [normalizedFileTarget.executionPath] : [];
|
|
6788
|
-
const source = isSuiteFile ? "suite" : "dashboard";
|
|
6789
|
-
try {
|
|
6790
|
-
await executionBackend?.preflight?.({ args, source });
|
|
6791
|
-
} catch (error) {
|
|
6792
|
-
const denial = runDenialOf(error);
|
|
6793
|
-
if (denial !== void 0) {
|
|
6794
|
-
json(res, denial, 403);
|
|
6795
|
-
return;
|
|
6796
|
-
}
|
|
6797
|
-
json(res, { error: error instanceof Error ? error.message : String(error) }, 400);
|
|
6798
|
-
return;
|
|
6799
|
-
}
|
|
6800
6682
|
const runId = jobQueue.enqueue({
|
|
6801
6683
|
name: body.name,
|
|
6802
6684
|
filePath: normalizedFileTarget?.storagePath,
|
|
6803
|
-
//
|
|
6804
|
-
// with member handles, so a parent artifact kind would claim parenthood over rows
|
|
6805
|
-
// nothing creates. `isSuiteFile` still travels in `metadata.isSuite` below, and the run's
|
|
6806
|
-
// own `suite_id` plus the artifact's `source.kind` snapshot still say it is a suite.
|
|
6685
|
+
// No parent kind is minted; `metadata.isSuite` and `suite_id` say it is a suite.
|
|
6807
6686
|
kind: "test",
|
|
6808
6687
|
attributes,
|
|
6809
6688
|
priority: body.priority,
|
|
@@ -6886,17 +6765,6 @@ function createRouter(dbOrDeps) {
|
|
|
6886
6765
|
args.push("--disable-cache");
|
|
6887
6766
|
}
|
|
6888
6767
|
const isSuite = Boolean(isSuiteFile);
|
|
6889
|
-
try {
|
|
6890
|
-
await executionBackend?.preflight?.({ args, source: isSuite ? "suite" : "dashboard" });
|
|
6891
|
-
} catch (error) {
|
|
6892
|
-
const denial = runDenialOf(error);
|
|
6893
|
-
if (denial !== void 0) {
|
|
6894
|
-
json(res, denial, 403);
|
|
6895
|
-
return;
|
|
6896
|
-
}
|
|
6897
|
-
json(res, { error: error instanceof Error ? error.message : String(error) }, 400);
|
|
6898
|
-
return;
|
|
6899
|
-
}
|
|
6900
6768
|
let testName = body.file ? basename5(normalizedFileTarget?.storagePath ?? body.file, ".yaml") : "unknown";
|
|
6901
6769
|
let fileParallel;
|
|
6902
6770
|
let platform;
|
|
@@ -6939,11 +6807,10 @@ function createRouter(dbOrDeps) {
|
|
|
6939
6807
|
const runId = jobQueue.enqueue({
|
|
6940
6808
|
name: testName,
|
|
6941
6809
|
filePath: normalizedFileTarget?.storagePath ?? body.file,
|
|
6942
|
-
//
|
|
6810
|
+
// No parent kind is minted; see the enqueue route above.
|
|
6943
6811
|
kind: "test",
|
|
6944
6812
|
attributes: buildRunRequestAttributes({
|
|
6945
|
-
trigger: triggerSource.trigger
|
|
6946
|
-
runner: body.local === false ? "browserstack" : "local"
|
|
6813
|
+
trigger: triggerSource.trigger
|
|
6947
6814
|
}),
|
|
6948
6815
|
platform,
|
|
6949
6816
|
parallel: effectiveParallel,
|
|
@@ -7452,7 +7319,7 @@ function createRouter(dbOrDeps) {
|
|
|
7452
7319
|
return;
|
|
7453
7320
|
}
|
|
7454
7321
|
if (path === "/api/app-metadata" && req.method === "GET") {
|
|
7455
|
-
const version =
|
|
7322
|
+
const version = getAgentQaVersion2().trim() || "0.0.0";
|
|
7456
7323
|
(async () => {
|
|
7457
7324
|
try {
|
|
7458
7325
|
const status = await getAgentQaUpdateStatus();
|
|
@@ -7496,9 +7363,8 @@ function createRouter(dbOrDeps) {
|
|
|
7496
7363
|
return;
|
|
7497
7364
|
}
|
|
7498
7365
|
if (path === "/api/config" && req.method === "GET") {
|
|
7499
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
7500
7366
|
if (!configManager) {
|
|
7501
|
-
|
|
7367
|
+
json(res, { error: "Config management not available" }, 501);
|
|
7502
7368
|
return;
|
|
7503
7369
|
}
|
|
7504
7370
|
;
|
|
@@ -7506,7 +7372,7 @@ function createRouter(dbOrDeps) {
|
|
|
7506
7372
|
try {
|
|
7507
7373
|
const read = await configManager.readMasked();
|
|
7508
7374
|
if (read.kind === "file_invalid") {
|
|
7509
|
-
|
|
7375
|
+
json(res, { error: "file_invalid", message: read.message }, 409);
|
|
7510
7376
|
return;
|
|
7511
7377
|
}
|
|
7512
7378
|
const { config, revision, lines, fileIssues } = read;
|
|
@@ -7516,7 +7382,7 @@ function createRouter(dbOrDeps) {
|
|
|
7516
7382
|
const activeLlm = use?.llm ?? config.defaultLLM;
|
|
7517
7383
|
const defaultCfg = Array.isArray(llms) ? llms.find((c) => c?.name === activeLlm) : void 0;
|
|
7518
7384
|
const provider = defaultCfg?.provider ?? null;
|
|
7519
|
-
|
|
7385
|
+
json(res, {
|
|
7520
7386
|
config,
|
|
7521
7387
|
provider,
|
|
7522
7388
|
revision,
|
|
@@ -7524,45 +7390,17 @@ function createRouter(dbOrDeps) {
|
|
|
7524
7390
|
...fileIssues.length > 0 ? { fileIssues } : {}
|
|
7525
7391
|
});
|
|
7526
7392
|
} catch (err) {
|
|
7527
|
-
|
|
7393
|
+
json(res, { error: err instanceof Error ? err.message : "Failed to read config" }, 500);
|
|
7528
7394
|
}
|
|
7529
7395
|
})();
|
|
7530
7396
|
return;
|
|
7531
7397
|
}
|
|
7532
|
-
if (path === "/api/config/farm/test-connection" && req.method === "POST") {
|
|
7533
|
-
readJsonBody(req).then(async (body) => {
|
|
7534
|
-
if (body.provider !== "browserstack") {
|
|
7535
|
-
json(res, { error: "Only BrowserStack is currently supported" }, 400);
|
|
7536
|
-
return;
|
|
7537
|
-
}
|
|
7538
|
-
try {
|
|
7539
|
-
const auth = Buffer.from(`${body.username}:${body.accessKey}`).toString("base64");
|
|
7540
|
-
const response = await fetch("https://api-cloud.browserstack.com/app-automate/devices.json", {
|
|
7541
|
-
headers: { Authorization: `Basic ${auth}` }
|
|
7542
|
-
});
|
|
7543
|
-
if (response.ok) {
|
|
7544
|
-
json(res, { success: true });
|
|
7545
|
-
} else if (response.status === 401) {
|
|
7546
|
-
json(res, { success: false, error: "Invalid credentials" });
|
|
7547
|
-
} else {
|
|
7548
|
-
json(res, { success: false, error: `BrowserStack API returned ${response.status}` });
|
|
7549
|
-
}
|
|
7550
|
-
} catch (err) {
|
|
7551
|
-
json(res, { success: false, error: err instanceof Error ? err.message : "Connection failed" });
|
|
7552
|
-
}
|
|
7553
|
-
}).catch(() => json(res, { error: "Invalid request body" }, 400));
|
|
7554
|
-
return;
|
|
7555
|
-
}
|
|
7556
7398
|
if (path.startsWith("/api/auth/") && req.method === "DELETE") {
|
|
7557
7399
|
const segments = path.split("/");
|
|
7558
7400
|
if (segments.length === 4 && segments[2] === "auth") {
|
|
7559
7401
|
const providerName = decodeURIComponent(segments[3]);
|
|
7560
7402
|
if (providerName === "status" || providerName === "credential" || providerName === "oauth") {
|
|
7561
7403
|
} else {
|
|
7562
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
7563
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
7564
|
-
return;
|
|
7565
|
-
}
|
|
7566
7404
|
;
|
|
7567
7405
|
(async () => {
|
|
7568
7406
|
try {
|
|
@@ -7573,13 +7411,13 @@ function createRouter(dbOrDeps) {
|
|
|
7573
7411
|
return credential;
|
|
7574
7412
|
});
|
|
7575
7413
|
if (removed === void 0) {
|
|
7576
|
-
|
|
7414
|
+
json(res, { deleted: false });
|
|
7577
7415
|
return;
|
|
7578
7416
|
}
|
|
7579
7417
|
const undo = undoStore.put({ kind: "credential", name: providerName, credential: removed }, Date.now());
|
|
7580
|
-
|
|
7418
|
+
json(res, { deleted: true, undo });
|
|
7581
7419
|
} catch (err) {
|
|
7582
|
-
|
|
7420
|
+
json(res, { error: err instanceof Error ? err.message : "Failed to delete credential" }, 500);
|
|
7583
7421
|
}
|
|
7584
7422
|
})();
|
|
7585
7423
|
return;
|
|
@@ -7689,27 +7527,21 @@ function createRouter(dbOrDeps) {
|
|
|
7689
7527
|
const segments = path.split("/");
|
|
7690
7528
|
const provider = segments.length >= 6 ? decodeURIComponent(segments[4] ?? "") : "";
|
|
7691
7529
|
const action = segments.length >= 6 ? segments[5] : "";
|
|
7692
|
-
const authDoor = req.method === "POST" && (action === "start" || action === "exchange");
|
|
7693
|
-
if (authDoor && refuseCrossSiteRequest(req) !== void 0) {
|
|
7694
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
7695
|
-
return;
|
|
7696
|
-
}
|
|
7697
7530
|
const plugin = provider ? getLLMAuthProviderPlugin2(provider) : void 0;
|
|
7698
7531
|
if (!plugin) {
|
|
7699
|
-
;
|
|
7700
|
-
(authDoor ? jsonNoCors : json)(res, { error: `Auth plugin provider "${provider}" is not registered` }, 404);
|
|
7532
|
+
json(res, { error: `Auth plugin provider "${provider}" is not registered` }, 404);
|
|
7701
7533
|
return;
|
|
7702
7534
|
}
|
|
7703
7535
|
if (action === "start" && req.method === "POST") {
|
|
7704
7536
|
readJsonBody(req).then(async (body) => {
|
|
7705
7537
|
if (!plugin.startAuth) {
|
|
7706
|
-
|
|
7538
|
+
json(res, { error: `Provider "${provider}" does not support dashboard auth start` }, 400);
|
|
7707
7539
|
return;
|
|
7708
7540
|
}
|
|
7709
7541
|
try {
|
|
7710
7542
|
const target = await requirePluginAuthConfig(configManager, body.configName, plugin);
|
|
7711
7543
|
if (!target.ok) {
|
|
7712
|
-
|
|
7544
|
+
json(res, credentialRefusalBody(target), target.status);
|
|
7713
7545
|
return;
|
|
7714
7546
|
}
|
|
7715
7547
|
const started = await plugin.startAuth({
|
|
@@ -7745,16 +7577,16 @@ function createRouter(dbOrDeps) {
|
|
|
7745
7577
|
session.error = err instanceof Error ? err.message : "Authentication failed";
|
|
7746
7578
|
}).finally(() => cleanupPluginOAuthSession(session));
|
|
7747
7579
|
}
|
|
7748
|
-
|
|
7580
|
+
json(res, {
|
|
7749
7581
|
authorizeUrl: started.authorizeUrl,
|
|
7750
7582
|
sessionId,
|
|
7751
7583
|
mode: plugin.dashboardAuth.mode,
|
|
7752
7584
|
created: target.created
|
|
7753
7585
|
});
|
|
7754
7586
|
} catch (err) {
|
|
7755
|
-
|
|
7587
|
+
json(res, { error: err instanceof Error ? err.message : "Failed to start auth plugin flow" }, 500);
|
|
7756
7588
|
}
|
|
7757
|
-
}).catch(() =>
|
|
7589
|
+
}).catch(() => json(res, { error: "Invalid request body" }, 400));
|
|
7758
7590
|
return;
|
|
7759
7591
|
}
|
|
7760
7592
|
if (action === "result" && req.method === "GET") {
|
|
@@ -7782,22 +7614,22 @@ function createRouter(dbOrDeps) {
|
|
|
7782
7614
|
if (action === "exchange" && req.method === "POST") {
|
|
7783
7615
|
readJsonBody(req).then(async (body) => {
|
|
7784
7616
|
if (!plugin.exchangeCode) {
|
|
7785
|
-
|
|
7617
|
+
json(res, { error: `Provider "${provider}" does not support code exchange` }, 400);
|
|
7786
7618
|
return;
|
|
7787
7619
|
}
|
|
7788
7620
|
const sessionId = typeof body.sessionId === "string" ? body.sessionId.trim() : "";
|
|
7789
7621
|
const code = typeof body.code === "string" ? body.code.trim() : "";
|
|
7790
7622
|
if (!sessionId || !code) {
|
|
7791
|
-
|
|
7623
|
+
json(res, { error: "sessionId and code are required" }, 400);
|
|
7792
7624
|
return;
|
|
7793
7625
|
}
|
|
7794
7626
|
const session = pluginOAuthSessions.get(sessionId);
|
|
7795
7627
|
if (!session || session.providerId !== provider) {
|
|
7796
|
-
|
|
7628
|
+
json(res, { error: "Auth session not found" }, 404);
|
|
7797
7629
|
return;
|
|
7798
7630
|
}
|
|
7799
7631
|
if (session.status !== "pending") {
|
|
7800
|
-
|
|
7632
|
+
json(res, { error: "Auth session is no longer pending" }, 409);
|
|
7801
7633
|
return;
|
|
7802
7634
|
}
|
|
7803
7635
|
try {
|
|
@@ -7819,61 +7651,57 @@ function createRouter(dbOrDeps) {
|
|
|
7819
7651
|
session.error = CREDENTIAL_PROVIDER_CONFLICT.message;
|
|
7820
7652
|
cleanupPluginOAuthSession(session);
|
|
7821
7653
|
pluginOAuthSessions.delete(sessionId);
|
|
7822
|
-
|
|
7654
|
+
json(res, credentialRefusalBody(CREDENTIAL_PROVIDER_CONFLICT), CREDENTIAL_PROVIDER_CONFLICT.status);
|
|
7823
7655
|
return;
|
|
7824
7656
|
}
|
|
7825
7657
|
session.status = "completed";
|
|
7826
7658
|
cleanupPluginOAuthSession(session);
|
|
7827
7659
|
pluginOAuthSessions.delete(sessionId);
|
|
7828
|
-
|
|
7660
|
+
json(res, { status: "completed", saved: true });
|
|
7829
7661
|
} catch (err) {
|
|
7830
7662
|
session.status = "error";
|
|
7831
7663
|
session.error = err instanceof Error ? err.message : "Token exchange failed";
|
|
7832
7664
|
cleanupPluginOAuthSession(session);
|
|
7833
7665
|
pluginOAuthSessions.delete(sessionId);
|
|
7834
|
-
|
|
7666
|
+
json(res, { error: session.error }, 500);
|
|
7835
7667
|
}
|
|
7836
|
-
}).catch(() =>
|
|
7668
|
+
}).catch(() => json(res, { error: "Invalid request body" }, 400));
|
|
7837
7669
|
return;
|
|
7838
7670
|
}
|
|
7839
7671
|
json(res, { error: "Auth plugin route not found" }, 404);
|
|
7840
7672
|
return;
|
|
7841
7673
|
}
|
|
7842
7674
|
if (path === "/api/auth/credential" && req.method === "POST") {
|
|
7843
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
7844
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
7845
|
-
return;
|
|
7846
|
-
}
|
|
7847
7675
|
readJsonBody(req).then(async (body) => {
|
|
7848
7676
|
const provider = typeof body.provider === "string" ? body.provider.trim() : "";
|
|
7849
7677
|
const credentialType = typeof body.type === "string" ? body.type.trim() : "";
|
|
7850
7678
|
const secret = typeof body.secret === "string" ? body.secret.trim() : "";
|
|
7851
7679
|
if (typeof body.configName !== "string" || body.configName.trim() === "") {
|
|
7852
|
-
|
|
7680
|
+
json(res, { error: "configName is required" }, 400);
|
|
7853
7681
|
return;
|
|
7854
7682
|
}
|
|
7855
7683
|
if (!provider || !isKnownLLMProvider(provider)) {
|
|
7856
|
-
|
|
7684
|
+
json(res, { error: "valid provider is required" }, 400);
|
|
7857
7685
|
return;
|
|
7858
7686
|
}
|
|
7859
7687
|
if (credentialType !== "api-key" && credentialType !== "bearer-token") {
|
|
7860
|
-
|
|
7688
|
+
json(res, { error: "type must be api-key or bearer-token" }, 400);
|
|
7861
7689
|
return;
|
|
7862
7690
|
}
|
|
7863
7691
|
if (!secret) {
|
|
7864
|
-
|
|
7692
|
+
json(res, { error: "secret is required" }, 400);
|
|
7865
7693
|
return;
|
|
7866
7694
|
}
|
|
7867
7695
|
if (getLLMAuthProviderPlugin2(provider)) {
|
|
7868
|
-
|
|
7696
|
+
json(res, { error: "Subscription providers use OAuth login" }, 400);
|
|
7869
7697
|
return;
|
|
7870
7698
|
}
|
|
7871
7699
|
if (credentialType === "bearer-token" && provider !== "anthropic-compatible") {
|
|
7872
|
-
|
|
7700
|
+
json(res, { error: "bearer-token credentials are only supported for anthropic-compatible configs" }, 400);
|
|
7873
7701
|
return;
|
|
7874
7702
|
}
|
|
7875
7703
|
if (credentialType === "api-key" && !API_KEY_CREDENTIAL_PROVIDERS.has(provider)) {
|
|
7876
|
-
|
|
7704
|
+
json(res, { error: "api-key credentials are not supported for this provider" }, 400);
|
|
7877
7705
|
return;
|
|
7878
7706
|
}
|
|
7879
7707
|
try {
|
|
@@ -7888,35 +7716,31 @@ function createRouter(dbOrDeps) {
|
|
|
7888
7716
|
return checked;
|
|
7889
7717
|
});
|
|
7890
7718
|
if (!target.ok) {
|
|
7891
|
-
|
|
7719
|
+
json(res, credentialRefusalBody(target), target.status);
|
|
7892
7720
|
return;
|
|
7893
7721
|
}
|
|
7894
|
-
|
|
7722
|
+
json(res, { saved: true, created: target.created });
|
|
7895
7723
|
} catch (err) {
|
|
7896
|
-
|
|
7724
|
+
json(res, { error: err instanceof Error ? err.message : "Failed to save credential" }, 500);
|
|
7897
7725
|
}
|
|
7898
|
-
}).catch(() =>
|
|
7726
|
+
}).catch(() => json(res, { error: "Invalid request body" }, 400));
|
|
7899
7727
|
return;
|
|
7900
7728
|
}
|
|
7901
7729
|
if (path === "/api/auth/credential/move" && req.method === "POST") {
|
|
7902
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
7903
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
7904
|
-
return;
|
|
7905
|
-
}
|
|
7906
7730
|
readJsonBody(req).then(async (raw) => {
|
|
7907
7731
|
const parsed = LlmCredentialMoveRequestSchema.safeParse(raw);
|
|
7908
7732
|
if (!parsed.success) {
|
|
7909
|
-
|
|
7733
|
+
json(res, { error: "invalid_setup_input" }, 400);
|
|
7910
7734
|
return;
|
|
7911
7735
|
}
|
|
7912
7736
|
const from = checkCredentialName(parsed.data.from);
|
|
7913
7737
|
const to = checkCredentialName(parsed.data.to);
|
|
7914
7738
|
if (!from.ok || !to.ok) {
|
|
7915
|
-
|
|
7739
|
+
json(res, { error: "name_invalid" }, 400);
|
|
7916
7740
|
return;
|
|
7917
7741
|
}
|
|
7918
7742
|
if (!configManager) {
|
|
7919
|
-
|
|
7743
|
+
json(res, { error: "Config manager is required to move credentials" }, 400);
|
|
7920
7744
|
return;
|
|
7921
7745
|
}
|
|
7922
7746
|
try {
|
|
@@ -7934,25 +7758,21 @@ function createRouter(dbOrDeps) {
|
|
|
7934
7758
|
return void 0;
|
|
7935
7759
|
});
|
|
7936
7760
|
if (refusal) {
|
|
7937
|
-
|
|
7761
|
+
json(res, credentialRefusalBody(refusal), refusal.status);
|
|
7938
7762
|
return;
|
|
7939
7763
|
}
|
|
7940
|
-
|
|
7764
|
+
json(res, { moved: true });
|
|
7941
7765
|
} catch (err) {
|
|
7942
|
-
|
|
7766
|
+
json(res, { error: err instanceof Error ? err.message : "Failed to move credential" }, 500);
|
|
7943
7767
|
}
|
|
7944
|
-
}).catch(() =>
|
|
7768
|
+
}).catch(() => json(res, { error: "invalid_setup_input" }, 400));
|
|
7945
7769
|
return;
|
|
7946
7770
|
}
|
|
7947
7771
|
if (path === "/api/llm/test" && req.method === "POST") {
|
|
7948
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
7949
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
7950
|
-
return;
|
|
7951
|
-
}
|
|
7952
7772
|
readJsonBody(req).then(async (body) => {
|
|
7953
7773
|
const rawBody = body;
|
|
7954
7774
|
if ("apiKey" in rawBody) {
|
|
7955
|
-
|
|
7775
|
+
json(res, {
|
|
7956
7776
|
success: false,
|
|
7957
7777
|
error: "invalid_request",
|
|
7958
7778
|
message: "apiKey is not accepted in LLM test requests. Save credentials for the named config instead."
|
|
@@ -7962,7 +7782,7 @@ function createRouter(dbOrDeps) {
|
|
|
7962
7782
|
const providerName = typeof body.provider === "string" ? body.provider.trim() : "";
|
|
7963
7783
|
const modelName = typeof body.model === "string" ? body.model.trim() : "";
|
|
7964
7784
|
if (!providerName || !modelName) {
|
|
7965
|
-
|
|
7785
|
+
json(res, { success: false, error: "invalid_request", message: "provider and model are required" }, 400);
|
|
7966
7786
|
return;
|
|
7967
7787
|
}
|
|
7968
7788
|
const configName = typeof body.configName === "string" ? body.configName.trim() : "";
|
|
@@ -7987,7 +7807,7 @@ function createRouter(dbOrDeps) {
|
|
|
7987
7807
|
}
|
|
7988
7808
|
const parsedConfig = ModelConfigSchema2.safeParse(llmCandidate);
|
|
7989
7809
|
if (!parsedConfig.success) {
|
|
7990
|
-
|
|
7810
|
+
json(res, {
|
|
7991
7811
|
success: false,
|
|
7992
7812
|
error: "invalid_request",
|
|
7993
7813
|
message: parsedConfig.error.issues.map((issue) => `${issue.path.map(String).join(".") || "config"}: ${issue.message}`).join("; ")
|
|
@@ -7999,7 +7819,7 @@ function createRouter(dbOrDeps) {
|
|
|
7999
7819
|
const coreAuth = await import("@vostride/agent-qa-core");
|
|
8000
7820
|
const resolvedAuth = await coreAuth.resolveLLMAuth(configName, llmConfig2);
|
|
8001
7821
|
if (resolvedAuth.kind === "missing") {
|
|
8002
|
-
|
|
7822
|
+
json(res, {
|
|
8003
7823
|
success: false,
|
|
8004
7824
|
error: "missing_credential",
|
|
8005
7825
|
message: resolvedAuth.message
|
|
@@ -8028,7 +7848,7 @@ function createRouter(dbOrDeps) {
|
|
|
8028
7848
|
} finally {
|
|
8029
7849
|
clearTimeout(timeout);
|
|
8030
7850
|
}
|
|
8031
|
-
|
|
7851
|
+
json(res, {
|
|
8032
7852
|
success: true,
|
|
8033
7853
|
model: modelName,
|
|
8034
7854
|
provider: providerName,
|
|
@@ -8043,7 +7863,7 @@ function createRouter(dbOrDeps) {
|
|
|
8043
7863
|
const providerMessage = withDeclaredMask(declaredMask, (mask) => mask(unsavedMessage));
|
|
8044
7864
|
const errorClass = classifyProviderError2(providerMessage ?? unsavedMessage, statusCode);
|
|
8045
7865
|
const message = providerMessage === void 0 ? providerErrorSentence2(errorClass, "").trimEnd() : providerErrorSentence2(errorClass, providerMessage);
|
|
8046
|
-
|
|
7866
|
+
json(res, {
|
|
8047
7867
|
success: false,
|
|
8048
7868
|
error: errorClass,
|
|
8049
7869
|
message,
|
|
@@ -8053,7 +7873,7 @@ function createRouter(dbOrDeps) {
|
|
|
8053
7873
|
...unauthenticated ? { unauthenticated: true, authMessage } : {}
|
|
8054
7874
|
});
|
|
8055
7875
|
}
|
|
8056
|
-
}).catch(() =>
|
|
7876
|
+
}).catch(() => json(res, { success: false, error: "invalid_request", message: "Invalid request body" }, 400));
|
|
8057
7877
|
return;
|
|
8058
7878
|
}
|
|
8059
7879
|
if (path === "/api/cache/purge" && req.method === "POST") {
|
|
@@ -8062,56 +7882,15 @@ function createRouter(dbOrDeps) {
|
|
|
8062
7882
|
return;
|
|
8063
7883
|
}
|
|
8064
7884
|
readJsonBody(req).then(async (body) => {
|
|
8065
|
-
if (
|
|
8066
|
-
json(res, { error: "
|
|
7885
|
+
if (body.all !== true) {
|
|
7886
|
+
json(res, { error: "all is required" }, 400);
|
|
8067
7887
|
return;
|
|
8068
7888
|
}
|
|
8069
7889
|
try {
|
|
8070
7890
|
const cfg = await readConfigValues(configManager);
|
|
8071
|
-
const cacheDir = cfg.services?.cache?.dir ?? cfg.cache?.dir;
|
|
8072
|
-
if (!cacheDir) {
|
|
8073
|
-
json(res, { error: "Cache directory not configured" }, 400);
|
|
8074
|
-
return;
|
|
8075
|
-
}
|
|
8076
7891
|
const configDir = deps.configPath ? dirname7(resolve3(deps.configPath)) : process.cwd();
|
|
8077
|
-
const
|
|
8078
|
-
|
|
8079
|
-
try {
|
|
8080
|
-
branchDirs = await readdir(resolvedCacheDir);
|
|
8081
|
-
} catch (err) {
|
|
8082
|
-
if (err.code === "ENOENT") {
|
|
8083
|
-
json(res, { purged: 0 });
|
|
8084
|
-
return;
|
|
8085
|
-
}
|
|
8086
|
-
throw err;
|
|
8087
|
-
}
|
|
8088
|
-
let purged = 0;
|
|
8089
|
-
for (const branch of branchDirs) {
|
|
8090
|
-
const branchPath = join4(resolvedCacheDir, branch);
|
|
8091
|
-
let entryStat;
|
|
8092
|
-
try {
|
|
8093
|
-
entryStat = await stat5(branchPath);
|
|
8094
|
-
} catch {
|
|
8095
|
-
continue;
|
|
8096
|
-
}
|
|
8097
|
-
if (!entryStat.isDirectory()) {
|
|
8098
|
-
await rm5(branchPath, { force: true });
|
|
8099
|
-
continue;
|
|
8100
|
-
}
|
|
8101
|
-
let files;
|
|
8102
|
-
try {
|
|
8103
|
-
files = await readdir(branchPath);
|
|
8104
|
-
} catch {
|
|
8105
|
-
continue;
|
|
8106
|
-
}
|
|
8107
|
-
for (const file of files) {
|
|
8108
|
-
if (!file.endsWith(".json")) continue;
|
|
8109
|
-
await rm5(join4(branchPath, file), { force: true });
|
|
8110
|
-
purged++;
|
|
8111
|
-
}
|
|
8112
|
-
await rm5(branchPath, { recursive: true, force: true }).catch(() => {
|
|
8113
|
-
});
|
|
8114
|
-
}
|
|
7892
|
+
const cacheDir = cfg.services?.cache?.dir ?? DEFAULT_AGENT_QA_CACHE_DIR;
|
|
7893
|
+
const purged = await purgeActionCache(resolve3(configDir, cacheDir));
|
|
8115
7894
|
json(res, { purged });
|
|
8116
7895
|
} catch (err) {
|
|
8117
7896
|
json(res, { error: err instanceof Error ? err.message : "Cache purge failed" }, 500);
|
|
@@ -8195,7 +7974,7 @@ function createRouter(dbOrDeps) {
|
|
|
8195
7974
|
return;
|
|
8196
7975
|
}
|
|
8197
7976
|
const workspaceEnv = await readWorkspaceEnvVars(workspacePaths);
|
|
8198
|
-
const result = await new DockerHookExecutor2().execute(resolvedHook, {
|
|
7977
|
+
const result = await new DockerHookExecutor2({ version: getAgentQaVersion2() }).execute(resolvedHook, {
|
|
8199
7978
|
env: {
|
|
8200
7979
|
...workspaceEnv,
|
|
8201
7980
|
...validation.overrides
|
|
@@ -8231,7 +8010,7 @@ function createRouter(dbOrDeps) {
|
|
|
8231
8010
|
variables: shown.variables,
|
|
8232
8011
|
sandbox: {
|
|
8233
8012
|
runtime: resolvedHook.runtime,
|
|
8234
|
-
image:
|
|
8013
|
+
image: hookRunnerImage(resolvedHook.runtime, getAgentQaVersion2()),
|
|
8235
8014
|
networkMode: resolvedHook.network ? "enabled" : "disabled",
|
|
8236
8015
|
dockerVersion: null,
|
|
8237
8016
|
networkLogsAvailable: shown.networkLogs.length > 0,
|
|
@@ -8435,12 +8214,8 @@ function createRouter(dbOrDeps) {
|
|
|
8435
8214
|
return;
|
|
8436
8215
|
}
|
|
8437
8216
|
if (req.method === "GET" && /^\/api\/live-editor\/sessions\/[^/]+\/live-url$/.test(path)) {
|
|
8438
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
8439
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
8440
|
-
return;
|
|
8441
|
-
}
|
|
8442
8217
|
if (!deps.mintEditorLiveUrl) {
|
|
8443
|
-
|
|
8218
|
+
json(res, { error: "live_view_unavailable" }, 503);
|
|
8444
8219
|
return;
|
|
8445
8220
|
}
|
|
8446
8221
|
let editorSessionId;
|
|
@@ -8449,33 +8224,32 @@ function createRouter(dbOrDeps) {
|
|
|
8449
8224
|
path.slice("/api/live-editor/sessions/".length, -"/live-url".length)
|
|
8450
8225
|
);
|
|
8451
8226
|
} catch {
|
|
8452
|
-
|
|
8227
|
+
json(res, { error: "unknown_session" }, 404);
|
|
8453
8228
|
return;
|
|
8454
8229
|
}
|
|
8455
8230
|
void deps.mintEditorLiveUrl(editorSessionId).then(
|
|
8456
8231
|
(result) => {
|
|
8457
|
-
|
|
8232
|
+
json(res, result);
|
|
8458
8233
|
},
|
|
8459
8234
|
(error) => {
|
|
8460
8235
|
const name = error instanceof Error ? error.name : "";
|
|
8461
8236
|
if (name === "UnknownLiveEditorSessionError") {
|
|
8462
|
-
|
|
8237
|
+
json(res, { error: "unknown_session" }, 404);
|
|
8463
8238
|
return;
|
|
8464
8239
|
}
|
|
8465
8240
|
if (name === "LiveViewUnavailableError") {
|
|
8466
|
-
|
|
8241
|
+
json(res, { error: "live_view_unavailable" }, 503);
|
|
8467
8242
|
return;
|
|
8468
8243
|
}
|
|
8469
|
-
|
|
8244
|
+
json(res, { error: "live_url_mint_failed" }, 500);
|
|
8470
8245
|
}
|
|
8471
8246
|
);
|
|
8472
8247
|
return;
|
|
8473
8248
|
}
|
|
8474
8249
|
const liveEditorAuthStateMatch = path.match(/^\/api\/live-editor\/sessions\/([^/]+)\/auth-state$/);
|
|
8475
8250
|
if (liveEditorAuthStateMatch && req.method === "POST") {
|
|
8476
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
8477
8251
|
if (!sessionManager) {
|
|
8478
|
-
|
|
8252
|
+
json(res, { error: "Live editor not available" }, 503);
|
|
8479
8253
|
return;
|
|
8480
8254
|
}
|
|
8481
8255
|
;
|
|
@@ -8486,35 +8260,34 @@ function createRouter(dbOrDeps) {
|
|
|
8486
8260
|
const body = await readJsonBody(req);
|
|
8487
8261
|
const parsedName = AuthStateNameSchema.safeParse(typeof body?.name === "string" ? body.name : "");
|
|
8488
8262
|
if (!parsedName.success) {
|
|
8489
|
-
|
|
8263
|
+
json(res, { error: "Auth state name must be a lowercase slug." }, 400);
|
|
8490
8264
|
return;
|
|
8491
8265
|
}
|
|
8492
8266
|
stateName = parsedName.data;
|
|
8493
8267
|
session = sessionManager.getSession(decodeURIComponent(liveEditorAuthStateMatch[1]));
|
|
8494
8268
|
if (!session) {
|
|
8495
|
-
|
|
8269
|
+
json(res, { error: "Session not found" }, 404);
|
|
8496
8270
|
return;
|
|
8497
8271
|
}
|
|
8498
8272
|
if (typeof session.captureWebAuthState !== "function") {
|
|
8499
|
-
|
|
8273
|
+
json(res, {
|
|
8500
8274
|
error: `Could not save auth state "${stateName}" for target "${getSessionTargetNameForAuthState(session)}".`
|
|
8501
8275
|
}, 500);
|
|
8502
8276
|
return;
|
|
8503
8277
|
}
|
|
8504
8278
|
const authState = await session.captureWebAuthState(stateName, { replace: body?.replace === true });
|
|
8505
|
-
|
|
8279
|
+
json(res, { authState });
|
|
8506
8280
|
} catch (err) {
|
|
8507
8281
|
const targetName = getSessionTargetNameForAuthState(session);
|
|
8508
8282
|
const response = buildAuthStateSaveErrorMessage(stateName || "unknown", targetName, err);
|
|
8509
|
-
|
|
8283
|
+
json(res, { error: response.message }, response.status);
|
|
8510
8284
|
}
|
|
8511
8285
|
})();
|
|
8512
8286
|
return;
|
|
8513
8287
|
}
|
|
8514
8288
|
if (path === "/api/live-editor/sessions" && req.method === "POST") {
|
|
8515
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
8516
8289
|
if (!sessionManager) {
|
|
8517
|
-
|
|
8290
|
+
json(res, { error: "Live editor not available" }, 503);
|
|
8518
8291
|
return;
|
|
8519
8292
|
}
|
|
8520
8293
|
;
|
|
@@ -8522,16 +8295,10 @@ function createRouter(dbOrDeps) {
|
|
|
8522
8295
|
try {
|
|
8523
8296
|
const body = await readJsonBody(req);
|
|
8524
8297
|
const targetName = typeof body?.targetName === "string" ? body.targetName : void 0;
|
|
8525
|
-
const production = await resolveLiveTargetProduction(configManager, targetName);
|
|
8526
|
-
const productionRefusal = liveProductionRefusal(production);
|
|
8527
|
-
if (productionRefusal !== void 0) {
|
|
8528
|
-
jsonNoCors(res, { error: productionRefusal.message }, 403);
|
|
8529
|
-
return;
|
|
8530
|
-
}
|
|
8531
8298
|
const platform = body?.platform ?? "web";
|
|
8532
8299
|
const runtimeLLM = await resolveDashboardRuntimeLLM(configManager, llmConfig, authFetch);
|
|
8533
8300
|
if (!runtimeLLM.ok) {
|
|
8534
|
-
|
|
8301
|
+
json(res, { error: runtimeLLM.error }, 400);
|
|
8535
8302
|
return;
|
|
8536
8303
|
}
|
|
8537
8304
|
const setupHooks = Array.isArray(body?.setupHooks) ? body.setupHooks.filter((value) => typeof value === "string" && value.trim().length > 0) : [];
|
|
@@ -8550,14 +8317,14 @@ function createRouter(dbOrDeps) {
|
|
|
8550
8317
|
let resolvedHooks = /* @__PURE__ */ new Map();
|
|
8551
8318
|
let hookRegistryError;
|
|
8552
8319
|
if (!workspacePaths) {
|
|
8553
|
-
|
|
8320
|
+
json(res, { error: "Workspace path resolution not available" }, 500);
|
|
8554
8321
|
return;
|
|
8555
8322
|
}
|
|
8556
8323
|
try {
|
|
8557
8324
|
const envContent = await readFile6(workspacePaths.envFile.absolutePath, "utf-8");
|
|
8558
8325
|
envVars = parseEnvFile(envContent);
|
|
8559
8326
|
} catch {
|
|
8560
|
-
|
|
8327
|
+
json(res, { error: `workspace.envFile file could not be loaded: ${workspacePaths.envFile.workspaceRelativePath}` }, 400);
|
|
8561
8328
|
return;
|
|
8562
8329
|
}
|
|
8563
8330
|
try {
|
|
@@ -8565,7 +8332,7 @@ function createRouter(dbOrDeps) {
|
|
|
8565
8332
|
secretStore = SecretStore.fromEnvContent(secretsContent);
|
|
8566
8333
|
secretRedactor = new SecretRedactor(secretStore);
|
|
8567
8334
|
} catch {
|
|
8568
|
-
|
|
8335
|
+
json(res, { error: `workspace.secretsFile file could not be loaded: ${workspacePaths.secretsFile.workspaceRelativePath}` }, 400);
|
|
8569
8336
|
return;
|
|
8570
8337
|
}
|
|
8571
8338
|
const hookCatalog = await readWorkspaceHooks(configManager, deps.configPath);
|
|
@@ -8576,6 +8343,11 @@ function createRouter(dbOrDeps) {
|
|
|
8576
8343
|
...suiteFileManager === void 0 ? {} : { findSuite: (id) => suiteFileManager.findBySuiteId(id) }
|
|
8577
8344
|
};
|
|
8578
8345
|
const plannerLimits = await resolveLivePlannerLimits(configManager, entityRef, entityReaders);
|
|
8346
|
+
const navigationTimeoutMs = await resolveLiveNavigationTimeoutMs(
|
|
8347
|
+
configManager,
|
|
8348
|
+
entityRef,
|
|
8349
|
+
entityReaders
|
|
8350
|
+
);
|
|
8579
8351
|
const sessionSuiteRef = entityRef?.type === "suite" ? entityRef : void 0;
|
|
8580
8352
|
const plannerLimitsForTest = (testId) => resolveLivePlannerLimits(
|
|
8581
8353
|
configManager,
|
|
@@ -8586,7 +8358,6 @@ function createRouter(dbOrDeps) {
|
|
|
8586
8358
|
{
|
|
8587
8359
|
platform,
|
|
8588
8360
|
targetName,
|
|
8589
|
-
production: { productionTargetNames: production.productionTargetNames },
|
|
8590
8361
|
plannerLimits,
|
|
8591
8362
|
plannerLimitsForTest,
|
|
8592
8363
|
llmConfig: runtimeLLM.llmConfig,
|
|
@@ -8600,6 +8371,7 @@ function createRouter(dbOrDeps) {
|
|
|
8600
8371
|
hookRegistryError,
|
|
8601
8372
|
url: body?.url,
|
|
8602
8373
|
headless: body?.headless ?? false,
|
|
8374
|
+
navigationTimeoutMs,
|
|
8603
8375
|
useDeviceName: typeof body?.useDeviceName === "string" ? body.useDeviceName : void 0,
|
|
8604
8376
|
appState: body?.appState === "preserve" || body?.appState === "reset" ? body.appState : void 0,
|
|
8605
8377
|
bundleId: typeof body?.bundleId === "string" ? body.bundleId : void 0,
|
|
@@ -8609,38 +8381,37 @@ function createRouter(dbOrDeps) {
|
|
|
8609
8381
|
},
|
|
8610
8382
|
entityRef
|
|
8611
8383
|
);
|
|
8612
|
-
|
|
8384
|
+
json(res, { sessionId, sessionNumber }, 201);
|
|
8613
8385
|
} catch (err) {
|
|
8614
8386
|
const message = err instanceof Error ? err.message : String(err);
|
|
8615
|
-
const
|
|
8616
|
-
|
|
8387
|
+
const refused = err instanceof LivePlannerLimitsRefusedError || err instanceof LiveNavigationTimeoutRefusedError;
|
|
8388
|
+
const status = refused ? 400 : 500;
|
|
8389
|
+
json(res, { error: message }, status);
|
|
8617
8390
|
}
|
|
8618
8391
|
})();
|
|
8619
8392
|
return;
|
|
8620
8393
|
}
|
|
8621
8394
|
if (path === "/api/live-editor/sessions" && req.method === "GET") {
|
|
8622
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
8623
8395
|
if (!sessionManager) {
|
|
8624
|
-
|
|
8396
|
+
json(res, { error: "Live editor not available" }, 503);
|
|
8625
8397
|
return;
|
|
8626
8398
|
}
|
|
8627
|
-
|
|
8399
|
+
json(res, { sessions: sessionManager.listSessions() });
|
|
8628
8400
|
return;
|
|
8629
8401
|
}
|
|
8630
8402
|
const liveEditorSessionMatch = path.match(/^\/api\/live-editor\/sessions\/([^/]+)$/);
|
|
8631
8403
|
if (liveEditorSessionMatch && req.method === "DELETE") {
|
|
8632
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
8633
8404
|
if (!sessionManager) {
|
|
8634
|
-
|
|
8405
|
+
json(res, { error: "Live editor not available" }, 503);
|
|
8635
8406
|
return;
|
|
8636
8407
|
}
|
|
8637
8408
|
const sessionId = liveEditorSessionMatch[1];
|
|
8638
8409
|
(async () => {
|
|
8639
8410
|
const terminated = await sessionManager.terminateSession(sessionId);
|
|
8640
8411
|
if (terminated) {
|
|
8641
|
-
|
|
8412
|
+
json(res, { ok: true });
|
|
8642
8413
|
} else {
|
|
8643
|
-
|
|
8414
|
+
json(res, { error: "Session not found" }, 404);
|
|
8644
8415
|
}
|
|
8645
8416
|
})();
|
|
8646
8417
|
return;
|
|
@@ -8689,6 +8460,7 @@ function createRouter(dbOrDeps) {
|
|
|
8689
8460
|
// src/queue/job-queue.ts
|
|
8690
8461
|
import { EventEmitter } from "node:events";
|
|
8691
8462
|
import { cpus } from "node:os";
|
|
8463
|
+
import { printRunText as printRunText2 } from "@vostride/agent-qa-core";
|
|
8692
8464
|
var NOT_CANCELLABLE = { kind: "not-cancellable" };
|
|
8693
8465
|
var JobQueue = class extends EventEmitter {
|
|
8694
8466
|
db;
|
|
@@ -8711,8 +8483,7 @@ var JobQueue = class extends EventEmitter {
|
|
|
8711
8483
|
this.pollInterval = setInterval(() => this.dispatchReported("a poll"), intervalMs);
|
|
8712
8484
|
this.on("enqueued", () => this.dispatchReported("an enqueue"));
|
|
8713
8485
|
}
|
|
8714
|
-
//
|
|
8715
|
-
// still frees its slot, and nothing reads the closed database.
|
|
8486
|
+
// The server closes the database after this; a run settling later still frees its slot.
|
|
8716
8487
|
stop() {
|
|
8717
8488
|
this.stopped = true;
|
|
8718
8489
|
if (this.pollInterval) {
|
|
@@ -8798,7 +8569,10 @@ var JobQueue = class extends EventEmitter {
|
|
|
8798
8569
|
this.processNext();
|
|
8799
8570
|
} catch (error) {
|
|
8800
8571
|
const message = error instanceof Error ? error.message : String(error);
|
|
8801
|
-
|
|
8572
|
+
printRunText2(
|
|
8573
|
+
`Queue could not dispatch the next run after ${trigger}: ${message}`,
|
|
8574
|
+
(text) => console.error(text)
|
|
8575
|
+
);
|
|
8802
8576
|
}
|
|
8803
8577
|
}
|
|
8804
8578
|
getActiveCount() {
|
|
@@ -8807,9 +8581,7 @@ var JobQueue = class extends EventEmitter {
|
|
|
8807
8581
|
getConcurrency() {
|
|
8808
8582
|
return this.concurrency;
|
|
8809
8583
|
}
|
|
8810
|
-
//
|
|
8811
|
-
// 442-15 (GAP-D2): own is the stamp this handle wrote, or equal non-null start times.
|
|
8812
|
-
// 2026-10-17 (444-06, D-11): a running row is never written here; the run answers with its verdict.
|
|
8584
|
+
// Only the owner stops its running row, never written here; a gone owner's row is an orphan.
|
|
8813
8585
|
cancel(runId) {
|
|
8814
8586
|
const run = this.db.getRun(runId);
|
|
8815
8587
|
if (!run) return NOT_CANCELLABLE;
|
|
@@ -8839,21 +8611,7 @@ var JobQueue = class extends EventEmitter {
|
|
|
8839
8611
|
const run = this.db.getRun(runId);
|
|
8840
8612
|
return run && run.status !== "running" && run.status !== "pending" ? { kind: "finished", status: run.status } : NOT_CANCELLABLE;
|
|
8841
8613
|
}
|
|
8842
|
-
//
|
|
8843
|
-
// MODEL THAT MINTED ITS CHILDREN.
|
|
8844
|
-
//
|
|
8845
|
-
// ~19 lines stood here walking the run's child rows and cancelling each member row. Under
|
|
8846
|
-
// the one-run model a parallel suite is ONE run row with member handles (402-04) — nothing writes
|
|
8847
|
-
// the run row's parent-link column any more (405-07 then deleted the column outright), so the
|
|
8848
|
-
// walk could only ever have read an empty list while reading as live cancellation policy.
|
|
8849
|
-
// Cancelling the run cancels the run.
|
|
8850
|
-
//
|
|
8851
|
-
// WHAT THIS DOES NOT TOUCH: `parallel` below. D-11 keeps the queue's parallel flag as CROSS-RUN
|
|
8852
|
-
// concurrency of INDEPENDENT jobs, which is what `findNextEligible` has always used it for. What
|
|
8853
|
-
// was deleted is parallel-SUITE semantics, not the queue's ability to run two unrelated jobs.
|
|
8854
|
-
//
|
|
8855
|
-
// HISTORICAL ROWS: a pre-402 suite run still lists its members on the read side; cancelling
|
|
8856
|
-
// such a run now leaves those member rows in whatever state their own process left them.
|
|
8614
|
+
// `parallel` is cross-run concurrency of independent jobs; a suite is one run row.
|
|
8857
8615
|
findNextEligible() {
|
|
8858
8616
|
if (this.activeCount >= this.concurrency) return void 0;
|
|
8859
8617
|
const pending = this.db.getPendingRuns();
|
|
@@ -8876,7 +8634,7 @@ var JobQueue = class extends EventEmitter {
|
|
|
8876
8634
|
};
|
|
8877
8635
|
|
|
8878
8636
|
// src/tests/suite-file-manager.ts
|
|
8879
|
-
import { readFile as readFile7, mkdir as mkdir4, stat as
|
|
8637
|
+
import { readFile as readFile7, mkdir as mkdir4, stat as stat5, rm as rm5 } from "node:fs/promises";
|
|
8880
8638
|
import { dirname as dirname8, basename as basename6 } from "node:path";
|
|
8881
8639
|
import { generateSuiteId } from "@vostride/agent-qa-ids";
|
|
8882
8640
|
import { parse as parseYaml7 } from "yaml";
|
|
@@ -8887,14 +8645,14 @@ var SuiteFileManager = class {
|
|
|
8887
8645
|
this.testFileManager = testFileManager;
|
|
8888
8646
|
}
|
|
8889
8647
|
testFileManager;
|
|
8890
|
-
//
|
|
8648
|
+
// Each list call reads the workspace itself; each file is written whole, one writer at a time.
|
|
8891
8649
|
underLock = createPathLock();
|
|
8892
8650
|
async list() {
|
|
8893
8651
|
const records = await discoverWorkspaceFiles4({ workspace: this.workspace, kind: "suite" });
|
|
8894
8652
|
const files = [];
|
|
8895
8653
|
for (const record of records) {
|
|
8896
8654
|
try {
|
|
8897
|
-
const s = await
|
|
8655
|
+
const s = await stat5(record.absolutePath);
|
|
8898
8656
|
if (s.isFile()) {
|
|
8899
8657
|
let suiteName = basename6(record.workspaceRelativePath);
|
|
8900
8658
|
let testCount = 0;
|
|
@@ -8971,7 +8729,7 @@ var SuiteFileManager = class {
|
|
|
8971
8729
|
}
|
|
8972
8730
|
async delete(relativePath) {
|
|
8973
8731
|
const target = await this.resolveTarget(relativePath, true);
|
|
8974
|
-
await this.underLock(target.absolutePath, () =>
|
|
8732
|
+
await this.underLock(target.absolutePath, () => rm5(target.absolutePath));
|
|
8975
8733
|
}
|
|
8976
8734
|
async validate(content) {
|
|
8977
8735
|
try {
|
|
@@ -9028,6 +8786,7 @@ import { request } from "node:http";
|
|
|
9028
8786
|
import { spawn } from "node:child_process";
|
|
9029
8787
|
import {
|
|
9030
8788
|
formatAppiumInstallGuidance,
|
|
8789
|
+
printRunText as printRunText3,
|
|
9031
8790
|
resolveAppiumExecutable
|
|
9032
8791
|
} from "@vostride/agent-qa-core";
|
|
9033
8792
|
import treeKill from "tree-kill";
|
|
@@ -9155,21 +8914,21 @@ var AppiumManager = class {
|
|
|
9155
8914
|
if (err.code === "ENOENT") {
|
|
9156
8915
|
console.error(`${APPIUM_PREFIX} Appium not found. ${formatAppiumInstallGuidance()}`);
|
|
9157
8916
|
} else {
|
|
9158
|
-
|
|
8917
|
+
printRunText3(`${APPIUM_PREFIX} Error: ${err.message}`, (text) => console.error(text));
|
|
9159
8918
|
}
|
|
9160
8919
|
});
|
|
9161
8920
|
child.stdout?.on("data", (chunk) => {
|
|
9162
8921
|
if (this.logLevel !== "debug") return;
|
|
9163
8922
|
const lines = chunk.toString().split("\n").filter(Boolean);
|
|
9164
8923
|
for (const line of lines) {
|
|
9165
|
-
|
|
8924
|
+
printRunText3(`${APPIUM_PREFIX} ${pc.dim(line)}`, (text) => console.log(text));
|
|
9166
8925
|
}
|
|
9167
8926
|
});
|
|
9168
8927
|
child.stderr?.on("data", (chunk) => {
|
|
9169
8928
|
if (this.logLevel !== "debug") return;
|
|
9170
8929
|
const lines = chunk.toString().split("\n").filter(Boolean);
|
|
9171
8930
|
for (const line of lines) {
|
|
9172
|
-
|
|
8931
|
+
printRunText3(`${APPIUM_PREFIX} ${pc.dim(line)}`, (text) => console.error(text));
|
|
9173
8932
|
}
|
|
9174
8933
|
});
|
|
9175
8934
|
const start = Date.now();
|
|
@@ -9251,10 +9010,7 @@ var SessionManager = class {
|
|
|
9251
9010
|
this.deps = deps;
|
|
9252
9011
|
}
|
|
9253
9012
|
sessions = /* @__PURE__ */ new Map();
|
|
9254
|
-
//
|
|
9255
|
-
// Monotonic and ephemeral: resets on server restart per CONTEXT D-30.
|
|
9256
|
-
// Never decremented on terminate — a re-open of a terminated entity yields
|
|
9257
|
-
// the next number so "Session #3" after "#2" ends does not confuse users.
|
|
9013
|
+
// Monotonic per entity and reset on restart, so a reopened entity gets the next number.
|
|
9258
9014
|
counters = /* @__PURE__ */ new Map();
|
|
9259
9015
|
async createSession(config, entity) {
|
|
9260
9016
|
if (!config.llmConfig) {
|
|
@@ -9311,7 +9067,8 @@ var SessionManager = class {
|
|
|
9311
9067
|
};
|
|
9312
9068
|
|
|
9313
9069
|
// src/live-editor/ws-handler.ts
|
|
9314
|
-
import {
|
|
9070
|
+
import { format as format2 } from "node:util";
|
|
9071
|
+
import { parseTestFile, printRunText as printRunText4 } from "@vostride/agent-qa-core";
|
|
9315
9072
|
|
|
9316
9073
|
// src/live-editor/types.ts
|
|
9317
9074
|
var LIVE_EDITOR_UNKNOWN_SESSION_CLOSE_CODE = 4404;
|
|
@@ -9498,12 +9255,14 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
|
|
|
9498
9255
|
try {
|
|
9499
9256
|
raw = await testFileManager.read(msg.path);
|
|
9500
9257
|
} catch (readErr) {
|
|
9501
|
-
|
|
9258
|
+
const line = `[ws-handler] execute-test: read failed for path=${msg.path}`;
|
|
9259
|
+
printRunText4(format2(line, readErr), () => console.error(line, readErr));
|
|
9502
9260
|
throw new Error(`Cannot read test file "${msg.path}": ${readErr?.message ?? String(readErr)}`);
|
|
9503
9261
|
}
|
|
9504
9262
|
const parsed = parseTestFile(raw, msg.path);
|
|
9505
9263
|
if (parsed.errors.length > 0) {
|
|
9506
|
-
|
|
9264
|
+
const line = `[ws-handler] execute-test: parse errors for ${msg.path}`;
|
|
9265
|
+
printRunText4(format2(line, parsed.errors), () => console.error(line, parsed.errors));
|
|
9507
9266
|
throw new Error(`Parse error in ${msg.path}: ${parsed.errors[0].message}`);
|
|
9508
9267
|
}
|
|
9509
9268
|
const testDef = parsed.tests[0];
|
|
@@ -9513,7 +9272,13 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
|
|
|
9513
9272
|
const stepInstructions = testDef.steps.map(
|
|
9514
9273
|
(s) => typeof s === "string" ? s : s.step
|
|
9515
9274
|
);
|
|
9516
|
-
|
|
9275
|
+
const stepCount = stepInstructions.length;
|
|
9276
|
+
const setupCount = (testDef.setup ?? []).length;
|
|
9277
|
+
const teardownCount = (testDef.teardown ?? []).length;
|
|
9278
|
+
printRunText4(
|
|
9279
|
+
`[ws-handler] execute-test: starting "${testDef.name}" (${stepCount} steps, setup=${setupCount}, teardown=${teardownCount})`,
|
|
9280
|
+
(text) => console.log(text)
|
|
9281
|
+
);
|
|
9517
9282
|
reply({
|
|
9518
9283
|
type: "test-start",
|
|
9519
9284
|
test: {
|
|
@@ -9535,7 +9300,11 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
|
|
|
9535
9300
|
teardown: testDef.teardown ?? []
|
|
9536
9301
|
}
|
|
9537
9302
|
);
|
|
9538
|
-
|
|
9303
|
+
const errorPart = result.error ? ` error=${result.error}` : "";
|
|
9304
|
+
printRunText4(
|
|
9305
|
+
`[ws-handler] execute-test: finished "${testDef.name}" status=${result.status} duration=${result.duration}ms${errorPart}`,
|
|
9306
|
+
(text) => console.log(text)
|
|
9307
|
+
);
|
|
9539
9308
|
reply({
|
|
9540
9309
|
type: "test-complete",
|
|
9541
9310
|
test: {
|
|
@@ -9548,11 +9317,9 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
|
|
|
9548
9317
|
});
|
|
9549
9318
|
reply({ type: "session-state", state: session.getState() });
|
|
9550
9319
|
} catch (err) {
|
|
9551
|
-
|
|
9552
|
-
|
|
9553
|
-
|
|
9554
|
-
code: err?.code
|
|
9555
|
-
});
|
|
9320
|
+
const line = `[ws-handler] execute-test: caught error for path=${msg.path}`;
|
|
9321
|
+
const detail = { message: err?.message, stack: err?.stack, code: err?.code };
|
|
9322
|
+
printRunText4(format2(line, detail), () => console.error(line, detail));
|
|
9556
9323
|
reply({
|
|
9557
9324
|
type: "test-error",
|
|
9558
9325
|
test: {
|
|
@@ -9809,8 +9576,7 @@ function renderLiveEmbedPage(input) {
|
|
|
9809
9576
|
if (pending === null) return;
|
|
9810
9577
|
var header = pending;
|
|
9811
9578
|
pending = null;
|
|
9812
|
-
//
|
|
9813
|
-
// both paint in every host, not only in Chromium (428 RESEARCH A2).
|
|
9579
|
+
// No MIME type: the decoder sniffs, so JPEG and PNG frames paint in every host.
|
|
9814
9580
|
var blob = new Blob([event.data]);
|
|
9815
9581
|
createImageBitmap(blob).then(function (bitmap) {
|
|
9816
9582
|
if (canvas.width !== header.width || canvas.height !== header.height) {
|
|
@@ -9834,8 +9600,7 @@ function renderLiveEmbedPage(input) {
|
|
|
9834
9600
|
try { msg = JSON.parse(event.data); } catch (e) { return; }
|
|
9835
9601
|
|
|
9836
9602
|
if (msg.type === 'ready') {
|
|
9837
|
-
//
|
|
9838
|
-
// a transport guess (LIVE-04's prohibition). A snapshot paints on the same canvas (428 D-11).
|
|
9603
|
+
// The declared kind picks the painter, never a browser name, adapter or transport guess.
|
|
9839
9604
|
var kind = msg.stream ? msg.stream.kind : undefined;
|
|
9840
9605
|
if (kind === 'cdp-screencast' || kind === 'snapshot') {
|
|
9841
9606
|
setState('connecting', 'waiting for the first frame\u2026');
|
|
@@ -9882,8 +9647,7 @@ function renderLiveEmbedPage(input) {
|
|
|
9882
9647
|
report({ event: 'lost' });
|
|
9883
9648
|
});
|
|
9884
9649
|
|
|
9885
|
-
// A
|
|
9886
|
-
// so a renewal never costs the viewer the stream it is watching (D-02).
|
|
9650
|
+
// A re-minting host replaces the fragment; the socket renews in place and the stream continues.
|
|
9887
9651
|
window.addEventListener('hashchange', function () {
|
|
9888
9652
|
var next = location.hash.slice(1);
|
|
9889
9653
|
if (next === '' || socket.readyState !== 1) return;
|
|
@@ -10093,7 +9857,8 @@ function createFrameProxy(deps) {
|
|
|
10093
9857
|
|
|
10094
9858
|
// src/server/server.ts
|
|
10095
9859
|
import {
|
|
10096
|
-
isLoopbackHost as isLoopbackHost2
|
|
9860
|
+
isLoopbackHost as isLoopbackHost2,
|
|
9861
|
+
printRunText as printRunText5
|
|
10097
9862
|
} from "@vostride/agent-qa-core";
|
|
10098
9863
|
var MIME_TYPES = {
|
|
10099
9864
|
".html": "text/html; charset=utf-8",
|
|
@@ -10113,9 +9878,10 @@ var MIME_TYPES = {
|
|
|
10113
9878
|
".webp": "image/webp"
|
|
10114
9879
|
};
|
|
10115
9880
|
var DashboardRemoteBindRefusedError = class extends Error {
|
|
10116
|
-
constructor(host) {
|
|
9881
|
+
constructor(host, port) {
|
|
9882
|
+
const p = String(port === 0 ? 3470 : port);
|
|
10117
9883
|
super(
|
|
10118
|
-
`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})
|
|
9884
|
+
`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}/.`
|
|
10119
9885
|
);
|
|
10120
9886
|
this.host = host;
|
|
10121
9887
|
this.name = "DashboardRemoteBindRefusedError";
|
|
@@ -10127,7 +9893,7 @@ async function startServer(opts) {
|
|
|
10127
9893
|
const runServiceAttach = opts.runServiceAttach ?? runService?.attach;
|
|
10128
9894
|
const host = opts.host?.trim() || DEFAULT_DASHBOARD_HOST;
|
|
10129
9895
|
if (!isLoopbackHost2(host)) {
|
|
10130
|
-
throw new DashboardRemoteBindRefusedError(host);
|
|
9896
|
+
throw new DashboardRemoteBindRefusedError(host, port);
|
|
10131
9897
|
}
|
|
10132
9898
|
const resolvedConfigPath = configPath ?? resolve4("agent-qa.config.yaml");
|
|
10133
9899
|
const configManager = new ConfigManager(resolvedConfigPath, opts.declaredSecretMask);
|
|
@@ -10162,7 +9928,7 @@ async function startServer(opts) {
|
|
|
10162
9928
|
failureSummary = "Test timed out -- process was killed";
|
|
10163
9929
|
} else if (details.refusedBeforeStart) {
|
|
10164
9930
|
dbStatus = "failed";
|
|
10165
|
-
refusalProse = details.
|
|
9931
|
+
refusalProse = details.reason;
|
|
10166
9932
|
failureSummary = refusalProse ? `Refused before the run started: ${refusalProse}`.slice(0, 300) : "Refused before the run started";
|
|
10167
9933
|
} else if (status === "completed") {
|
|
10168
9934
|
dbStatus = "failed";
|
|
@@ -10243,7 +10009,10 @@ async function startServer(opts) {
|
|
|
10243
10009
|
mobileRuns.set(run.id, platform);
|
|
10244
10010
|
} catch (err) {
|
|
10245
10011
|
const message = err instanceof Error ? err.message : String(err);
|
|
10246
|
-
|
|
10012
|
+
printRunText5(
|
|
10013
|
+
`Failed to start Appium for run ${run.id}: ${message}`,
|
|
10014
|
+
(text) => console.error(text)
|
|
10015
|
+
);
|
|
10247
10016
|
db.updateRun(run.id, {
|
|
10248
10017
|
status: "failed",
|
|
10249
10018
|
failureSummary: `Appium server failed to start: ${message}`,
|
|
@@ -10263,19 +10032,9 @@ async function startServer(opts) {
|
|
|
10263
10032
|
runId: run.id,
|
|
10264
10033
|
args,
|
|
10265
10034
|
source: meta.isSuite ? "suite" : "dashboard",
|
|
10266
|
-
// 2026-10-01 (432-14, D-14): no confirm scope is read off the row any more. The browser that
|
|
10267
|
-
// triggered a run is not asked anything, so nothing about it has to survive the wait.
|
|
10268
10035
|
attributes: run.attributes,
|
|
10269
|
-
//
|
|
10270
|
-
// `in-process-backend.ts:211` reads `opts.timeout ?? defaultTimeout` and validates AFTER, so
|
|
10271
|
-
// any non-nullish nonsense here skips the default and leaves the run with no reclamation
|
|
10272
|
-
// timer at all. An unusable value falls back to the backend's default rather than a number
|
|
10273
|
-
// this call invented.
|
|
10036
|
+
// The backend takes `opts.timeout ?? defaultTimeout`, so an unusable value falls back here.
|
|
10274
10037
|
timeout: sanitizeQueuedExecutionTimeout(meta.timeout),
|
|
10275
|
-
// THE LEASE ABOVE IS UNCHANGED; ONLY ITS DELIVERY IS. `acquireLease` still runs where it ran,
|
|
10276
|
-
// still keys on this run id and still owns the bundled Appium's lifecycle (design-of-record).
|
|
10277
|
-
// What used to ride `AGENT_QA_APPIUM_URL` in a child's environment now travels as an
|
|
10278
|
-
// argument, because there is no child to carry an environment.
|
|
10279
10038
|
...isMobile ? { appiumUrl: appiumManager.getUrl() } : {}
|
|
10280
10039
|
});
|
|
10281
10040
|
});
|
|
@@ -10286,16 +10045,14 @@ async function startServer(opts) {
|
|
|
10286
10045
|
appiumManager,
|
|
10287
10046
|
configManager,
|
|
10288
10047
|
configPath: resolvedConfigPath,
|
|
10289
|
-
//
|
|
10048
|
+
// Each created session registers here, which is what the frame socket attaches.
|
|
10290
10049
|
...liveEditorSubjects === void 0 ? {} : { liveEditorSubjects }
|
|
10291
10050
|
});
|
|
10292
10051
|
const servesLiveView = liveView !== void 0 && (runServiceAttach !== void 0 || liveEditorSubjects !== void 0);
|
|
10293
10052
|
const apiRouter = createRouter({
|
|
10294
10053
|
db,
|
|
10295
10054
|
workspacePaths,
|
|
10296
|
-
//
|
|
10297
|
-
// dependency is an ABSENT field, never a present `undefined`, so the router's own 503 arm is
|
|
10298
|
-
// what decides, in one place.
|
|
10055
|
+
// Absent dependencies are absent fields, so the router's own 503 arm decides in one place.
|
|
10299
10056
|
...executionBackend === void 0 ? {} : { executionBackend },
|
|
10300
10057
|
jobQueue,
|
|
10301
10058
|
testFileManager,
|
|
@@ -10306,27 +10063,13 @@ async function startServer(opts) {
|
|
|
10306
10063
|
llmConfig,
|
|
10307
10064
|
authFetch,
|
|
10308
10065
|
sessionManager,
|
|
10309
|
-
//
|
|
10310
|
-
// never a present `undefined` — the router's own 501 arm is what decides, in one place.
|
|
10066
|
+
// Absent dependencies are absent fields, so the router's own 501 arm decides in one place.
|
|
10311
10067
|
...agentHost === void 0 ? {} : { agentHost },
|
|
10312
|
-
// 402-05 (D-15): the same conditional-spread house rule. Absent means a legacy mount rather
|
|
10313
|
-
// than an unconfigured service, and the router's own 503 arm is what decides, in one place.
|
|
10314
10068
|
...runServiceReadiness === void 0 ? {} : { runServiceReadiness },
|
|
10315
|
-
//
|
|
10316
|
-
// composed again. That derivation is the "one session registry serves both doors" claim in its
|
|
10317
|
-
// structural form — there is no second wiring a composition root could forget or point
|
|
10318
|
-
// somewhere else, and the two doors cannot end up attached to different registries. Absent
|
|
10319
|
-
// `runService` means no registry exists at all, so the same conditional-spread house rule
|
|
10320
|
-
// applies and the router's own 503 arm decides.
|
|
10069
|
+
// Read off `runService` by default, so both doors attach to one session registry.
|
|
10321
10070
|
...runServiceAttach === void 0 ? {} : { runServiceAttach },
|
|
10322
|
-
// 403 D-03: the mint route's port, on the same conditional-spread house rule — absent means
|
|
10323
|
-
// this host serves no live view and the route's own 503 arm decides, in one place.
|
|
10324
10071
|
...liveView === void 0 ? {} : { mintLiveUrl: liveView.mintLiveUrl },
|
|
10325
|
-
// 428 D-07: the editor mint, on the same house rule — absent means the editor mint route's own
|
|
10326
|
-
// 503 decides.
|
|
10327
10072
|
...liveView?.mintEditorLiveUrl === void 0 ? {} : { mintEditorLiveUrl: liveView.mintEditorLiveUrl },
|
|
10328
|
-
// 404-05 (D-16): the same conditional-spread house rule — absent means this host serves no
|
|
10329
|
-
// post-processing push channel and the stream arm's own 503 decides, in one place.
|
|
10330
10073
|
...postProcessingRelay === void 0 ? {} : { postProcessingRelay }
|
|
10331
10074
|
});
|
|
10332
10075
|
const runServiceRouter = runService === void 0 ? void 0 : createRunServiceRoutes(runService);
|
|
@@ -10346,13 +10089,15 @@ async function startServer(opts) {
|
|
|
10346
10089
|
apiRouter(req, res);
|
|
10347
10090
|
return;
|
|
10348
10091
|
}
|
|
10092
|
+
if (refuseNonLoopbackHost(req) !== void 0) {
|
|
10093
|
+
const address = server.address();
|
|
10094
|
+
const boundPort = typeof address === "object" && address !== null ? address.port : port;
|
|
10095
|
+
refuseForeignHostPage(res, boundPort);
|
|
10096
|
+
return;
|
|
10097
|
+
}
|
|
10349
10098
|
if (req.method === "OPTIONS") {
|
|
10350
|
-
res.writeHead(
|
|
10351
|
-
|
|
10352
|
-
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
10353
|
-
"Access-Control-Allow-Headers": "Content-Type"
|
|
10354
|
-
});
|
|
10355
|
-
res.end();
|
|
10099
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
10100
|
+
res.end("Not Found");
|
|
10356
10101
|
return;
|
|
10357
10102
|
}
|
|
10358
10103
|
if (req.method === "GET" && path.startsWith(LIVE_EMBED_PATH_PREFIX)) {
|
|
@@ -10387,17 +10132,16 @@ async function startServer(opts) {
|
|
|
10387
10132
|
return;
|
|
10388
10133
|
}
|
|
10389
10134
|
if (uiDir) {
|
|
10390
|
-
const filePath = path === "/" ?
|
|
10135
|
+
const filePath = path === "/" ? join4(uiDir, "index.html") : join4(uiDir, path);
|
|
10391
10136
|
try {
|
|
10392
|
-
const fileStat = await
|
|
10137
|
+
const fileStat = await stat6(filePath);
|
|
10393
10138
|
if (fileStat.isFile()) {
|
|
10394
10139
|
const content = await readFile8(filePath);
|
|
10395
10140
|
const ext = extname(filePath);
|
|
10396
10141
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
10397
10142
|
res.writeHead(200, {
|
|
10398
10143
|
"Content-Type": contentType,
|
|
10399
|
-
"Content-Length": content.length
|
|
10400
|
-
"Access-Control-Allow-Origin": "*"
|
|
10144
|
+
"Content-Length": content.length
|
|
10401
10145
|
});
|
|
10402
10146
|
res.end(content);
|
|
10403
10147
|
return;
|
|
@@ -10405,12 +10149,11 @@ async function startServer(opts) {
|
|
|
10405
10149
|
} catch {
|
|
10406
10150
|
}
|
|
10407
10151
|
try {
|
|
10408
|
-
const indexPath =
|
|
10152
|
+
const indexPath = join4(uiDir, "index.html");
|
|
10409
10153
|
const content = await readFile8(indexPath);
|
|
10410
10154
|
res.writeHead(200, {
|
|
10411
10155
|
"Content-Type": "text/html",
|
|
10412
|
-
"Content-Length": content.length
|
|
10413
|
-
"Access-Control-Allow-Origin": "*"
|
|
10156
|
+
"Content-Length": content.length
|
|
10414
10157
|
});
|
|
10415
10158
|
res.end(content);
|
|
10416
10159
|
return;
|
|
@@ -10495,7 +10238,8 @@ async function startServer(opts) {
|
|
|
10495
10238
|
session.clearIdleTimer();
|
|
10496
10239
|
const handler = createSessionHandler(session, () => {
|
|
10497
10240
|
void sessionManager.terminateSession(sessionId).catch((error) => {
|
|
10498
|
-
|
|
10241
|
+
const line = `[live-editor] could not end session ${sessionId}`;
|
|
10242
|
+
printRunText5(format3(line, error), () => console.error(line, error));
|
|
10499
10243
|
});
|
|
10500
10244
|
}, (event) => {
|
|
10501
10245
|
db.insertTokenEvent(event);
|