@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/index.cjs
CHANGED
|
@@ -98,7 +98,6 @@ __export(index_exports, {
|
|
|
98
98
|
renderLiveEmbedPage: () => renderLiveEmbedPage,
|
|
99
99
|
resolveDashboardDbPath: () => resolveDashboardDbPath,
|
|
100
100
|
resolveRunOwnerHost: () => resolveRunOwnerHost,
|
|
101
|
-
runDenialOf: () => runDenialOf,
|
|
102
101
|
runRowIdentity: () => runRowIdentity,
|
|
103
102
|
serializeConfigWrite: () => serializeConfigWrite,
|
|
104
103
|
servePostProcessingRead: () => servePostProcessingRead,
|
|
@@ -136,7 +135,6 @@ var MIGRATIONS = [
|
|
|
136
135
|
llm_provider TEXT,
|
|
137
136
|
parallel INTEGER DEFAULT 0,
|
|
138
137
|
error_log TEXT,
|
|
139
|
-
memory_log TEXT,
|
|
140
138
|
test_id TEXT,
|
|
141
139
|
suite_id TEXT,
|
|
142
140
|
owner_pid INTEGER,
|
|
@@ -159,9 +157,7 @@ var MIGRATIONS = [
|
|
|
159
157
|
variables TEXT,
|
|
160
158
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
161
159
|
hook_id TEXT,
|
|
162
|
-
--
|
|
163
|
-
-- optional testIndex/stepIndex; NULL means a run-scoped hook, which is a real state and
|
|
164
|
-
-- not a missing value.
|
|
160
|
+
-- Where in the run tree this hook ran; NULL means a run-scoped hook, not a missing value.
|
|
165
161
|
test_index INTEGER,
|
|
166
162
|
step_index INTEGER
|
|
167
163
|
)`,
|
|
@@ -175,10 +171,7 @@ var MIGRATIONS = [
|
|
|
175
171
|
)`,
|
|
176
172
|
`CREATE TABLE IF NOT EXISTS run_artifacts (
|
|
177
173
|
run_id TEXT PRIMARY KEY REFERENCES runs(id) ON DELETE CASCADE,
|
|
178
|
-
--
|
|
179
|
-
-- The set used to admit two more, named for the parent/child run tree 402-09 retired; no
|
|
180
|
-
-- writer has produced either since, so admitting them was the schema agreeing to store a
|
|
181
|
-
-- distinction the product no longer makes.
|
|
174
|
+
-- Two kinds, because no writer mints any other.
|
|
182
175
|
kind TEXT NOT NULL CHECK(kind IN ('test', 'unknown')),
|
|
183
176
|
schema_version INTEGER NOT NULL,
|
|
184
177
|
payload TEXT NOT NULL DEFAULT '{}',
|
|
@@ -186,17 +179,7 @@ var MIGRATIONS = [
|
|
|
186
179
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
187
180
|
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
188
181
|
)`,
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
// THE COLUMNS ARE THE PAYLOAD'S OWN NAMES, snake_cased and nothing else. No field is
|
|
192
|
-
// re-derived, renamed or re-interpreted on the way in — that is D-01's whole claim, and
|
|
193
|
-
// it is what makes the read a mirror rather than a second opinion. Nested payload objects
|
|
194
|
-
// (`planned_action`, `grounding`, `result`, `artifact_refs`) are `JSON.stringify` of the
|
|
195
|
-
// field VERBATIM; the projection never reaches inside one.
|
|
196
|
-
//
|
|
197
|
-
// `test_index` IS THE HANDLE (RESEARCH D4). `handleId(runId, testIndex)` is how this
|
|
198
|
-
// product spells run-scoped member identity, so minting a second id here would be a
|
|
199
|
-
// second spelling of the same fact.
|
|
182
|
+
// Columns are the payload's names snake_cased; `test_index` is the handle, so no second id.
|
|
200
183
|
`CREATE TABLE IF NOT EXISTS run_members (
|
|
201
184
|
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
202
185
|
test_index INTEGER NOT NULL,
|
|
@@ -211,15 +194,12 @@ var MIGRATIONS = [
|
|
|
211
194
|
duration_ms INTEGER,
|
|
212
195
|
failure_summary TEXT,
|
|
213
196
|
retry_count INTEGER,
|
|
214
|
-
--
|
|
197
|
+
-- Starts at 1 and rises on testRetryScheduled; never reset by an upsert.
|
|
215
198
|
attempts INTEGER NOT NULL DEFAULT 1,
|
|
216
199
|
recording_json TEXT,
|
|
217
200
|
PRIMARY KEY (run_id, test_index)
|
|
218
201
|
)`,
|
|
219
|
-
//
|
|
220
|
-
// declared these `INTEGER DEFAULT 0`, and that zero was a measurement nobody made — the exact
|
|
221
|
-
// lie this shape refuses to be able to tell. 405-04 landed the producer, so a step the runner
|
|
222
|
-
// measured carries real counts and a step it did not carries NULL, which stays honest.
|
|
202
|
+
// Token columns are nullable with no default, so a step nobody measured carries NULL, not 0.
|
|
223
203
|
`CREATE TABLE IF NOT EXISTS run_steps (
|
|
224
204
|
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
225
205
|
test_index INTEGER NOT NULL,
|
|
@@ -741,9 +721,7 @@ var DashboardDatabase = class {
|
|
|
741
721
|
if (owner) this.ownStamps.set(id, owner);
|
|
742
722
|
return id;
|
|
743
723
|
}
|
|
744
|
-
//
|
|
745
|
-
// running row; any other row is refused and left untouched. An adopt that names no name or
|
|
746
|
-
// attributes keeps the row's own (443-04).
|
|
724
|
+
// Inserts a fresh row or adopts this handle's own running row; any other row is left untouched.
|
|
747
725
|
claimRunningRow(id, fields) {
|
|
748
726
|
const stamp = this.ownStamps.get(id) ?? this.readOwnStamp();
|
|
749
727
|
const result = this.db.prepare(`
|
|
@@ -812,10 +790,6 @@ var DashboardDatabase = class {
|
|
|
812
790
|
fields.push("error_log = ?");
|
|
813
791
|
values.push(updates.errorLog);
|
|
814
792
|
}
|
|
815
|
-
if (updates.memoryLog !== void 0) {
|
|
816
|
-
fields.push("memory_log = ?");
|
|
817
|
-
values.push(updates.memoryLog);
|
|
818
|
-
}
|
|
819
793
|
if (updates.attributes !== void 0) {
|
|
820
794
|
fields.push("attributes = ?");
|
|
821
795
|
values.push(JSON.stringify(updates.attributes));
|
|
@@ -918,26 +892,8 @@ var DashboardDatabase = class {
|
|
|
918
892
|
this.ownStamps.set(id, stamp);
|
|
919
893
|
return true;
|
|
920
894
|
}
|
|
921
|
-
//
|
|
922
|
-
//
|
|
923
|
-
// SIX MEMBERS, AND EVERY ONE OF THEM MIRRORS. The columns are the bus payload's own names; the
|
|
924
|
-
// nested objects are `JSON.stringify` of the field verbatim. Nothing in this block decides what
|
|
925
|
-
// a status means, re-derives a duration or fills a token count — a projection that interpreted
|
|
926
|
-
// would be a second opinion about a run the event log already settled.
|
|
927
|
-
/**
|
|
928
|
-
* Write or refresh ONE member row, keyed by the handle `(run_id, test_index)`.
|
|
929
|
-
*
|
|
930
|
-
* AN UPSERT AND NOT TWO WRITES, because the member row is fed by two events at two times:
|
|
931
|
-
* `testStarted` knows the identity fields and `testFinished` knows the outcome — and D-18
|
|
932
|
-
* requires a member whose stream begins with a terminal event to render anyway, so the second
|
|
933
|
-
* one alone has to be able to create the row.
|
|
934
|
-
*
|
|
935
|
-
* ONLY SUPPLIED FIELDS MOVE. `COALESCE(excluded.x, run_members.x)` on every optional column, so
|
|
936
|
-
* a `testFinished` that carries no name cannot blank the name `testStarted` wrote. `attempts` is
|
|
937
|
-
* excluded from the update entirely: it is the D-17 retry ordinal, owned by
|
|
938
|
-
* `incrementRunMemberAttempts`, and an upsert that reset it would erase the fact the header
|
|
939
|
-
* renders.
|
|
940
|
-
*/
|
|
895
|
+
// The projection mirrors bus payload names verbatim and interprets nothing.
|
|
896
|
+
// An upsert because a member's first event may be terminal; `COALESCE` keeps unsupplied fields.
|
|
941
897
|
upsertRunMember(member) {
|
|
942
898
|
this.db.prepare(`
|
|
943
899
|
INSERT INTO run_members (
|
|
@@ -975,26 +931,13 @@ var DashboardDatabase = class {
|
|
|
975
931
|
member.recording === void 0 ? null : JSON.stringify(member.recording)
|
|
976
932
|
);
|
|
977
933
|
}
|
|
978
|
-
|
|
979
|
-
* Raise ONE member's attempt ordinal (D-17). A no-op when the member row does not exist yet —
|
|
980
|
-
* a retry scheduled before any identity event is a real ordering, and inventing a row here
|
|
981
|
-
* would put a member on the page that nothing has said anything about.
|
|
982
|
-
*/
|
|
934
|
+
// A no-op without a member row, so a retry before any identity event adds no member.
|
|
983
935
|
incrementRunMemberAttempts(runId, testIndex) {
|
|
984
936
|
this.db.prepare(
|
|
985
937
|
"UPDATE run_members SET attempts = attempts + 1 WHERE run_id = ? AND test_index = ?"
|
|
986
938
|
).run(runId, testIndex);
|
|
987
939
|
}
|
|
988
|
-
|
|
989
|
-
* Drop ONE HANDLE's projected step and sub-action rows (D-17).
|
|
990
|
-
*
|
|
991
|
-
* SCOPED TO THE HANDLE, which was the whole change from the v1 run-wide step delete this
|
|
992
|
-
* replaced (deleted outright by 405-07's census): that member wiped every step of the RUN on any
|
|
993
|
-
* member's retry, so in a suite the abandoned attempt of test 3 deleted the completed history of
|
|
994
|
-
* tests 0-2. The member row itself survives — its outcome and its attempt count are facts about
|
|
995
|
-
* the member, not about the attempt. So do its setup and teardown hook rows, which run once per
|
|
996
|
-
* member outside the retry loop; only the attempt's `inline` hook rows go (433-41, CR-01).
|
|
997
|
-
*/
|
|
940
|
+
// Scoped to the handle: the member row and its setup and teardown hooks survive a retry.
|
|
998
941
|
deleteMemberProjection(runId, testIndex) {
|
|
999
942
|
this.db.prepare("DELETE FROM run_steps WHERE run_id = ? AND test_index = ?").run(runId, testIndex);
|
|
1000
943
|
this.db.prepare("DELETE FROM run_sub_actions WHERE run_id = ? AND test_index = ?").run(runId, testIndex);
|
|
@@ -1002,12 +945,7 @@ var DashboardDatabase = class {
|
|
|
1002
945
|
"DELETE FROM execution_logs WHERE run_id = ? AND test_index = ? AND phase = 'inline'"
|
|
1003
946
|
).run(runId, testIndex);
|
|
1004
947
|
}
|
|
1005
|
-
|
|
1006
|
-
* Write ONE finalized step row. `INSERT OR REPLACE` because a re-run of the same handle re-emits
|
|
1007
|
-
* the same `(test_index, step_index)` and the LAST attempt is the one the page renders (D-17).
|
|
1008
|
-
*
|
|
1009
|
-
* The three token columns stay NULL when `tokenUsage` is absent — never 0 (D-05).
|
|
1010
|
-
*/
|
|
948
|
+
// `INSERT OR REPLACE`, so a re-run's last attempt wins; tokens stay NULL when absent, never 0.
|
|
1011
949
|
insertRunStep(step) {
|
|
1012
950
|
this.db.prepare(`
|
|
1013
951
|
INSERT OR REPLACE INTO run_steps (
|
|
@@ -1067,14 +1005,7 @@ var DashboardDatabase = class {
|
|
|
1067
1005
|
subAction.recordingOffsetMs ?? null
|
|
1068
1006
|
);
|
|
1069
1007
|
}
|
|
1070
|
-
|
|
1071
|
-
* THE run-detail read (D-03). `undefined` when no `runs` row exists — absence is a RESULT here,
|
|
1072
|
-
* and turning it into a typed refusal is the `runs.detail` op's job, so the "not found"
|
|
1073
|
-
* vocabulary lives in the function layer rather than in this store.
|
|
1074
|
-
*
|
|
1075
|
-
* A run that exists and projected nothing answers three empty arrays, which is a DIFFERENT fact
|
|
1076
|
-
* and stays a different answer.
|
|
1077
|
-
*/
|
|
1008
|
+
// `undefined` when no run row exists; a run that projected nothing answers three empty arrays.
|
|
1078
1009
|
readRunDetailRows(runId) {
|
|
1079
1010
|
const run = this.db.prepare("SELECT id FROM runs WHERE id = ?").get(runId);
|
|
1080
1011
|
if (run === void 0) return void 0;
|
|
@@ -1214,7 +1145,7 @@ var DashboardDatabase = class {
|
|
|
1214
1145
|
offset: opts.offset
|
|
1215
1146
|
}));
|
|
1216
1147
|
}
|
|
1217
|
-
//
|
|
1148
|
+
// Members are read for the returned rows only, after pagination.
|
|
1218
1149
|
withMemberSummaries(runs) {
|
|
1219
1150
|
for (let offset = 0; offset < runs.length; offset += 400) {
|
|
1220
1151
|
const batch = runs.slice(offset, offset + 400);
|
|
@@ -1257,7 +1188,7 @@ var DashboardDatabase = class {
|
|
|
1257
1188
|
owner: { pid: row.owner_pid, incarnation: row.owner_incarnation, host: row.owner_host ?? "" }
|
|
1258
1189
|
}));
|
|
1259
1190
|
}
|
|
1260
|
-
//
|
|
1191
|
+
// One compare-and-set on the stamped owner; never read-then-write.
|
|
1261
1192
|
finalizeOwnRun(id, updates) {
|
|
1262
1193
|
const fields = ["status = ?", "ended_at = ?"];
|
|
1263
1194
|
const values = [updates.status, updates.endedAt];
|
|
@@ -1279,8 +1210,7 @@ var DashboardDatabase = class {
|
|
|
1279
1210
|
this.ownStamps.delete(id);
|
|
1280
1211
|
return true;
|
|
1281
1212
|
}
|
|
1282
|
-
//
|
|
1283
|
-
// moved the row off running; the owner columns alone decide whose row it is.
|
|
1213
|
+
// No status condition, as the verdict may have moved the row off running; owner columns decide.
|
|
1284
1214
|
closeOwnRun(id, updates) {
|
|
1285
1215
|
const stamp = this.ownStamps.get(id) ?? this.readOwnStamp();
|
|
1286
1216
|
const result = this.db.prepare(`
|
|
@@ -1291,8 +1221,7 @@ var DashboardDatabase = class {
|
|
|
1291
1221
|
this.ownStamps.delete(id);
|
|
1292
1222
|
return true;
|
|
1293
1223
|
}
|
|
1294
|
-
//
|
|
1295
|
-
// this handle's own write, never an inference about another process.
|
|
1224
|
+
// Own is the stamp this handle wrote, so a NULL start time here is this handle's own write.
|
|
1296
1225
|
isOwnStamp(id, owner) {
|
|
1297
1226
|
const stamp = this.ownStamps.get(id);
|
|
1298
1227
|
return stamp !== void 0 && stamp.pid === owner.pid && stamp.incarnation === owner.incarnation && stamp.host === owner.host;
|
|
@@ -1391,20 +1320,7 @@ var DashboardDatabase = class {
|
|
|
1391
1320
|
`).all(...params);
|
|
1392
1321
|
return rows.map((row) => ({ value: row.value, count: row.count }));
|
|
1393
1322
|
}
|
|
1394
|
-
|
|
1395
|
-
* Delete ONE run and everything the schema hangs off it (405-07, D-02/D-14 · T-405-27).
|
|
1396
|
-
*
|
|
1397
|
-
* ONE ID, AND THE CASCADE DOES THE REST. This used to open with a recursive `run_tree` CTE that
|
|
1398
|
-
* walked the child rows of a suite parent, then hand-delete their step and trace rows in a
|
|
1399
|
-
* transaction. Nothing has written a child row since 402-09 — one `runs` row per run, member
|
|
1400
|
-
* identity on `run_members` — so the walk could only ever return the one row it was seeded with,
|
|
1401
|
-
* and the hand-deletes named tables this schema no longer has. Every table that references a run
|
|
1402
|
-
* declares `ON DELETE CASCADE`, so the single statement below is the whole deletion.
|
|
1403
|
-
*
|
|
1404
|
-
* AND IT TOUCHES NO FILE. The two path arrays it used to collect fed the router's artifact
|
|
1405
|
-
* cleanup; both are gone. The run's directory under `.agent-qa/runs/` — its event log and its
|
|
1406
|
-
* content-addressed blobs — is the run's own record and is not the dashboard's to remove.
|
|
1407
|
-
*/
|
|
1323
|
+
// Every table referencing a run cascades on delete, and no file is touched.
|
|
1408
1324
|
deleteRun(id) {
|
|
1409
1325
|
const changes = this.db.prepare("DELETE FROM runs WHERE id = ?").run(id);
|
|
1410
1326
|
const deleted = Number(changes.changes ?? 0) > 0;
|
|
@@ -1491,15 +1407,7 @@ var DashboardDatabase = class {
|
|
|
1491
1407
|
getRunArtifactBundle(runId) {
|
|
1492
1408
|
return { artifact: this.getRunArtifact(runId) };
|
|
1493
1409
|
}
|
|
1494
|
-
|
|
1495
|
-
* The runs a self-heal happened on (405-07, D-02).
|
|
1496
|
-
*
|
|
1497
|
-
* READ OFF `run_steps.self_heal_occurred`, WHICH IS THE SURVIVING HEALING FACT. This scanned the
|
|
1498
|
-
* v1 wide step table for `status = 'healed'`; that table had no producer left and its readers
|
|
1499
|
-
* went with it. `selfHealOccurred` is the bus payload's own boolean, projected verbatim by the
|
|
1500
|
-
* subscriber — so the flake rate below is now computed from what the runner actually reported
|
|
1501
|
-
* rather than from a status word a retired projection wrote.
|
|
1502
|
-
*/
|
|
1410
|
+
// Read off `run_steps.self_heal_occurred`, the bus payload's own boolean.
|
|
1503
1411
|
getHealedRunIds(runIds) {
|
|
1504
1412
|
if (runIds.length === 0) return /* @__PURE__ */ new Set();
|
|
1505
1413
|
const placeholders = runIds.map(() => "?").join(", ");
|
|
@@ -1522,7 +1430,7 @@ var DashboardDatabase = class {
|
|
|
1522
1430
|
passed,
|
|
1523
1431
|
failed,
|
|
1524
1432
|
flaky,
|
|
1525
|
-
//
|
|
1433
|
+
// A group's rate is passed ÷ (passed + failed); neither is no rate.
|
|
1526
1434
|
passRate: passed + failed > 0 ? passed / (passed + failed) : null,
|
|
1527
1435
|
flakeRate: totalRuns > 0 ? flaky / totalRuns : 0,
|
|
1528
1436
|
avgDuration: totalRuns > 0 ? totalDuration / totalRuns : 0
|
|
@@ -1584,28 +1492,6 @@ var DashboardDatabase = class {
|
|
|
1584
1492
|
FROM run_steps
|
|
1585
1493
|
WHERE run_id IN (${runIds.map(() => "?").join(",")})
|
|
1586
1494
|
`).get(...runIds).total_tokens || 0 : 0;
|
|
1587
|
-
const memorySummary = {
|
|
1588
|
-
runs: 0,
|
|
1589
|
-
added: 0,
|
|
1590
|
-
confirmed: 0,
|
|
1591
|
-
deprecated: 0,
|
|
1592
|
-
deleted: 0,
|
|
1593
|
-
curatorTokens: 0
|
|
1594
|
-
};
|
|
1595
|
-
for (const run of runs) {
|
|
1596
|
-
if (!run.memoryLog) continue;
|
|
1597
|
-
memorySummary.runs++;
|
|
1598
|
-
try {
|
|
1599
|
-
const parsed = JSON.parse(run.memoryLog);
|
|
1600
|
-
memorySummary.added += Number(parsed.added ?? 0) || 0;
|
|
1601
|
-
memorySummary.confirmed += Number(parsed.confirmed ?? 0) || 0;
|
|
1602
|
-
memorySummary.deprecated += Number(parsed.deprecated ?? 0) || 0;
|
|
1603
|
-
memorySummary.deleted += Number(parsed.deleted ?? 0) || 0;
|
|
1604
|
-
const tokenUsage = parsed.tokenUsage && typeof parsed.tokenUsage === "object" ? parsed.tokenUsage : {};
|
|
1605
|
-
memorySummary.curatorTokens += Number(tokenUsage.totalTokens ?? 0) || 0;
|
|
1606
|
-
} catch {
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
1495
|
return {
|
|
1610
1496
|
totalRuns: summary.totalRuns,
|
|
1611
1497
|
passed: summary.passed,
|
|
@@ -1614,8 +1500,7 @@ var DashboardDatabase = class {
|
|
|
1614
1500
|
flakeRate: summary.flakeRate,
|
|
1615
1501
|
avgDuration: summary.avgDuration,
|
|
1616
1502
|
totalTokens,
|
|
1617
|
-
runs: daily
|
|
1618
|
-
memory: memorySummary
|
|
1503
|
+
runs: daily
|
|
1619
1504
|
};
|
|
1620
1505
|
}
|
|
1621
1506
|
getCostStats(opts = {}) {
|
|
@@ -1790,12 +1675,7 @@ var DashboardDatabase = class {
|
|
|
1790
1675
|
testRunUnmeasuredSteps: unmeasuredSteps
|
|
1791
1676
|
};
|
|
1792
1677
|
}
|
|
1793
|
-
|
|
1794
|
-
* The window's settled run ids, newest first, capped at `limit`, with their full count, and the
|
|
1795
|
-
* count of the window's runs still in flight (433-30, 433-42). A settled run has started and is
|
|
1796
|
-
* neither `pending` nor `running`; every other row in the window is in flight. Ids only: the
|
|
1797
|
-
* Insights knowledge-change read dispatches each through the agent host.
|
|
1798
|
-
*/
|
|
1678
|
+
// Settled run ids newest first, capped at `limit`, with the full and in-flight counts.
|
|
1799
1679
|
listInsightsWindowRunIds(opts) {
|
|
1800
1680
|
const conditions = [];
|
|
1801
1681
|
const params = [];
|
|
@@ -1951,7 +1831,7 @@ var DashboardDatabase = class {
|
|
|
1951
1831
|
const limit = opts.limit ?? 25;
|
|
1952
1832
|
return rows.slice(0, limit);
|
|
1953
1833
|
}
|
|
1954
|
-
//
|
|
1834
|
+
// Grouped by member file path; only pass- and fail-class outcomes count.
|
|
1955
1835
|
getFlakyTests(opts) {
|
|
1956
1836
|
const minRuns = opts?.minRuns ?? 3;
|
|
1957
1837
|
const limit = opts?.limit ?? 50;
|
|
@@ -2076,7 +1956,6 @@ var DashboardDatabase = class {
|
|
|
2076
1956
|
endedAt: r.ended_at,
|
|
2077
1957
|
failureSummary: r.failure_summary ?? null,
|
|
2078
1958
|
errorLog: r.error_log ?? null,
|
|
2079
|
-
memoryLog: r.memory_log ?? null,
|
|
2080
1959
|
testId: r.test_id ?? null,
|
|
2081
1960
|
suiteId: r.suite_id ?? null,
|
|
2082
1961
|
platform: r.platform ?? "web",
|
|
@@ -2400,11 +2279,7 @@ function createDashboardRunIngest(deps) {
|
|
|
2400
2279
|
...payload.status === void 0 ? {} : { status: payload.status },
|
|
2401
2280
|
...payload.durationMs === void 0 ? {} : { durationMs: payload.durationMs },
|
|
2402
2281
|
...payload.error === void 0 ? {} : { error: payload.error },
|
|
2403
|
-
//
|
|
2404
|
-
// columns 405-01 declared stop being always-NULL. Spread-when-defined, like every other
|
|
2405
|
-
// optional above it — the DB layer writes NULL for all three when the key is absent, and
|
|
2406
|
-
// a NULL row is what makes `runs.detail` able to say "M of K steps unmeasured" instead of
|
|
2407
|
-
// reporting a zero nobody measured.
|
|
2282
|
+
// Spread when defined, so an unmeasured step stays NULL and `runs.detail` counts it so.
|
|
2408
2283
|
...payload.tokenUsage === void 0 ? {} : { tokenUsage: payload.tokenUsage },
|
|
2409
2284
|
...payload.modelUsage === void 0 ? {} : { modelUsage: payload.modelUsage },
|
|
2410
2285
|
...payload.evidence === void 0 ? {} : { evidence: payload.evidence },
|
|
@@ -2454,8 +2329,7 @@ function createDashboardRunIngest(deps) {
|
|
|
2454
2329
|
stdout: readString(payload.stdout) ?? null,
|
|
2455
2330
|
stderr: readString(payload.stderr) ?? null,
|
|
2456
2331
|
variables: payload.variables ?? null,
|
|
2457
|
-
//
|
|
2458
|
-
// is a real state, and inventing handle 0 for it would put a setup hook inside a test.
|
|
2332
|
+
// Absent stays NULL: a run-scoped hook is a real state, not handle 0.
|
|
2459
2333
|
...payload.testIndex === void 0 ? {} : { testIndex: payload.testIndex },
|
|
2460
2334
|
...payload.stepIndex === void 0 ? {} : { stepIndex: payload.stepIndex }
|
|
2461
2335
|
});
|
|
@@ -2553,6 +2427,7 @@ var import_node_crypto9 = require("node:crypto");
|
|
|
2553
2427
|
var import_promises9 = require("node:fs/promises");
|
|
2554
2428
|
var import_node_path12 = require("node:path");
|
|
2555
2429
|
var import_node_os3 = require("node:os");
|
|
2430
|
+
var import_node_util5 = require("node:util");
|
|
2556
2431
|
|
|
2557
2432
|
// src/server/routes.ts
|
|
2558
2433
|
var import_node_crypto7 = require("node:crypto");
|
|
@@ -2727,7 +2602,7 @@ var TestFileManager = class {
|
|
|
2727
2602
|
constructor(workspace) {
|
|
2728
2603
|
this.workspace = workspace;
|
|
2729
2604
|
}
|
|
2730
|
-
//
|
|
2605
|
+
// Each list call reads the workspace itself; each file is written whole, one writer at a time.
|
|
2731
2606
|
underLock = createPathLock();
|
|
2732
2607
|
async list() {
|
|
2733
2608
|
const records = await (0, import_agent_qa_core3.discoverWorkspaceFiles)({ workspace: this.workspace, kind: "test" });
|
|
@@ -2891,8 +2766,7 @@ var ConfigManager = class _ConfigManager {
|
|
|
2891
2766
|
this.configPath = configPath;
|
|
2892
2767
|
this.mask = mask;
|
|
2893
2768
|
}
|
|
2894
|
-
//
|
|
2895
|
-
// add one. A missing file holds none.
|
|
2769
|
+
// A whole document may keep a marker the file already holds, as often, but never add one.
|
|
2896
2770
|
static addsShownMarker(current, next) {
|
|
2897
2771
|
const held = shownMarkerCounts(current);
|
|
2898
2772
|
for (const [marker, count] of shownMarkerCounts(next)) {
|
|
@@ -2900,13 +2774,13 @@ var ConfigManager = class _ConfigManager {
|
|
|
2900
2774
|
}
|
|
2901
2775
|
return false;
|
|
2902
2776
|
}
|
|
2903
|
-
//
|
|
2777
|
+
// A half-edited file is named, never served as the partial config yaml recovers.
|
|
2904
2778
|
async readWithRevision() {
|
|
2905
2779
|
const read = await this.readParsed();
|
|
2906
2780
|
if (read.kind !== "ok") return read;
|
|
2907
2781
|
return { kind: "ok", config: read.config, raw: read.raw, revision: read.revision };
|
|
2908
2782
|
}
|
|
2909
|
-
//
|
|
2783
|
+
// The payload and the file view pass through the declared mask and nothing else.
|
|
2910
2784
|
async readMasked() {
|
|
2911
2785
|
const read = await this.readParsed();
|
|
2912
2786
|
if (read.kind === "absent") {
|
|
@@ -2950,8 +2824,7 @@ var ConfigManager = class _ConfigManager {
|
|
|
2950
2824
|
revision
|
|
2951
2825
|
};
|
|
2952
2826
|
}
|
|
2953
|
-
//
|
|
2954
|
-
// read the same bytes and drop each other's field.
|
|
2827
|
+
// One transaction per request, serialised so overlapping autosaves never drop each other's field.
|
|
2955
2828
|
async applyChanges(changes, options = {}) {
|
|
2956
2829
|
const marked = changeWithShownMarker(changes);
|
|
2957
2830
|
if (marked !== void 0) {
|
|
@@ -2966,13 +2839,11 @@ var ConfigManager = class _ConfigManager {
|
|
|
2966
2839
|
}
|
|
2967
2840
|
return this.serialized(() => this.applyChangesOnce(changes, options.capturePriors === true));
|
|
2968
2841
|
}
|
|
2969
|
-
//
|
|
2970
|
-
// that no longer holds what the change left there was written since, and is never overwritten.
|
|
2842
|
+
// An Undo restores newest first; a field written after the change is never overwritten.
|
|
2971
2843
|
async restorePriors(priors) {
|
|
2972
2844
|
return this.serialized(() => this.restorePriorsOnce(priors));
|
|
2973
2845
|
}
|
|
2974
|
-
//
|
|
2975
|
-
// bytes it was built from, read again in this path's turn. Anything else is somebody's edit.
|
|
2846
|
+
// A generated whole-file change publishes only over the bytes it is built from, re-read in turn.
|
|
2976
2847
|
async replaceDocument(expectedRevision, content) {
|
|
2977
2848
|
return this.serialized(async () => {
|
|
2978
2849
|
let current;
|
|
@@ -3099,8 +2970,7 @@ var ConfigManager = class _ConfigManager {
|
|
|
3099
2970
|
await this.writeAtomic(next);
|
|
3100
2971
|
return capturePriors ? { kind: "ok", revision: revisionOf(next), baseRevision, priors } : { kind: "ok", revision: revisionOf(next), baseRevision };
|
|
3101
2972
|
}
|
|
3102
|
-
//
|
|
3103
|
-
// (chmod, because writeFile's mode is filtered by the umask), and replaces it in one rename.
|
|
2973
|
+
// The temp file sits beside the real one and is chmodded, as the umask filters `writeFile` modes.
|
|
3104
2974
|
async writeAtomic(content) {
|
|
3105
2975
|
const target = await this.writeTarget();
|
|
3106
2976
|
const directory = (0, import_node_path5.dirname)(target);
|
|
@@ -4300,6 +4170,12 @@ var HookRegistryManager = class {
|
|
|
4300
4170
|
|
|
4301
4171
|
// src/server/body-parser.ts
|
|
4302
4172
|
var DEFAULT_MAX_BYTES = 1024 * 1024;
|
|
4173
|
+
function carriesNonJsonBody(req) {
|
|
4174
|
+
const hasBody = req.headers["transfer-encoding"] !== void 0 || Number(req.headers["content-length"] ?? "0") > 0;
|
|
4175
|
+
if (!hasBody) return false;
|
|
4176
|
+
const type = req.headers["content-type"];
|
|
4177
|
+
return type === void 0 || type.split(";", 1)[0].trim().toLowerCase() !== "application/json";
|
|
4178
|
+
}
|
|
4303
4179
|
function readJsonBody(req, maxBytes = DEFAULT_MAX_BYTES) {
|
|
4304
4180
|
return new Promise((resolve7, reject) => {
|
|
4305
4181
|
const chunks = [];
|
|
@@ -4334,6 +4210,7 @@ function readJsonBody(req, maxBytes = DEFAULT_MAX_BYTES) {
|
|
|
4334
4210
|
// src/live-editor/live-session.ts
|
|
4335
4211
|
var import_node_crypto6 = require("node:crypto");
|
|
4336
4212
|
var import_node_path9 = require("node:path");
|
|
4213
|
+
var import_node_util3 = require("node:util");
|
|
4337
4214
|
var import_agent_qa_core8 = require("@vostride/agent-qa-core");
|
|
4338
4215
|
var import_agent_qa_runner = require("@vostride/agent-qa-runner");
|
|
4339
4216
|
var import_agent_qa_runner_local = require("@vostride/agent-qa-runner-local");
|
|
@@ -4421,12 +4298,10 @@ async function loadLocalBindings(configPath) {
|
|
|
4421
4298
|
try {
|
|
4422
4299
|
const content = await (0, import_promises6.readFile)(filePath, "utf-8");
|
|
4423
4300
|
const parsed = (0, import_yaml5.parse)(content);
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
filePath
|
|
4429
|
-
} : { filePath };
|
|
4301
|
+
(0, import_agent_qa_core7.refuseRetiredLocalBindings)(parsed, filePath);
|
|
4302
|
+
if (!parsed || typeof parsed !== "object") return { filePath };
|
|
4303
|
+
const bindings = parsed;
|
|
4304
|
+
return { devices: bindings.devices, apps: bindings.apps, filePath };
|
|
4430
4305
|
} catch (err) {
|
|
4431
4306
|
if (err.code === "ENOENT") return null;
|
|
4432
4307
|
throw err;
|
|
@@ -4516,44 +4391,15 @@ async function prepareMobileLiveSession(input) {
|
|
|
4516
4391
|
appActivity: resolved.app.appActivity,
|
|
4517
4392
|
deepLinkAppId: resolved.app.deepLinkAppId,
|
|
4518
4393
|
appPath: resolved.app.install?.path,
|
|
4519
|
-
browserstackApp: resolved.app.install?.browserstack,
|
|
4520
4394
|
appState: resolved.appState
|
|
4521
4395
|
}
|
|
4522
4396
|
};
|
|
4523
4397
|
}
|
|
4524
4398
|
|
|
4525
4399
|
// src/live-editor/live-session.ts
|
|
4526
|
-
var LiveProductionRefusedError = class extends Error {
|
|
4527
|
-
productionTargetNames;
|
|
4528
|
-
constructor(productionTargetNames) {
|
|
4529
|
-
super((0, import_agent_qa_schema3.buildProductionRefusal)(productionTargetNames));
|
|
4530
|
-
this.name = "LiveProductionRefusedError";
|
|
4531
|
-
this.productionTargetNames = (0, import_agent_qa_schema3.distinctInFirstOccurrenceOrder)(productionTargetNames);
|
|
4532
|
-
}
|
|
4533
|
-
};
|
|
4534
4400
|
function isConfigRecord(value) {
|
|
4535
4401
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4536
4402
|
}
|
|
4537
|
-
function liveTargetName(targetName) {
|
|
4538
|
-
if (typeof targetName !== "string") return void 0;
|
|
4539
|
-
const trimmed = targetName.trim();
|
|
4540
|
-
return trimmed.length > 0 ? trimmed : void 0;
|
|
4541
|
-
}
|
|
4542
|
-
async function resolveLiveTargetProduction(configManager, targetName) {
|
|
4543
|
-
const name = liveTargetName(targetName);
|
|
4544
|
-
if (name === void 0 || configManager === void 0) {
|
|
4545
|
-
return { productionTargetNames: [], allowProduction: false };
|
|
4546
|
-
}
|
|
4547
|
-
const config = await readConfigValues(configManager);
|
|
4548
|
-
const registry = isConfigRecord(config.registry) ? config.registry : void 0;
|
|
4549
|
-
const targets = registry !== void 0 && isConfigRecord(registry.targets) ? registry.targets : void 0;
|
|
4550
|
-
const declared = targets !== void 0 && Object.hasOwn(targets, name) ? targets[name] : void 0;
|
|
4551
|
-
const production = isConfigRecord(declared) && declared.environment === "production";
|
|
4552
|
-
return {
|
|
4553
|
-
productionTargetNames: production ? [name] : [],
|
|
4554
|
-
allowProduction: config.allowProduction === true
|
|
4555
|
-
};
|
|
4556
|
-
}
|
|
4557
4403
|
async function readEntityUse(entity, readers) {
|
|
4558
4404
|
if (entity === void 0) return void 0;
|
|
4559
4405
|
const find = entity.type === "suite" ? readers.findSuite : readers.findTest;
|
|
@@ -4573,6 +4419,12 @@ var LivePlannerLimitsRefusedError = class extends Error {
|
|
|
4573
4419
|
this.name = "LivePlannerLimitsRefusedError";
|
|
4574
4420
|
}
|
|
4575
4421
|
};
|
|
4422
|
+
var LiveNavigationTimeoutRefusedError = class extends Error {
|
|
4423
|
+
constructor(message) {
|
|
4424
|
+
super(message);
|
|
4425
|
+
this.name = "LiveNavigationTimeoutRefusedError";
|
|
4426
|
+
}
|
|
4427
|
+
};
|
|
4576
4428
|
var LIVE_LIMIT_LEAVES = ["maxActions", "maxPlanningTurns"];
|
|
4577
4429
|
function shownLimitValue(value) {
|
|
4578
4430
|
return typeof value === "number" ? String(value) : JSON.stringify(value) ?? String(value);
|
|
@@ -4621,9 +4473,30 @@ async function resolveLivePlannerLimits(configManager, entities, readers) {
|
|
|
4621
4473
|
}
|
|
4622
4474
|
return (0, import_agent_qa_schema3.resolvePlannerLimits)(config.use, ...uses);
|
|
4623
4475
|
}
|
|
4624
|
-
function
|
|
4625
|
-
|
|
4626
|
-
|
|
4476
|
+
async function resolveLiveNavigationTimeoutMs(configManager, entities, readers) {
|
|
4477
|
+
const config = configManager === void 0 ? {} : await readConfigValues(configManager);
|
|
4478
|
+
const layers = [
|
|
4479
|
+
{ layer: "project config", use: config.use }
|
|
4480
|
+
];
|
|
4481
|
+
for (const entity of [].concat(entities ?? [])) {
|
|
4482
|
+
const use = await readEntityUse(entity, readers);
|
|
4483
|
+
layers.push({ layer: `${entity.type} file ${entity.id}`, use });
|
|
4484
|
+
}
|
|
4485
|
+
let navigation;
|
|
4486
|
+
for (const { layer, use } of layers) {
|
|
4487
|
+
if (!isConfigRecord(use) || !isConfigRecord(use.timeout)) continue;
|
|
4488
|
+
if (!Object.hasOwn(use.timeout, "navigation")) continue;
|
|
4489
|
+
const value = use.timeout.navigation;
|
|
4490
|
+
const parsed = import_agent_qa_core8.TimeoutConfigSchema.shape.navigation.safeParse(value);
|
|
4491
|
+
if (!parsed.success) {
|
|
4492
|
+
const issue = parsed.error.issues[0]?.message ?? "invalid value";
|
|
4493
|
+
throw new LiveNavigationTimeoutRefusedError(
|
|
4494
|
+
`${layer} sets use.timeout.navigation to ${shownLimitValue(value)}: ${issue}. The CLI refuses this file too.`
|
|
4495
|
+
);
|
|
4496
|
+
}
|
|
4497
|
+
navigation = parsed.data;
|
|
4498
|
+
}
|
|
4499
|
+
return navigation;
|
|
4627
4500
|
}
|
|
4628
4501
|
var IDLE_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
4629
4502
|
var LIVE_HUB_CLOSE_LIMIT_MS = 2e3;
|
|
@@ -4759,8 +4632,7 @@ var LiveSession = class {
|
|
|
4759
4632
|
planner = null;
|
|
4760
4633
|
verifier = null;
|
|
4761
4634
|
modelProfiles = null;
|
|
4762
|
-
//
|
|
4763
|
-
// rooted at the session's `.agent-qa` runtime dir (the FsStorageStateStore rooting idiom).
|
|
4635
|
+
// Built lazily on first use, rooted at the session's `.agent-qa` runtime dir.
|
|
4764
4636
|
storageStateStore = null;
|
|
4765
4637
|
model = null;
|
|
4766
4638
|
variableStore = null;
|
|
@@ -4788,15 +4660,14 @@ var LiveSession = class {
|
|
|
4788
4660
|
mobileAppiumLease = null;
|
|
4789
4661
|
secretStore;
|
|
4790
4662
|
secretRedactor;
|
|
4791
|
-
//
|
|
4792
|
-
// setup hooks failed has no adapter and therefore no hub; its subject declares `none`.
|
|
4663
|
+
// One hub per session at handle 0, created once the adapter exists; no adapter means no hub.
|
|
4793
4664
|
liveHub = null;
|
|
4794
4665
|
attachment = null;
|
|
4795
4666
|
resolveEnded = () => {
|
|
4796
4667
|
};
|
|
4797
4668
|
ended;
|
|
4798
4669
|
cleanupRun = null;
|
|
4799
|
-
//
|
|
4670
|
+
// Which control socket is the current one; only its close may detach the session.
|
|
4800
4671
|
attachmentGeneration = 0;
|
|
4801
4672
|
releaseAttachment = null;
|
|
4802
4673
|
get modelName() {
|
|
@@ -4818,11 +4689,7 @@ var LiveSession = class {
|
|
|
4818
4689
|
hub.register(0, registration?.capability, registration?.fallback);
|
|
4819
4690
|
this.liveHub = hub;
|
|
4820
4691
|
}
|
|
4821
|
-
|
|
4822
|
-
* Make a control socket the current one. `release` is how the session lets go of it later: the
|
|
4823
|
-
* previous attachment's is called with `superseded` here (428 WR-01), so one socket drives the
|
|
4824
|
-
* session at a time.
|
|
4825
|
-
*/
|
|
4692
|
+
// The previous attachment's `release` gets `superseded`, so one socket drives the session.
|
|
4826
4693
|
beginAttachment(release) {
|
|
4827
4694
|
const previous = this.releaseAttachment;
|
|
4828
4695
|
this.attachmentGeneration += 1;
|
|
@@ -4841,11 +4708,7 @@ var LiveSession = class {
|
|
|
4841
4708
|
}
|
|
4842
4709
|
this.pendingMessages = [];
|
|
4843
4710
|
}
|
|
4844
|
-
|
|
4845
|
-
* A command's reply, for whichever control socket is attached now (428 CR-01). A command can
|
|
4846
|
-
* outlive the socket that issued it, and its outcome belongs to the page, not to that socket:
|
|
4847
|
-
* with none attached it is held and flushed to the next one.
|
|
4848
|
-
*/
|
|
4711
|
+
// A reply can outlive its socket, so it goes to the current socket or waits for the next one.
|
|
4849
4712
|
deliver(message) {
|
|
4850
4713
|
if (this.messageSink) {
|
|
4851
4714
|
this.messageSink(message);
|
|
@@ -4956,7 +4819,7 @@ var LiveSession = class {
|
|
|
4956
4819
|
continue;
|
|
4957
4820
|
}
|
|
4958
4821
|
const result = await (0, import_agent_qa_runner.runHooks)([hookDef], {
|
|
4959
|
-
executor: new import_agent_qa_runner_local.DockerHookExecutor(),
|
|
4822
|
+
executor: new import_agent_qa_runner_local.DockerHookExecutor({ version: (0, import_agent_qa_core8.getAgentQaVersion)() }),
|
|
4960
4823
|
env: this.getHookEnvVars(),
|
|
4961
4824
|
secretStore: this.secretStore,
|
|
4962
4825
|
secretRedactor: this.secretRedactor
|
|
@@ -5030,7 +4893,6 @@ var LiveSession = class {
|
|
|
5030
4893
|
device: config.device ? {
|
|
5031
4894
|
name: config.device.name,
|
|
5032
4895
|
platform,
|
|
5033
|
-
transport: "local",
|
|
5034
4896
|
match: config.device
|
|
5035
4897
|
} : void 0
|
|
5036
4898
|
};
|
|
@@ -5095,15 +4957,6 @@ var LiveSession = class {
|
|
|
5095
4957
|
}
|
|
5096
4958
|
}
|
|
5097
4959
|
async initialize(config) {
|
|
5098
|
-
const found = await resolveLiveTargetProduction(this.deps.configManager, config.targetName);
|
|
5099
|
-
const refusal = liveProductionRefusal({
|
|
5100
|
-
productionTargetNames: (0, import_agent_qa_schema3.distinctInFirstOccurrenceOrder)([
|
|
5101
|
-
...config.production?.productionTargetNames ?? [],
|
|
5102
|
-
...found.productionTargetNames
|
|
5103
|
-
]),
|
|
5104
|
-
allowProduction: found.allowProduction
|
|
5105
|
-
});
|
|
5106
|
-
if (refusal !== void 0) throw refusal;
|
|
5107
4960
|
this.platform = config.platform;
|
|
5108
4961
|
this.targetName = typeof config.targetName === "string" && config.targetName.trim().length > 0 ? config.targetName.trim() : null;
|
|
5109
4962
|
this._modelName = config.llmConfig.model ?? "unknown";
|
|
@@ -5138,7 +4991,11 @@ var LiveSession = class {
|
|
|
5138
4991
|
} else {
|
|
5139
4992
|
const { WebPlatformAdapter } = await import("@vostride/agent-qa-web");
|
|
5140
4993
|
const adapter = new WebPlatformAdapter();
|
|
5141
|
-
await adapter.setup({
|
|
4994
|
+
await adapter.setup({
|
|
4995
|
+
platform: "web",
|
|
4996
|
+
browser: { name: "chromium", headless: config.headless ?? true },
|
|
4997
|
+
...config.navigationTimeoutMs === void 0 ? {} : { timeouts: { navigation: config.navigationTimeoutMs } }
|
|
4998
|
+
});
|
|
5142
4999
|
this.adapter = adapter;
|
|
5143
5000
|
if (config.url) {
|
|
5144
5001
|
const resolvedUrl = this.resolveSecrets(config.url);
|
|
@@ -5200,7 +5057,7 @@ var LiveSession = class {
|
|
|
5200
5057
|
}
|
|
5201
5058
|
const startedAt = performance.now();
|
|
5202
5059
|
try {
|
|
5203
|
-
const result = await new import_agent_qa_runner_local.DockerHookExecutor().execute(hookDef, {
|
|
5060
|
+
const result = await new import_agent_qa_runner_local.DockerHookExecutor({ version: (0, import_agent_qa_core8.getAgentQaVersion)() }).execute(hookDef, {
|
|
5204
5061
|
env: this.getHookEnvVars(),
|
|
5205
5062
|
secretEnv: (0, import_agent_qa_runner.resolveSecretEnv)(this.secretStore),
|
|
5206
5063
|
secretRedactor: this.secretRedactor
|
|
@@ -5367,12 +5224,7 @@ var LiveSession = class {
|
|
|
5367
5224
|
};
|
|
5368
5225
|
return this.redact(result);
|
|
5369
5226
|
}
|
|
5370
|
-
//
|
|
5371
|
-
// lifecycle (executeStepCommand for single steps, executeTestCommand for
|
|
5372
|
-
// the per-test loop that runs many steps under one outer gate).
|
|
5373
|
-
// D-14 (Phase 360): lazily construct the run-scoped storage-state store, rooted at the session's
|
|
5374
|
-
// `.agent-qa` runtime dir. All fs writes live in runner-local (validate:arch storageNeutral);
|
|
5375
|
-
// the loop records only { name, fingerprint }. Absent configPath falls back to the cwd root.
|
|
5227
|
+
// Lazily built and rooted at the session's `.agent-qa` dir; fs writes stay in runner-local.
|
|
5376
5228
|
async getStorageStateStore() {
|
|
5377
5229
|
let store = this.storageStateStore;
|
|
5378
5230
|
if (!store) {
|
|
@@ -5511,7 +5363,9 @@ ${errorLines.join("\n")}`;
|
|
|
5511
5363
|
testLimits = this.plannerLimitsForTest !== void 0 && testDraft.testId.length > 0 ? await this.plannerLimitsForTest(testDraft.testId) : this.plannerLimits ?? (0, import_agent_qa_schema3.resolvePlannerLimits)();
|
|
5512
5364
|
} catch (error) {
|
|
5513
5365
|
if (!(error instanceof LivePlannerLimitsRefusedError)) throw error;
|
|
5514
|
-
|
|
5366
|
+
const line = `[live-session] executeTestCommand: ${error.message}`;
|
|
5367
|
+
const detail = { testName: testDraft.testName };
|
|
5368
|
+
(0, import_agent_qa_core8.printRunText)((0, import_node_util3.format)(line, detail), () => console.warn(line, detail));
|
|
5515
5369
|
return this.redact({
|
|
5516
5370
|
status: "failed",
|
|
5517
5371
|
duration: performance.now() - start,
|
|
@@ -5531,11 +5385,13 @@ ${errorLines.join("\n")}`;
|
|
|
5531
5385
|
if (!setupResult.allPassed) {
|
|
5532
5386
|
const failedHook = setupHookPayloads.find((h) => h.status === "failed");
|
|
5533
5387
|
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";
|
|
5534
|
-
|
|
5388
|
+
const line = `[live-session] executeTestCommand: ${detail}`;
|
|
5389
|
+
const context = {
|
|
5535
5390
|
testName: testDraft.testName,
|
|
5536
5391
|
setupHooks: testDraft.setup,
|
|
5537
5392
|
failedHook: failedHook ? { name: failedHook.hookName, stderr: failedHook.stderr, error: failedHook.error } : null
|
|
5538
|
-
}
|
|
5393
|
+
};
|
|
5394
|
+
(0, import_agent_qa_core8.printRunText)((0, import_node_util3.format)(line, context), () => console.warn(line, context));
|
|
5539
5395
|
return {
|
|
5540
5396
|
status: "failed",
|
|
5541
5397
|
duration: performance.now() - start,
|
|
@@ -5613,7 +5469,9 @@ ${errorLines.join("\n")}`;
|
|
|
5613
5469
|
topLevelError = failedHook ? `Teardown hook "${failedHook.hookName}" failed${failedHook.error ? `: ${failedHook.error}` : ""}` : "Teardown hook failed";
|
|
5614
5470
|
}
|
|
5615
5471
|
if (topLevelError) {
|
|
5616
|
-
|
|
5472
|
+
const line = `[live-session] executeTestCommand: ${topLevelError}`;
|
|
5473
|
+
const detail = { testName: testDraft.testName };
|
|
5474
|
+
(0, import_agent_qa_core8.printRunText)((0, import_node_util3.format)(line, detail), () => console.warn(line, detail));
|
|
5617
5475
|
}
|
|
5618
5476
|
return this.redact({
|
|
5619
5477
|
status: finalStatus,
|
|
@@ -5629,10 +5487,7 @@ ${errorLines.join("\n")}`;
|
|
|
5629
5487
|
this.status = "idle";
|
|
5630
5488
|
}
|
|
5631
5489
|
}
|
|
5632
|
-
//
|
|
5633
|
-
// hook payloads (hook-complete) for inclusion in the LiveTestResultPayload.
|
|
5634
|
-
// The capture sits beside the message sink rather than wrapping it, so a
|
|
5635
|
-
// control socket that reattaches mid-hook keeps its sink (428 CR-01).
|
|
5490
|
+
// Captures dispatched hook payloads beside the message sink, so a reattached socket keeps it.
|
|
5636
5491
|
async runLifecycleHooksCapturing(phase, hookIds, sink, owner = { scope: "suite" }) {
|
|
5637
5492
|
this.hookCaptures.add(sink);
|
|
5638
5493
|
try {
|
|
@@ -5876,6 +5731,21 @@ function refuseUnparseableRequest(res) {
|
|
|
5876
5731
|
});
|
|
5877
5732
|
res.end(body);
|
|
5878
5733
|
}
|
|
5734
|
+
function refuseForeignHostPage(res, port) {
|
|
5735
|
+
if (res.headersSent) {
|
|
5736
|
+
res.destroy();
|
|
5737
|
+
return;
|
|
5738
|
+
}
|
|
5739
|
+
const p = String(port);
|
|
5740
|
+
const body = `The agent-qa dashboard answers only at localhost, 127.0.0.1 or [::1].
|
|
5741
|
+
Open http://localhost:${p}/ on the machine it runs on, or forward the port with ssh -L ${p}:localhost:${p} <host>.
|
|
5742
|
+
`;
|
|
5743
|
+
res.writeHead(403, {
|
|
5744
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
5745
|
+
"Content-Length": Buffer.byteLength(body)
|
|
5746
|
+
});
|
|
5747
|
+
res.end(body);
|
|
5748
|
+
}
|
|
5879
5749
|
function backstopServerError(res, req, error) {
|
|
5880
5750
|
const kind = error instanceof Error ? error.constructor.name : typeof error;
|
|
5881
5751
|
console.error(
|
|
@@ -5955,10 +5825,6 @@ function respondJson(res, status, data) {
|
|
|
5955
5825
|
res.end(body);
|
|
5956
5826
|
}
|
|
5957
5827
|
function serveRunEvidenceRead(req, res, deps) {
|
|
5958
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req)) {
|
|
5959
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
5960
|
-
return;
|
|
5961
|
-
}
|
|
5962
5828
|
const query = new URL(req.url ?? "/", "http://localhost").searchParams;
|
|
5963
5829
|
const entries = [["runId", deps.runId]];
|
|
5964
5830
|
const seen = /* @__PURE__ */ new Set(["runId"]);
|
|
@@ -6000,10 +5866,6 @@ function respondFeedbackRead(res, deps, functionId, input, parse) {
|
|
|
6000
5866
|
});
|
|
6001
5867
|
}
|
|
6002
5868
|
function serveRunMarksHistoryRead(req, res, deps) {
|
|
6003
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
|
|
6004
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
6005
|
-
return;
|
|
6006
|
-
}
|
|
6007
5869
|
if (new URL(req.url ?? "/", "http://localhost").search !== "") {
|
|
6008
5870
|
respondJson(res, 400, { error: "invalid_marks_input" });
|
|
6009
5871
|
return;
|
|
@@ -6022,10 +5884,6 @@ function serveRunMarksHistoryRead(req, res, deps) {
|
|
|
6022
5884
|
);
|
|
6023
5885
|
}
|
|
6024
5886
|
function serveRunMarkPreviewRead(req, res, deps) {
|
|
6025
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
|
|
6026
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
6027
|
-
return;
|
|
6028
|
-
}
|
|
6029
5887
|
void readJsonBody(req).then((body) => {
|
|
6030
5888
|
const parsed = import_agent_qa_schema4.RunsMarkInputSchema.safeParse(body);
|
|
6031
5889
|
if (!parsed.success) {
|
|
@@ -6048,10 +5906,6 @@ function serveRunMarkPreviewRead(req, res, deps) {
|
|
|
6048
5906
|
});
|
|
6049
5907
|
}
|
|
6050
5908
|
function serveRunDetailRead(req, res, deps) {
|
|
6051
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
|
|
6052
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
6053
|
-
return;
|
|
6054
|
-
}
|
|
6055
5909
|
void readRunDetail(deps.host, deps.runId).then(
|
|
6056
5910
|
(outcome) => {
|
|
6057
5911
|
if (outcome.kind === "refused") {
|
|
@@ -6201,10 +6055,6 @@ async function readRunAnalysis(host, runId) {
|
|
|
6201
6055
|
}
|
|
6202
6056
|
}
|
|
6203
6057
|
function serveRunAnalysisRead(req, res, deps) {
|
|
6204
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
|
|
6205
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
6206
|
-
return;
|
|
6207
|
-
}
|
|
6208
6058
|
void readRunAnalysis(deps.host, deps.runId).then(
|
|
6209
6059
|
(outcome) => {
|
|
6210
6060
|
if (outcome.kind === "refused") {
|
|
@@ -6428,10 +6278,7 @@ function serveRunEventStream(req, res, deps) {
|
|
|
6428
6278
|
else pending.push(frame);
|
|
6429
6279
|
};
|
|
6430
6280
|
const frameOf = (event) => (
|
|
6431
|
-
//
|
|
6432
|
-
// data IS the envelope's own JSON. No projection, no rename, no second vocabulary. The filter
|
|
6433
|
-
// DROPS envelopes; it never edits one, so a filtered stream's frames are byte-identical to the
|
|
6434
|
-
// firehose's — with gaps in the ids, which is what seq-as-id means once a filter exists.
|
|
6281
|
+
// The filter drops envelopes and never edits one, so filtered frames match the firehose bytes.
|
|
6435
6282
|
`id: ${String(event.seq)}
|
|
6436
6283
|
event: ${event.type}
|
|
6437
6284
|
data: ${JSON.stringify(event)}
|
|
@@ -6493,12 +6340,6 @@ data: ${JSON.stringify(event)}
|
|
|
6493
6340
|
deps.openStreams?.add(teardown);
|
|
6494
6341
|
req.on("close", teardown);
|
|
6495
6342
|
}
|
|
6496
|
-
function runDenialOf(error) {
|
|
6497
|
-
if (typeof error !== "object" || error === null) return void 0;
|
|
6498
|
-
const neutralReason = error.neutralReason;
|
|
6499
|
-
if (typeof neutralReason !== "string" || neutralReason.trim().length === 0) return void 0;
|
|
6500
|
-
return { error: "run_denied", neutralReason };
|
|
6501
|
-
}
|
|
6502
6343
|
function createRunServiceRoutes(ports) {
|
|
6503
6344
|
const openStreams = /* @__PURE__ */ new Set();
|
|
6504
6345
|
function handleStart(req, res) {
|
|
@@ -6529,12 +6370,7 @@ function createRunServiceRoutes(ports) {
|
|
|
6529
6370
|
ack = await ports.invokeRunStart(
|
|
6530
6371
|
parsed.data.patterns === void 0 ? {} : { patterns: parsed.data.patterns }
|
|
6531
6372
|
);
|
|
6532
|
-
} catch
|
|
6533
|
-
const denial = runDenialOf(error);
|
|
6534
|
-
if (denial !== void 0) {
|
|
6535
|
-
respondJson2(res, 403, denial);
|
|
6536
|
-
return;
|
|
6537
|
-
}
|
|
6373
|
+
} catch {
|
|
6538
6374
|
refuse(res, 500, "run_start_failed");
|
|
6539
6375
|
return;
|
|
6540
6376
|
}
|
|
@@ -6614,6 +6450,10 @@ function createRunServiceRoutes(ports) {
|
|
|
6614
6450
|
refuse(res, 403, "cross_site_refused");
|
|
6615
6451
|
return;
|
|
6616
6452
|
}
|
|
6453
|
+
if (carriesNonJsonBody(req)) {
|
|
6454
|
+
refuse(res, 415, "unsupported_media_type");
|
|
6455
|
+
return;
|
|
6456
|
+
}
|
|
6617
6457
|
const matched = matchRunServiceRoute(req.method, path);
|
|
6618
6458
|
if (matched === void 0) {
|
|
6619
6459
|
refuse(res, 404, "unknown_route");
|
|
@@ -6879,8 +6719,7 @@ data: ${JSON.stringify(again.body)}
|
|
|
6879
6719
|
// A failed re-read is not a reason to close a live channel: the next envelope will trigger
|
|
6880
6720
|
// another, and the page's own poll is the floor underneath both.
|
|
6881
6721
|
() => void 0
|
|
6882
|
-
//
|
|
6883
|
-
// failure arm never sees its own fulfilment arm's throw (UF-01, on the nested chain).
|
|
6722
|
+
// A throw from the arm above is a dead socket, and `.then`'s failure arm never sees it.
|
|
6884
6723
|
).catch(failStream);
|
|
6885
6724
|
});
|
|
6886
6725
|
heartbeat = setInterval(() => {
|
|
@@ -6917,10 +6756,6 @@ async function readRunKnowledge(host, runId) {
|
|
|
6917
6756
|
}
|
|
6918
6757
|
}
|
|
6919
6758
|
function serveRunKnowledgeRead(req, res, deps) {
|
|
6920
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
|
|
6921
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
6922
|
-
return;
|
|
6923
|
-
}
|
|
6924
6759
|
void readRunKnowledge(deps.host, deps.runId).then(
|
|
6925
6760
|
(outcome) => {
|
|
6926
6761
|
if (outcome.kind === "refused") {
|
|
@@ -6961,10 +6796,6 @@ async function markRun(host, runId, body) {
|
|
|
6961
6796
|
}
|
|
6962
6797
|
}
|
|
6963
6798
|
function registerRunMarkRoute(req, res, deps) {
|
|
6964
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
|
|
6965
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
6966
|
-
return;
|
|
6967
|
-
}
|
|
6968
6799
|
void readJsonBody(req).then(async (body) => {
|
|
6969
6800
|
const outcome = await markRun(deps.host, deps.runId, body);
|
|
6970
6801
|
if (outcome.kind === "refused") {
|
|
@@ -6982,10 +6813,6 @@ function registerRunMarkRoute(req, res, deps) {
|
|
|
6982
6813
|
// src/server/config-routes.ts
|
|
6983
6814
|
var import_agent_qa_schema10 = require("@vostride/agent-qa-schema");
|
|
6984
6815
|
function registerConfigWriteRoute(req, res, deps) {
|
|
6985
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
6986
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
6987
|
-
return;
|
|
6988
|
-
}
|
|
6989
6816
|
void readJsonBody(req).then(async (body) => {
|
|
6990
6817
|
const parsed = import_agent_qa_schema10.ConfigWriteRequestSchema.safeParse(body);
|
|
6991
6818
|
if (!parsed.success) {
|
|
@@ -7043,10 +6870,6 @@ function registerConfigUndoRoute(req, res, deps) {
|
|
|
7043
6870
|
respondJson(res, 405, { error: "method_not_allowed" });
|
|
7044
6871
|
return;
|
|
7045
6872
|
}
|
|
7046
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
7047
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
7048
|
-
return;
|
|
7049
|
-
}
|
|
7050
6873
|
void readJsonBody(req).then(async (body) => {
|
|
7051
6874
|
const parsed = import_agent_qa_schema10.ConfigUndoRequestSchema.safeParse(body);
|
|
7052
6875
|
if (!parsed.success) {
|
|
@@ -7276,10 +7099,6 @@ async function connectName(request2, deps) {
|
|
|
7276
7099
|
});
|
|
7277
7100
|
}
|
|
7278
7101
|
function registerLlmConnectRoute(req, res, deps) {
|
|
7279
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
7280
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
7281
|
-
return;
|
|
7282
|
-
}
|
|
7283
7102
|
void readJsonBody(req).then(async (body) => {
|
|
7284
7103
|
const answer = await connectLlm(body, deps);
|
|
7285
7104
|
respondJson(res, answer.status, answer.body);
|
|
@@ -7601,25 +7420,10 @@ function llmConnectionTestTimeoutMs(config) {
|
|
|
7601
7420
|
return REMOTE_LLM_CONNECTION_TEST_TIMEOUT_MS;
|
|
7602
7421
|
}
|
|
7603
7422
|
function json(res, data, status = 200) {
|
|
7604
|
-
const body = JSON.stringify(data);
|
|
7605
|
-
res.writeHead(status, {
|
|
7606
|
-
"Content-Type": "application/json",
|
|
7607
|
-
"Access-Control-Allow-Origin": "*",
|
|
7608
|
-
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
7609
|
-
"Access-Control-Allow-Headers": "Content-Type"
|
|
7610
|
-
});
|
|
7611
|
-
res.end(body);
|
|
7612
|
-
}
|
|
7613
|
-
function jsonNoCors(res, data, status = 200) {
|
|
7614
7423
|
const body = JSON.stringify(data);
|
|
7615
7424
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
7616
7425
|
res.end(body);
|
|
7617
7426
|
}
|
|
7618
|
-
function refuseCrossSiteDisplayRead(req, res) {
|
|
7619
|
-
if (refuseCrossSiteRequest(req) === void 0) return false;
|
|
7620
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
7621
|
-
return true;
|
|
7622
|
-
}
|
|
7623
7427
|
var INSIGHTS_KNOWLEDGE_RUN_CAP = 200;
|
|
7624
7428
|
var INSIGHTS_KNOWLEDGE_CONCURRENCY = 4;
|
|
7625
7429
|
async function isPostProcessingSkipped(host, runId) {
|
|
@@ -7683,14 +7487,6 @@ async function readInsightsKnowledgeChanges(host, db, window) {
|
|
|
7683
7487
|
function notFound(res, message = "Not found") {
|
|
7684
7488
|
json(res, { error: message }, 404);
|
|
7685
7489
|
}
|
|
7686
|
-
function cors(res) {
|
|
7687
|
-
res.writeHead(204, {
|
|
7688
|
-
"Access-Control-Allow-Origin": "*",
|
|
7689
|
-
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
7690
|
-
"Access-Control-Allow-Headers": "Content-Type"
|
|
7691
|
-
});
|
|
7692
|
-
res.end();
|
|
7693
|
-
}
|
|
7694
7490
|
function isPlainRecord(value) {
|
|
7695
7491
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7696
7492
|
}
|
|
@@ -7743,7 +7539,7 @@ function normalizeRunFilterValue(value) {
|
|
|
7743
7539
|
function buildRunRequestAttributes(input) {
|
|
7744
7540
|
const internal = (0, import_agent_qa_core11.buildInternalRunAttributes)({
|
|
7745
7541
|
trigger: input.trigger,
|
|
7746
|
-
runner:
|
|
7542
|
+
runner: "local"
|
|
7747
7543
|
});
|
|
7748
7544
|
const user = (0, import_agent_qa_core11.validateUserRunAttributes)(input.userAttributes, "run attributes");
|
|
7749
7545
|
return (0, import_agent_qa_core11.mergeRunAttributes)(internal, user);
|
|
@@ -8188,9 +7984,7 @@ var RESPONSE_SAFE_RUN_METADATA_KEYS = /* @__PURE__ */ new Set([
|
|
|
8188
7984
|
"timeoutSource",
|
|
8189
7985
|
"timeoutBaseMs",
|
|
8190
7986
|
"timeoutBufferMs",
|
|
8191
|
-
"suiteIndex"
|
|
8192
|
-
"farmSessionUrl",
|
|
8193
|
-
"runDevice"
|
|
7987
|
+
"suiteIndex"
|
|
8194
7988
|
]);
|
|
8195
7989
|
function sanitizeRunMetadataForResponse(metadata) {
|
|
8196
7990
|
if (metadata === null || metadata === void 0) return metadata ?? null;
|
|
@@ -8220,17 +8014,7 @@ var RESPONSE_SAFE_RUN_READ_METHODS = [
|
|
|
8220
8014
|
"getRunsByTest"
|
|
8221
8015
|
];
|
|
8222
8016
|
var RUN_READS_WITHOUT_ROWS = /* @__PURE__ */ new Set([
|
|
8223
|
-
//
|
|
8224
|
-
//
|
|
8225
|
-
// The five WRITES return nothing; they are here for the same reason `insertRun` is.
|
|
8226
|
-
//
|
|
8227
|
-
// `readRunDetailRows` IS A READ AND IS STILL HERE, and that is the judgement this set exists to
|
|
8228
|
-
// make explicit rather than to hide. It returns members, steps and sub-actions — the bus
|
|
8229
|
-
// payloads as the subscriber MASKED them (T-363-05) — and NO `RunRow` and no
|
|
8230
|
-
// `RunArtifactRow`. It therefore carries none of the run metadata the 401-23 scrub law exists
|
|
8231
|
-
// to strip, so wrapping it would be wrapping a shape that has nothing to strip. This router
|
|
8232
|
-
// does not call it either: the detail arm dispatches through the injected host, which is the
|
|
8233
|
-
// whole of D-03.
|
|
8017
|
+
// `readRunDetailRows` returns masked bus payloads and no run row, so it has no metadata to strip.
|
|
8234
8018
|
"deleteMemberProjection",
|
|
8235
8019
|
"incrementRunMemberAttempts",
|
|
8236
8020
|
"insertRunStep",
|
|
@@ -8241,23 +8025,23 @@ var RUN_READS_WITHOUT_ROWS = /* @__PURE__ */ new Set([
|
|
|
8241
8025
|
"finalizeRunArtifact",
|
|
8242
8026
|
"getRunsBySuiteIdCount",
|
|
8243
8027
|
"getRunsByTestCount",
|
|
8244
|
-
//
|
|
8028
|
+
// Run ids and a count, no row; each id is dispatched through the agent host.
|
|
8245
8029
|
"listInsightsWindowRunIds",
|
|
8246
8030
|
"insertPendingRun",
|
|
8247
8031
|
"insertRun",
|
|
8248
8032
|
"insertRunArtifact",
|
|
8249
8033
|
"listRunAttributeKeys",
|
|
8250
8034
|
"listRunAttributeValues",
|
|
8251
|
-
//
|
|
8035
|
+
// An owner (pid, incarnation, host), no run metadata.
|
|
8252
8036
|
"readRunOwner",
|
|
8253
|
-
//
|
|
8037
|
+
// Ids and owners only, no run metadata.
|
|
8254
8038
|
"listRunningRunOwners",
|
|
8255
|
-
//
|
|
8039
|
+
// Compare-and-set writes that return a boolean.
|
|
8256
8040
|
"finalizeOrphanRun",
|
|
8257
8041
|
"finalizeOwnRun",
|
|
8258
|
-
//
|
|
8042
|
+
// A compare-and-set write that returns a boolean, no run metadata.
|
|
8259
8043
|
"closeOwnRun",
|
|
8260
|
-
//
|
|
8044
|
+
// A compare-and-set write that returns a boolean, no run metadata.
|
|
8261
8045
|
"claimRunningRow",
|
|
8262
8046
|
"stageRunArtifact",
|
|
8263
8047
|
"updateRun",
|
|
@@ -8328,10 +8112,7 @@ function withResponseSafeRunReads(db) {
|
|
|
8328
8112
|
// `sanitizeArtifactForResponse`; a caller reaching for the bare row has no such step, so the
|
|
8329
8113
|
// projection is its only choke point. Same allowlist, so the two cannot drift.
|
|
8330
8114
|
getRunArtifact: { value: (runId) => scrubArtifact(db.getRunArtifact(runId)) },
|
|
8331
|
-
//
|
|
8332
|
-
// parent, whose `run` rows this override scrubbed; nothing has written a child row since
|
|
8333
|
-
// 402-09, and the shape went with the column. `getRunArtifact` above remains the choke point
|
|
8334
|
-
// for the payload itself, and this wrapper delegates to it.
|
|
8115
|
+
// One artifact; `getRunArtifact` is the choke point for its payload, and this delegates to it.
|
|
8335
8116
|
getRunArtifactBundle: {
|
|
8336
8117
|
value: (runId) => ({ artifact: scrubArtifact(db.getRunArtifactBundle(runId).artifact) })
|
|
8337
8118
|
}
|
|
@@ -8479,10 +8260,18 @@ function createRouter(dbOrDeps) {
|
|
|
8479
8260
|
json(res, { error: "Bad Request" }, 400);
|
|
8480
8261
|
return;
|
|
8481
8262
|
}
|
|
8263
|
+
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
8264
|
+
json(res, { error: "cross_site_refused" }, 403);
|
|
8265
|
+
return;
|
|
8266
|
+
}
|
|
8267
|
+
if (carriesNonJsonBody(req)) {
|
|
8268
|
+
json(res, { error: "unsupported_media_type" }, 415);
|
|
8269
|
+
return;
|
|
8270
|
+
}
|
|
8482
8271
|
const path = url.pathname;
|
|
8483
8272
|
if (path === "/api/config" && req.method === "PATCH") {
|
|
8484
8273
|
if (!configManager) {
|
|
8485
|
-
|
|
8274
|
+
json(res, { error: "config_unavailable" }, 503);
|
|
8486
8275
|
return;
|
|
8487
8276
|
}
|
|
8488
8277
|
registerConfigWriteRoute(req, res, { configManager, undoStore });
|
|
@@ -8502,7 +8291,7 @@ function createRouter(dbOrDeps) {
|
|
|
8502
8291
|
}
|
|
8503
8292
|
if (path === "/api/config/revision" && req.method === "GET") {
|
|
8504
8293
|
if (!configManager) {
|
|
8505
|
-
|
|
8294
|
+
json(res, { error: "config_unavailable" }, 503);
|
|
8506
8295
|
return;
|
|
8507
8296
|
}
|
|
8508
8297
|
registerConfigRevisionRoute(req, res, { configManager });
|
|
@@ -8510,7 +8299,7 @@ function createRouter(dbOrDeps) {
|
|
|
8510
8299
|
}
|
|
8511
8300
|
if (path === "/api/llm/connect" && req.method === "POST") {
|
|
8512
8301
|
if (!configManager) {
|
|
8513
|
-
|
|
8302
|
+
json(res, { error: "config_unavailable" }, 503);
|
|
8514
8303
|
return;
|
|
8515
8304
|
}
|
|
8516
8305
|
registerLlmConnectRoute(req, res, {
|
|
@@ -8522,16 +8311,6 @@ function createRouter(dbOrDeps) {
|
|
|
8522
8311
|
});
|
|
8523
8312
|
return;
|
|
8524
8313
|
}
|
|
8525
|
-
if (path === "/api/runs/trigger" || path === "/api/queue/enqueue") {
|
|
8526
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
8527
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
8528
|
-
return;
|
|
8529
|
-
}
|
|
8530
|
-
}
|
|
8531
|
-
if (req.method === "OPTIONS") {
|
|
8532
|
-
cors(res);
|
|
8533
|
-
return;
|
|
8534
|
-
}
|
|
8535
8314
|
if (path === "/api/auth-states" && req.method === "GET") {
|
|
8536
8315
|
if (!configManager || !deps.configPath) {
|
|
8537
8316
|
json(res, { error: "Auth-state metadata not available" }, 503);
|
|
@@ -8555,11 +8334,10 @@ function createRouter(dbOrDeps) {
|
|
|
8555
8334
|
return;
|
|
8556
8335
|
}
|
|
8557
8336
|
if (path === "/api/runs" && req.method === "GET") {
|
|
8558
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
8559
8337
|
const statuses = url.searchParams.getAll("status");
|
|
8560
8338
|
const unknownStatus = statuses.find((word) => !isRunStatusWord(word));
|
|
8561
8339
|
if (unknownStatus !== void 0) {
|
|
8562
|
-
|
|
8340
|
+
json(res, { error: `Unknown run status: ${unknownStatus}.` }, 400);
|
|
8563
8341
|
return;
|
|
8564
8342
|
}
|
|
8565
8343
|
const name = url.searchParams.get("name") ?? void 0;
|
|
@@ -8591,12 +8369,12 @@ function createRouter(dbOrDeps) {
|
|
|
8591
8369
|
)).sort((a, b) => a.localeCompare(b));
|
|
8592
8370
|
const filtered = platformFilteredRuns.filter((run) => matchesTargetFilter(run, target));
|
|
8593
8371
|
const runs = filtered.slice(offset, offset + limit);
|
|
8594
|
-
|
|
8372
|
+
json(res, {
|
|
8595
8373
|
runs,
|
|
8596
8374
|
total: filtered.length,
|
|
8597
8375
|
targets: targetOptions
|
|
8598
8376
|
});
|
|
8599
|
-
}).catch(() =>
|
|
8377
|
+
}).catch(() => json(res, { error: "Failed to list runs" }, 500));
|
|
8600
8378
|
return;
|
|
8601
8379
|
}
|
|
8602
8380
|
if (path === "/api/runs/attributes/keys" && req.method === "GET") {
|
|
@@ -8634,7 +8412,6 @@ function createRouter(dbOrDeps) {
|
|
|
8634
8412
|
}
|
|
8635
8413
|
const runArtifactMatch = path.match(/^\/api\/runs\/([^/]+)\/artifact$/);
|
|
8636
8414
|
if (runArtifactMatch && req.method === "GET") {
|
|
8637
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
8638
8415
|
const runId = decodeURIComponent(runArtifactMatch[1]);
|
|
8639
8416
|
const run = db.getRun(runId);
|
|
8640
8417
|
if (!run) {
|
|
@@ -8642,7 +8419,7 @@ function createRouter(dbOrDeps) {
|
|
|
8642
8419
|
return;
|
|
8643
8420
|
}
|
|
8644
8421
|
const bundle = db.getRunArtifactBundle(runId);
|
|
8645
|
-
|
|
8422
|
+
json(res, {
|
|
8646
8423
|
run,
|
|
8647
8424
|
artifact: sanitizeArtifactForResponse(bundle.artifact),
|
|
8648
8425
|
missingSections: getArtifactMissingSections(bundle.artifact)
|
|
@@ -8650,7 +8427,6 @@ function createRouter(dbOrDeps) {
|
|
|
8650
8427
|
return;
|
|
8651
8428
|
}
|
|
8652
8429
|
if (path.startsWith("/api/runs/") && path.endsWith("/execution-logs") && req.method === "GET") {
|
|
8653
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
8654
8430
|
const segments = path.split("/");
|
|
8655
8431
|
const id = segments[3];
|
|
8656
8432
|
const run = db.getRun(id);
|
|
@@ -8661,37 +8437,37 @@ function createRouter(dbOrDeps) {
|
|
|
8661
8437
|
const stepId = url.searchParams.get("stepId") ?? void 0;
|
|
8662
8438
|
const type = url.searchParams.get("type") ?? void 0;
|
|
8663
8439
|
const logs = db.getExecutionLogs({ runId: id, stepId, type });
|
|
8664
|
-
|
|
8440
|
+
json(res, { logs });
|
|
8665
8441
|
return;
|
|
8666
8442
|
}
|
|
8667
8443
|
if (path.startsWith("/api/runs/") && path.endsWith("/mark") && req.method === "POST") {
|
|
8668
8444
|
if (deps.agentHost === void 0) {
|
|
8669
|
-
|
|
8445
|
+
json(res, { error: "agent_host_unavailable" }, 503);
|
|
8670
8446
|
return;
|
|
8671
8447
|
}
|
|
8672
8448
|
let runId;
|
|
8673
8449
|
try {
|
|
8674
8450
|
runId = decodeURIComponent(path.slice("/api/runs/".length, -"/mark".length));
|
|
8675
8451
|
} catch {
|
|
8676
|
-
|
|
8452
|
+
json(res, { error: "invalid_mark_input" }, 400);
|
|
8677
8453
|
return;
|
|
8678
8454
|
}
|
|
8679
|
-
registerRunMarkRoute(req, res, { host: deps.agentHost, runId
|
|
8455
|
+
registerRunMarkRoute(req, res, { host: deps.agentHost, runId });
|
|
8680
8456
|
return;
|
|
8681
8457
|
}
|
|
8682
8458
|
if (req.method === "POST" && /^\/api\/runs\/[^/]+\/mark-preview$/.test(path)) {
|
|
8683
8459
|
if (deps.agentHost === void 0) {
|
|
8684
|
-
|
|
8460
|
+
json(res, { error: "agent_host_unavailable" }, 503);
|
|
8685
8461
|
return;
|
|
8686
8462
|
}
|
|
8687
8463
|
let runId;
|
|
8688
8464
|
try {
|
|
8689
8465
|
runId = decodeURIComponent(path.slice("/api/runs/".length, -"/mark-preview".length));
|
|
8690
8466
|
} catch {
|
|
8691
|
-
|
|
8467
|
+
json(res, { error: "invalid_run_id" }, 400);
|
|
8692
8468
|
return;
|
|
8693
8469
|
}
|
|
8694
|
-
serveRunMarkPreviewRead(req, res, { runId, host: deps.agentHost
|
|
8470
|
+
serveRunMarkPreviewRead(req, res, { runId, host: deps.agentHost });
|
|
8695
8471
|
return;
|
|
8696
8472
|
}
|
|
8697
8473
|
if (path.startsWith("/api/runs/") && path.endsWith("/cancel") && req.method === "POST") {
|
|
@@ -8770,44 +8546,39 @@ function createRouter(dbOrDeps) {
|
|
|
8770
8546
|
return;
|
|
8771
8547
|
}
|
|
8772
8548
|
if (req.method === "GET" && /^\/api\/runs\/[^/]+\/live-url$/.test(path)) {
|
|
8773
|
-
const crossSite = refuseCrossSiteRequest(req);
|
|
8774
|
-
if (crossSite !== void 0) {
|
|
8775
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
8776
|
-
return;
|
|
8777
|
-
}
|
|
8778
8549
|
if (!deps.mintLiveUrl) {
|
|
8779
|
-
|
|
8550
|
+
json(res, { error: "live_view_unavailable" }, 503);
|
|
8780
8551
|
return;
|
|
8781
8552
|
}
|
|
8782
8553
|
let liveUrlRunId;
|
|
8783
8554
|
try {
|
|
8784
8555
|
liveUrlRunId = decodeURIComponent(path.slice("/api/runs/".length, -"/live-url".length));
|
|
8785
8556
|
} catch {
|
|
8786
|
-
|
|
8557
|
+
json(res, { error: "unknown_run" }, 404);
|
|
8787
8558
|
return;
|
|
8788
8559
|
}
|
|
8789
8560
|
const rawHandle = handleParamOf(req);
|
|
8790
8561
|
const parsedHandle = (0, import_agent_qa_schema12.parseHandleParam)(rawHandle);
|
|
8791
8562
|
if (parsedHandle.kind === "invalid") {
|
|
8792
|
-
|
|
8563
|
+
json(res, { error: "invalid_handle" }, 400);
|
|
8793
8564
|
return;
|
|
8794
8565
|
}
|
|
8795
8566
|
const handle = parsedHandle.kind === "absent" ? 0 : parsedHandle.index;
|
|
8796
8567
|
void deps.mintLiveUrl(liveUrlRunId, handle).then(
|
|
8797
8568
|
(result) => {
|
|
8798
|
-
|
|
8569
|
+
json(res, result);
|
|
8799
8570
|
},
|
|
8800
8571
|
(error) => {
|
|
8801
8572
|
const name = error instanceof Error ? error.name : "";
|
|
8802
8573
|
if (name === "UnknownRunError") {
|
|
8803
|
-
|
|
8574
|
+
json(res, { error: "unknown_run" }, 404);
|
|
8804
8575
|
return;
|
|
8805
8576
|
}
|
|
8806
8577
|
if (name === "LiveViewUnavailableError") {
|
|
8807
|
-
|
|
8578
|
+
json(res, { error: "live_view_unavailable" }, 503);
|
|
8808
8579
|
return;
|
|
8809
8580
|
}
|
|
8810
|
-
|
|
8581
|
+
json(res, { error: "live_url_mint_failed" }, 500);
|
|
8811
8582
|
}
|
|
8812
8583
|
);
|
|
8813
8584
|
return;
|
|
@@ -8836,32 +8607,32 @@ function createRouter(dbOrDeps) {
|
|
|
8836
8607
|
}
|
|
8837
8608
|
if (req.method === "GET" && /^\/api\/runs\/[^/]+\/evidence$/.test(path)) {
|
|
8838
8609
|
if (!deps.agentHost) {
|
|
8839
|
-
|
|
8610
|
+
json(res, { error: "agent_host_unavailable" }, 503);
|
|
8840
8611
|
return;
|
|
8841
8612
|
}
|
|
8842
8613
|
let runId;
|
|
8843
8614
|
try {
|
|
8844
8615
|
runId = decodeURIComponent(path.slice("/api/runs/".length, -"/evidence".length));
|
|
8845
8616
|
} catch {
|
|
8846
|
-
|
|
8617
|
+
json(res, { error: "invalid_run_id" }, 400);
|
|
8847
8618
|
return;
|
|
8848
8619
|
}
|
|
8849
|
-
serveRunEvidenceRead(req, res, { runId, host: deps.agentHost
|
|
8620
|
+
serveRunEvidenceRead(req, res, { runId, host: deps.agentHost });
|
|
8850
8621
|
return;
|
|
8851
8622
|
}
|
|
8852
8623
|
if (req.method === "GET" && /^\/api\/runs\/[^/]+\/marks$/.test(path)) {
|
|
8853
8624
|
if (!deps.agentHost) {
|
|
8854
|
-
|
|
8625
|
+
json(res, { error: "agent_host_unavailable" }, 503);
|
|
8855
8626
|
return;
|
|
8856
8627
|
}
|
|
8857
8628
|
let runId;
|
|
8858
8629
|
try {
|
|
8859
8630
|
runId = decodeURIComponent(path.slice("/api/runs/".length, -"/marks".length));
|
|
8860
8631
|
} catch {
|
|
8861
|
-
|
|
8632
|
+
json(res, { error: "invalid_run_id" }, 400);
|
|
8862
8633
|
return;
|
|
8863
8634
|
}
|
|
8864
|
-
serveRunMarksHistoryRead(req, res, { runId, host: deps.agentHost
|
|
8635
|
+
serveRunMarksHistoryRead(req, res, { runId, host: deps.agentHost });
|
|
8865
8636
|
return;
|
|
8866
8637
|
}
|
|
8867
8638
|
if (req.method === "GET" && /^\/api\/runs\/[^/]+\/detail$/.test(path)) {
|
|
@@ -8876,7 +8647,7 @@ function createRouter(dbOrDeps) {
|
|
|
8876
8647
|
json(res, { error: "invalid_run_id" }, 400);
|
|
8877
8648
|
return;
|
|
8878
8649
|
}
|
|
8879
|
-
serveRunDetailRead(req, res, { runId: detailRunId, host: deps.agentHost
|
|
8650
|
+
serveRunDetailRead(req, res, { runId: detailRunId, host: deps.agentHost });
|
|
8880
8651
|
return;
|
|
8881
8652
|
}
|
|
8882
8653
|
if (req.method === "GET" && /^\/api\/runs\/[^/]+\/analysis$/.test(path)) {
|
|
@@ -8891,7 +8662,7 @@ function createRouter(dbOrDeps) {
|
|
|
8891
8662
|
json(res, { error: "invalid_run_id" }, 400);
|
|
8892
8663
|
return;
|
|
8893
8664
|
}
|
|
8894
|
-
serveRunAnalysisRead(req, res, { runId: analysisRunId, host: deps.agentHost
|
|
8665
|
+
serveRunAnalysisRead(req, res, { runId: analysisRunId, host: deps.agentHost });
|
|
8895
8666
|
return;
|
|
8896
8667
|
}
|
|
8897
8668
|
if (req.method === "GET" && /^\/api\/runs\/[^/]+\/knowledge$/.test(path)) {
|
|
@@ -8908,8 +8679,7 @@ function createRouter(dbOrDeps) {
|
|
|
8908
8679
|
}
|
|
8909
8680
|
serveRunKnowledgeRead(req, res, {
|
|
8910
8681
|
runId: knowledgeRunId,
|
|
8911
|
-
host: deps.agentHost
|
|
8912
|
-
boundary: "human"
|
|
8682
|
+
host: deps.agentHost
|
|
8913
8683
|
});
|
|
8914
8684
|
return;
|
|
8915
8685
|
}
|
|
@@ -8959,14 +8729,13 @@ function createRouter(dbOrDeps) {
|
|
|
8959
8729
|
notFound(res);
|
|
8960
8730
|
return;
|
|
8961
8731
|
}
|
|
8962
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
8963
8732
|
const id = segments[3];
|
|
8964
8733
|
const run = db.getRun(id);
|
|
8965
8734
|
if (!run) {
|
|
8966
8735
|
notFound(res, "Run not found");
|
|
8967
8736
|
return;
|
|
8968
8737
|
}
|
|
8969
|
-
|
|
8738
|
+
json(res, { run });
|
|
8970
8739
|
return;
|
|
8971
8740
|
}
|
|
8972
8741
|
if (path === "/api/stats/costs" && req.method === "GET") {
|
|
@@ -8977,19 +8746,18 @@ function createRouter(dbOrDeps) {
|
|
|
8977
8746
|
return;
|
|
8978
8747
|
}
|
|
8979
8748
|
if (path === "/api/insights/knowledge-changes" && req.method === "GET") {
|
|
8980
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
8981
8749
|
if (!deps.agentHost) {
|
|
8982
|
-
|
|
8750
|
+
json(res, { status: "unavailable", reason: "agent_host_unavailable" }, 503);
|
|
8983
8751
|
return;
|
|
8984
8752
|
}
|
|
8985
8753
|
const from = url.searchParams.get("from") ?? void 0;
|
|
8986
8754
|
const to = url.searchParams.get("to") ?? void 0;
|
|
8987
8755
|
void readInsightsKnowledgeChanges(deps.agentHost, db, { from, to }).then(
|
|
8988
8756
|
(body) => {
|
|
8989
|
-
|
|
8757
|
+
json(res, body);
|
|
8990
8758
|
},
|
|
8991
8759
|
() => {
|
|
8992
|
-
|
|
8760
|
+
json(res, { status: "unavailable", reason: "read_failed" }, 500);
|
|
8993
8761
|
}
|
|
8994
8762
|
);
|
|
8995
8763
|
return;
|
|
@@ -9055,10 +8823,7 @@ function createRouter(dbOrDeps) {
|
|
|
9055
8823
|
serveRunEventStream(req, res, {
|
|
9056
8824
|
runId,
|
|
9057
8825
|
handleParam: handleParamOf(req),
|
|
9058
|
-
//
|
|
9059
|
-
// it from. `url.searchParams.get('lastEventId')` would resolve a repeated parameter to its
|
|
9060
|
-
// first value — the one answer `lastEventIdParamOf` exists to refuse — and a second
|
|
9061
|
-
// header/query precedence rule here would be a second contract for where a client resumes.
|
|
8826
|
+
// Read from the same pair as the authed door; `searchParams.get` takes a repeated first.
|
|
9062
8827
|
resume: resumePointOf(req.headers["last-event-id"], lastEventIdParamOf(req)),
|
|
9063
8828
|
attach: deps.runServiceAttach
|
|
9064
8829
|
});
|
|
@@ -9100,7 +8865,6 @@ function createRouter(dbOrDeps) {
|
|
|
9100
8865
|
try {
|
|
9101
8866
|
attributes = buildRunRequestAttributes({
|
|
9102
8867
|
trigger: "api",
|
|
9103
|
-
runner: "local",
|
|
9104
8868
|
userAttributes: body.attributes
|
|
9105
8869
|
});
|
|
9106
8870
|
} catch (err) {
|
|
@@ -9108,25 +8872,10 @@ function createRouter(dbOrDeps) {
|
|
|
9108
8872
|
return;
|
|
9109
8873
|
}
|
|
9110
8874
|
const args = normalizedFileTarget ? [normalizedFileTarget.executionPath] : [];
|
|
9111
|
-
const source = isSuiteFile ? "suite" : "dashboard";
|
|
9112
|
-
try {
|
|
9113
|
-
await executionBackend?.preflight?.({ args, source });
|
|
9114
|
-
} catch (error) {
|
|
9115
|
-
const denial = runDenialOf(error);
|
|
9116
|
-
if (denial !== void 0) {
|
|
9117
|
-
json(res, denial, 403);
|
|
9118
|
-
return;
|
|
9119
|
-
}
|
|
9120
|
-
json(res, { error: error instanceof Error ? error.message : String(error) }, 400);
|
|
9121
|
-
return;
|
|
9122
|
-
}
|
|
9123
8875
|
const runId = jobQueue.enqueue({
|
|
9124
8876
|
name: body.name,
|
|
9125
8877
|
filePath: normalizedFileTarget?.storagePath,
|
|
9126
|
-
//
|
|
9127
|
-
// with member handles, so a parent artifact kind would claim parenthood over rows
|
|
9128
|
-
// nothing creates. `isSuiteFile` still travels in `metadata.isSuite` below, and the run's
|
|
9129
|
-
// own `suite_id` plus the artifact's `source.kind` snapshot still say it is a suite.
|
|
8878
|
+
// No parent kind is minted; `metadata.isSuite` and `suite_id` say it is a suite.
|
|
9130
8879
|
kind: "test",
|
|
9131
8880
|
attributes,
|
|
9132
8881
|
priority: body.priority,
|
|
@@ -9209,17 +8958,6 @@ function createRouter(dbOrDeps) {
|
|
|
9209
8958
|
args.push("--disable-cache");
|
|
9210
8959
|
}
|
|
9211
8960
|
const isSuite = Boolean(isSuiteFile);
|
|
9212
|
-
try {
|
|
9213
|
-
await executionBackend?.preflight?.({ args, source: isSuite ? "suite" : "dashboard" });
|
|
9214
|
-
} catch (error) {
|
|
9215
|
-
const denial = runDenialOf(error);
|
|
9216
|
-
if (denial !== void 0) {
|
|
9217
|
-
json(res, denial, 403);
|
|
9218
|
-
return;
|
|
9219
|
-
}
|
|
9220
|
-
json(res, { error: error instanceof Error ? error.message : String(error) }, 400);
|
|
9221
|
-
return;
|
|
9222
|
-
}
|
|
9223
8961
|
let testName = body.file ? (0, import_node_path10.basename)(normalizedFileTarget?.storagePath ?? body.file, ".yaml") : "unknown";
|
|
9224
8962
|
let fileParallel;
|
|
9225
8963
|
let platform;
|
|
@@ -9262,11 +9000,10 @@ function createRouter(dbOrDeps) {
|
|
|
9262
9000
|
const runId = jobQueue.enqueue({
|
|
9263
9001
|
name: testName,
|
|
9264
9002
|
filePath: normalizedFileTarget?.storagePath ?? body.file,
|
|
9265
|
-
//
|
|
9003
|
+
// No parent kind is minted; see the enqueue route above.
|
|
9266
9004
|
kind: "test",
|
|
9267
9005
|
attributes: buildRunRequestAttributes({
|
|
9268
|
-
trigger: triggerSource.trigger
|
|
9269
|
-
runner: body.local === false ? "browserstack" : "local"
|
|
9006
|
+
trigger: triggerSource.trigger
|
|
9270
9007
|
}),
|
|
9271
9008
|
platform,
|
|
9272
9009
|
parallel: effectiveParallel,
|
|
@@ -9819,9 +9556,8 @@ function createRouter(dbOrDeps) {
|
|
|
9819
9556
|
return;
|
|
9820
9557
|
}
|
|
9821
9558
|
if (path === "/api/config" && req.method === "GET") {
|
|
9822
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
9823
9559
|
if (!configManager) {
|
|
9824
|
-
|
|
9560
|
+
json(res, { error: "Config management not available" }, 501);
|
|
9825
9561
|
return;
|
|
9826
9562
|
}
|
|
9827
9563
|
;
|
|
@@ -9829,7 +9565,7 @@ function createRouter(dbOrDeps) {
|
|
|
9829
9565
|
try {
|
|
9830
9566
|
const read = await configManager.readMasked();
|
|
9831
9567
|
if (read.kind === "file_invalid") {
|
|
9832
|
-
|
|
9568
|
+
json(res, { error: "file_invalid", message: read.message }, 409);
|
|
9833
9569
|
return;
|
|
9834
9570
|
}
|
|
9835
9571
|
const { config, revision, lines, fileIssues } = read;
|
|
@@ -9839,7 +9575,7 @@ function createRouter(dbOrDeps) {
|
|
|
9839
9575
|
const activeLlm = use?.llm ?? config.defaultLLM;
|
|
9840
9576
|
const defaultCfg = Array.isArray(llms) ? llms.find((c) => c?.name === activeLlm) : void 0;
|
|
9841
9577
|
const provider = defaultCfg?.provider ?? null;
|
|
9842
|
-
|
|
9578
|
+
json(res, {
|
|
9843
9579
|
config,
|
|
9844
9580
|
provider,
|
|
9845
9581
|
revision,
|
|
@@ -9847,45 +9583,17 @@ function createRouter(dbOrDeps) {
|
|
|
9847
9583
|
...fileIssues.length > 0 ? { fileIssues } : {}
|
|
9848
9584
|
});
|
|
9849
9585
|
} catch (err) {
|
|
9850
|
-
|
|
9586
|
+
json(res, { error: err instanceof Error ? err.message : "Failed to read config" }, 500);
|
|
9851
9587
|
}
|
|
9852
9588
|
})();
|
|
9853
9589
|
return;
|
|
9854
9590
|
}
|
|
9855
|
-
if (path === "/api/config/farm/test-connection" && req.method === "POST") {
|
|
9856
|
-
readJsonBody(req).then(async (body) => {
|
|
9857
|
-
if (body.provider !== "browserstack") {
|
|
9858
|
-
json(res, { error: "Only BrowserStack is currently supported" }, 400);
|
|
9859
|
-
return;
|
|
9860
|
-
}
|
|
9861
|
-
try {
|
|
9862
|
-
const auth = Buffer.from(`${body.username}:${body.accessKey}`).toString("base64");
|
|
9863
|
-
const response = await fetch("https://api-cloud.browserstack.com/app-automate/devices.json", {
|
|
9864
|
-
headers: { Authorization: `Basic ${auth}` }
|
|
9865
|
-
});
|
|
9866
|
-
if (response.ok) {
|
|
9867
|
-
json(res, { success: true });
|
|
9868
|
-
} else if (response.status === 401) {
|
|
9869
|
-
json(res, { success: false, error: "Invalid credentials" });
|
|
9870
|
-
} else {
|
|
9871
|
-
json(res, { success: false, error: `BrowserStack API returned ${response.status}` });
|
|
9872
|
-
}
|
|
9873
|
-
} catch (err) {
|
|
9874
|
-
json(res, { success: false, error: err instanceof Error ? err.message : "Connection failed" });
|
|
9875
|
-
}
|
|
9876
|
-
}).catch(() => json(res, { error: "Invalid request body" }, 400));
|
|
9877
|
-
return;
|
|
9878
|
-
}
|
|
9879
9591
|
if (path.startsWith("/api/auth/") && req.method === "DELETE") {
|
|
9880
9592
|
const segments = path.split("/");
|
|
9881
9593
|
if (segments.length === 4 && segments[2] === "auth") {
|
|
9882
9594
|
const providerName = decodeURIComponent(segments[3]);
|
|
9883
9595
|
if (providerName === "status" || providerName === "credential" || providerName === "oauth") {
|
|
9884
9596
|
} else {
|
|
9885
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
9886
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
9887
|
-
return;
|
|
9888
|
-
}
|
|
9889
9597
|
;
|
|
9890
9598
|
(async () => {
|
|
9891
9599
|
try {
|
|
@@ -9896,13 +9604,13 @@ function createRouter(dbOrDeps) {
|
|
|
9896
9604
|
return credential;
|
|
9897
9605
|
});
|
|
9898
9606
|
if (removed === void 0) {
|
|
9899
|
-
|
|
9607
|
+
json(res, { deleted: false });
|
|
9900
9608
|
return;
|
|
9901
9609
|
}
|
|
9902
9610
|
const undo = undoStore.put({ kind: "credential", name: providerName, credential: removed }, Date.now());
|
|
9903
|
-
|
|
9611
|
+
json(res, { deleted: true, undo });
|
|
9904
9612
|
} catch (err) {
|
|
9905
|
-
|
|
9613
|
+
json(res, { error: err instanceof Error ? err.message : "Failed to delete credential" }, 500);
|
|
9906
9614
|
}
|
|
9907
9615
|
})();
|
|
9908
9616
|
return;
|
|
@@ -10012,27 +9720,21 @@ function createRouter(dbOrDeps) {
|
|
|
10012
9720
|
const segments = path.split("/");
|
|
10013
9721
|
const provider = segments.length >= 6 ? decodeURIComponent(segments[4] ?? "") : "";
|
|
10014
9722
|
const action = segments.length >= 6 ? segments[5] : "";
|
|
10015
|
-
const authDoor = req.method === "POST" && (action === "start" || action === "exchange");
|
|
10016
|
-
if (authDoor && refuseCrossSiteRequest(req) !== void 0) {
|
|
10017
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
10018
|
-
return;
|
|
10019
|
-
}
|
|
10020
9723
|
const plugin = provider ? (0, import_agent_qa_core11.getLLMAuthProviderPlugin)(provider) : void 0;
|
|
10021
9724
|
if (!plugin) {
|
|
10022
|
-
;
|
|
10023
|
-
(authDoor ? jsonNoCors : json)(res, { error: `Auth plugin provider "${provider}" is not registered` }, 404);
|
|
9725
|
+
json(res, { error: `Auth plugin provider "${provider}" is not registered` }, 404);
|
|
10024
9726
|
return;
|
|
10025
9727
|
}
|
|
10026
9728
|
if (action === "start" && req.method === "POST") {
|
|
10027
9729
|
readJsonBody(req).then(async (body) => {
|
|
10028
9730
|
if (!plugin.startAuth) {
|
|
10029
|
-
|
|
9731
|
+
json(res, { error: `Provider "${provider}" does not support dashboard auth start` }, 400);
|
|
10030
9732
|
return;
|
|
10031
9733
|
}
|
|
10032
9734
|
try {
|
|
10033
9735
|
const target = await requirePluginAuthConfig(configManager, body.configName, plugin);
|
|
10034
9736
|
if (!target.ok) {
|
|
10035
|
-
|
|
9737
|
+
json(res, credentialRefusalBody(target), target.status);
|
|
10036
9738
|
return;
|
|
10037
9739
|
}
|
|
10038
9740
|
const started = await plugin.startAuth({
|
|
@@ -10068,16 +9770,16 @@ function createRouter(dbOrDeps) {
|
|
|
10068
9770
|
session.error = err instanceof Error ? err.message : "Authentication failed";
|
|
10069
9771
|
}).finally(() => cleanupPluginOAuthSession(session));
|
|
10070
9772
|
}
|
|
10071
|
-
|
|
9773
|
+
json(res, {
|
|
10072
9774
|
authorizeUrl: started.authorizeUrl,
|
|
10073
9775
|
sessionId,
|
|
10074
9776
|
mode: plugin.dashboardAuth.mode,
|
|
10075
9777
|
created: target.created
|
|
10076
9778
|
});
|
|
10077
9779
|
} catch (err) {
|
|
10078
|
-
|
|
9780
|
+
json(res, { error: err instanceof Error ? err.message : "Failed to start auth plugin flow" }, 500);
|
|
10079
9781
|
}
|
|
10080
|
-
}).catch(() =>
|
|
9782
|
+
}).catch(() => json(res, { error: "Invalid request body" }, 400));
|
|
10081
9783
|
return;
|
|
10082
9784
|
}
|
|
10083
9785
|
if (action === "result" && req.method === "GET") {
|
|
@@ -10105,22 +9807,22 @@ function createRouter(dbOrDeps) {
|
|
|
10105
9807
|
if (action === "exchange" && req.method === "POST") {
|
|
10106
9808
|
readJsonBody(req).then(async (body) => {
|
|
10107
9809
|
if (!plugin.exchangeCode) {
|
|
10108
|
-
|
|
9810
|
+
json(res, { error: `Provider "${provider}" does not support code exchange` }, 400);
|
|
10109
9811
|
return;
|
|
10110
9812
|
}
|
|
10111
9813
|
const sessionId = typeof body.sessionId === "string" ? body.sessionId.trim() : "";
|
|
10112
9814
|
const code = typeof body.code === "string" ? body.code.trim() : "";
|
|
10113
9815
|
if (!sessionId || !code) {
|
|
10114
|
-
|
|
9816
|
+
json(res, { error: "sessionId and code are required" }, 400);
|
|
10115
9817
|
return;
|
|
10116
9818
|
}
|
|
10117
9819
|
const session = pluginOAuthSessions.get(sessionId);
|
|
10118
9820
|
if (!session || session.providerId !== provider) {
|
|
10119
|
-
|
|
9821
|
+
json(res, { error: "Auth session not found" }, 404);
|
|
10120
9822
|
return;
|
|
10121
9823
|
}
|
|
10122
9824
|
if (session.status !== "pending") {
|
|
10123
|
-
|
|
9825
|
+
json(res, { error: "Auth session is no longer pending" }, 409);
|
|
10124
9826
|
return;
|
|
10125
9827
|
}
|
|
10126
9828
|
try {
|
|
@@ -10142,61 +9844,57 @@ function createRouter(dbOrDeps) {
|
|
|
10142
9844
|
session.error = CREDENTIAL_PROVIDER_CONFLICT.message;
|
|
10143
9845
|
cleanupPluginOAuthSession(session);
|
|
10144
9846
|
pluginOAuthSessions.delete(sessionId);
|
|
10145
|
-
|
|
9847
|
+
json(res, credentialRefusalBody(CREDENTIAL_PROVIDER_CONFLICT), CREDENTIAL_PROVIDER_CONFLICT.status);
|
|
10146
9848
|
return;
|
|
10147
9849
|
}
|
|
10148
9850
|
session.status = "completed";
|
|
10149
9851
|
cleanupPluginOAuthSession(session);
|
|
10150
9852
|
pluginOAuthSessions.delete(sessionId);
|
|
10151
|
-
|
|
9853
|
+
json(res, { status: "completed", saved: true });
|
|
10152
9854
|
} catch (err) {
|
|
10153
9855
|
session.status = "error";
|
|
10154
9856
|
session.error = err instanceof Error ? err.message : "Token exchange failed";
|
|
10155
9857
|
cleanupPluginOAuthSession(session);
|
|
10156
9858
|
pluginOAuthSessions.delete(sessionId);
|
|
10157
|
-
|
|
9859
|
+
json(res, { error: session.error }, 500);
|
|
10158
9860
|
}
|
|
10159
|
-
}).catch(() =>
|
|
9861
|
+
}).catch(() => json(res, { error: "Invalid request body" }, 400));
|
|
10160
9862
|
return;
|
|
10161
9863
|
}
|
|
10162
9864
|
json(res, { error: "Auth plugin route not found" }, 404);
|
|
10163
9865
|
return;
|
|
10164
9866
|
}
|
|
10165
9867
|
if (path === "/api/auth/credential" && req.method === "POST") {
|
|
10166
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
10167
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
10168
|
-
return;
|
|
10169
|
-
}
|
|
10170
9868
|
readJsonBody(req).then(async (body) => {
|
|
10171
9869
|
const provider = typeof body.provider === "string" ? body.provider.trim() : "";
|
|
10172
9870
|
const credentialType = typeof body.type === "string" ? body.type.trim() : "";
|
|
10173
9871
|
const secret = typeof body.secret === "string" ? body.secret.trim() : "";
|
|
10174
9872
|
if (typeof body.configName !== "string" || body.configName.trim() === "") {
|
|
10175
|
-
|
|
9873
|
+
json(res, { error: "configName is required" }, 400);
|
|
10176
9874
|
return;
|
|
10177
9875
|
}
|
|
10178
9876
|
if (!provider || !isKnownLLMProvider(provider)) {
|
|
10179
|
-
|
|
9877
|
+
json(res, { error: "valid provider is required" }, 400);
|
|
10180
9878
|
return;
|
|
10181
9879
|
}
|
|
10182
9880
|
if (credentialType !== "api-key" && credentialType !== "bearer-token") {
|
|
10183
|
-
|
|
9881
|
+
json(res, { error: "type must be api-key or bearer-token" }, 400);
|
|
10184
9882
|
return;
|
|
10185
9883
|
}
|
|
10186
9884
|
if (!secret) {
|
|
10187
|
-
|
|
9885
|
+
json(res, { error: "secret is required" }, 400);
|
|
10188
9886
|
return;
|
|
10189
9887
|
}
|
|
10190
9888
|
if ((0, import_agent_qa_core11.getLLMAuthProviderPlugin)(provider)) {
|
|
10191
|
-
|
|
9889
|
+
json(res, { error: "Subscription providers use OAuth login" }, 400);
|
|
10192
9890
|
return;
|
|
10193
9891
|
}
|
|
10194
9892
|
if (credentialType === "bearer-token" && provider !== "anthropic-compatible") {
|
|
10195
|
-
|
|
9893
|
+
json(res, { error: "bearer-token credentials are only supported for anthropic-compatible configs" }, 400);
|
|
10196
9894
|
return;
|
|
10197
9895
|
}
|
|
10198
9896
|
if (credentialType === "api-key" && !API_KEY_CREDENTIAL_PROVIDERS.has(provider)) {
|
|
10199
|
-
|
|
9897
|
+
json(res, { error: "api-key credentials are not supported for this provider" }, 400);
|
|
10200
9898
|
return;
|
|
10201
9899
|
}
|
|
10202
9900
|
try {
|
|
@@ -10211,35 +9909,31 @@ function createRouter(dbOrDeps) {
|
|
|
10211
9909
|
return checked;
|
|
10212
9910
|
});
|
|
10213
9911
|
if (!target.ok) {
|
|
10214
|
-
|
|
9912
|
+
json(res, credentialRefusalBody(target), target.status);
|
|
10215
9913
|
return;
|
|
10216
9914
|
}
|
|
10217
|
-
|
|
9915
|
+
json(res, { saved: true, created: target.created });
|
|
10218
9916
|
} catch (err) {
|
|
10219
|
-
|
|
9917
|
+
json(res, { error: err instanceof Error ? err.message : "Failed to save credential" }, 500);
|
|
10220
9918
|
}
|
|
10221
|
-
}).catch(() =>
|
|
9919
|
+
}).catch(() => json(res, { error: "Invalid request body" }, 400));
|
|
10222
9920
|
return;
|
|
10223
9921
|
}
|
|
10224
9922
|
if (path === "/api/auth/credential/move" && req.method === "POST") {
|
|
10225
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
10226
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
10227
|
-
return;
|
|
10228
|
-
}
|
|
10229
9923
|
readJsonBody(req).then(async (raw) => {
|
|
10230
9924
|
const parsed = import_agent_qa_schema12.LlmCredentialMoveRequestSchema.safeParse(raw);
|
|
10231
9925
|
if (!parsed.success) {
|
|
10232
|
-
|
|
9926
|
+
json(res, { error: "invalid_setup_input" }, 400);
|
|
10233
9927
|
return;
|
|
10234
9928
|
}
|
|
10235
9929
|
const from = checkCredentialName(parsed.data.from);
|
|
10236
9930
|
const to = checkCredentialName(parsed.data.to);
|
|
10237
9931
|
if (!from.ok || !to.ok) {
|
|
10238
|
-
|
|
9932
|
+
json(res, { error: "name_invalid" }, 400);
|
|
10239
9933
|
return;
|
|
10240
9934
|
}
|
|
10241
9935
|
if (!configManager) {
|
|
10242
|
-
|
|
9936
|
+
json(res, { error: "Config manager is required to move credentials" }, 400);
|
|
10243
9937
|
return;
|
|
10244
9938
|
}
|
|
10245
9939
|
try {
|
|
@@ -10257,25 +9951,21 @@ function createRouter(dbOrDeps) {
|
|
|
10257
9951
|
return void 0;
|
|
10258
9952
|
});
|
|
10259
9953
|
if (refusal) {
|
|
10260
|
-
|
|
9954
|
+
json(res, credentialRefusalBody(refusal), refusal.status);
|
|
10261
9955
|
return;
|
|
10262
9956
|
}
|
|
10263
|
-
|
|
9957
|
+
json(res, { moved: true });
|
|
10264
9958
|
} catch (err) {
|
|
10265
|
-
|
|
9959
|
+
json(res, { error: err instanceof Error ? err.message : "Failed to move credential" }, 500);
|
|
10266
9960
|
}
|
|
10267
|
-
}).catch(() =>
|
|
9961
|
+
}).catch(() => json(res, { error: "invalid_setup_input" }, 400));
|
|
10268
9962
|
return;
|
|
10269
9963
|
}
|
|
10270
9964
|
if (path === "/api/llm/test" && req.method === "POST") {
|
|
10271
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
10272
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
10273
|
-
return;
|
|
10274
|
-
}
|
|
10275
9965
|
readJsonBody(req).then(async (body) => {
|
|
10276
9966
|
const rawBody = body;
|
|
10277
9967
|
if ("apiKey" in rawBody) {
|
|
10278
|
-
|
|
9968
|
+
json(res, {
|
|
10279
9969
|
success: false,
|
|
10280
9970
|
error: "invalid_request",
|
|
10281
9971
|
message: "apiKey is not accepted in LLM test requests. Save credentials for the named config instead."
|
|
@@ -10285,7 +9975,7 @@ function createRouter(dbOrDeps) {
|
|
|
10285
9975
|
const providerName = typeof body.provider === "string" ? body.provider.trim() : "";
|
|
10286
9976
|
const modelName = typeof body.model === "string" ? body.model.trim() : "";
|
|
10287
9977
|
if (!providerName || !modelName) {
|
|
10288
|
-
|
|
9978
|
+
json(res, { success: false, error: "invalid_request", message: "provider and model are required" }, 400);
|
|
10289
9979
|
return;
|
|
10290
9980
|
}
|
|
10291
9981
|
const configName = typeof body.configName === "string" ? body.configName.trim() : "";
|
|
@@ -10310,7 +10000,7 @@ function createRouter(dbOrDeps) {
|
|
|
10310
10000
|
}
|
|
10311
10001
|
const parsedConfig = import_agent_qa_core11.ModelConfigSchema.safeParse(llmCandidate);
|
|
10312
10002
|
if (!parsedConfig.success) {
|
|
10313
|
-
|
|
10003
|
+
json(res, {
|
|
10314
10004
|
success: false,
|
|
10315
10005
|
error: "invalid_request",
|
|
10316
10006
|
message: parsedConfig.error.issues.map((issue) => `${issue.path.map(String).join(".") || "config"}: ${issue.message}`).join("; ")
|
|
@@ -10322,7 +10012,7 @@ function createRouter(dbOrDeps) {
|
|
|
10322
10012
|
const coreAuth = await import("@vostride/agent-qa-core");
|
|
10323
10013
|
const resolvedAuth = await coreAuth.resolveLLMAuth(configName, llmConfig2);
|
|
10324
10014
|
if (resolvedAuth.kind === "missing") {
|
|
10325
|
-
|
|
10015
|
+
json(res, {
|
|
10326
10016
|
success: false,
|
|
10327
10017
|
error: "missing_credential",
|
|
10328
10018
|
message: resolvedAuth.message
|
|
@@ -10351,7 +10041,7 @@ function createRouter(dbOrDeps) {
|
|
|
10351
10041
|
} finally {
|
|
10352
10042
|
clearTimeout(timeout);
|
|
10353
10043
|
}
|
|
10354
|
-
|
|
10044
|
+
json(res, {
|
|
10355
10045
|
success: true,
|
|
10356
10046
|
model: modelName,
|
|
10357
10047
|
provider: providerName,
|
|
@@ -10366,7 +10056,7 @@ function createRouter(dbOrDeps) {
|
|
|
10366
10056
|
const providerMessage = withDeclaredMask(declaredMask, (mask) => mask(unsavedMessage));
|
|
10367
10057
|
const errorClass = (0, import_agent_qa_schema12.classifyProviderError)(providerMessage ?? unsavedMessage, statusCode);
|
|
10368
10058
|
const message = providerMessage === void 0 ? (0, import_agent_qa_schema12.providerErrorSentence)(errorClass, "").trimEnd() : (0, import_agent_qa_schema12.providerErrorSentence)(errorClass, providerMessage);
|
|
10369
|
-
|
|
10059
|
+
json(res, {
|
|
10370
10060
|
success: false,
|
|
10371
10061
|
error: errorClass,
|
|
10372
10062
|
message,
|
|
@@ -10376,7 +10066,7 @@ function createRouter(dbOrDeps) {
|
|
|
10376
10066
|
...unauthenticated ? { unauthenticated: true, authMessage } : {}
|
|
10377
10067
|
});
|
|
10378
10068
|
}
|
|
10379
|
-
}).catch(() =>
|
|
10069
|
+
}).catch(() => json(res, { success: false, error: "invalid_request", message: "Invalid request body" }, 400));
|
|
10380
10070
|
return;
|
|
10381
10071
|
}
|
|
10382
10072
|
if (path === "/api/cache/purge" && req.method === "POST") {
|
|
@@ -10385,56 +10075,15 @@ function createRouter(dbOrDeps) {
|
|
|
10385
10075
|
return;
|
|
10386
10076
|
}
|
|
10387
10077
|
readJsonBody(req).then(async (body) => {
|
|
10388
|
-
if (
|
|
10389
|
-
json(res, { error: "
|
|
10078
|
+
if (body.all !== true) {
|
|
10079
|
+
json(res, { error: "all is required" }, 400);
|
|
10390
10080
|
return;
|
|
10391
10081
|
}
|
|
10392
10082
|
try {
|
|
10393
10083
|
const cfg = await readConfigValues(configManager);
|
|
10394
|
-
const cacheDir = cfg.services?.cache?.dir ?? cfg.cache?.dir;
|
|
10395
|
-
if (!cacheDir) {
|
|
10396
|
-
json(res, { error: "Cache directory not configured" }, 400);
|
|
10397
|
-
return;
|
|
10398
|
-
}
|
|
10399
10084
|
const configDir = deps.configPath ? (0, import_node_path10.dirname)((0, import_node_path10.resolve)(deps.configPath)) : process.cwd();
|
|
10400
|
-
const
|
|
10401
|
-
|
|
10402
|
-
try {
|
|
10403
|
-
branchDirs = await (0, import_promises7.readdir)(resolvedCacheDir);
|
|
10404
|
-
} catch (err) {
|
|
10405
|
-
if (err.code === "ENOENT") {
|
|
10406
|
-
json(res, { purged: 0 });
|
|
10407
|
-
return;
|
|
10408
|
-
}
|
|
10409
|
-
throw err;
|
|
10410
|
-
}
|
|
10411
|
-
let purged = 0;
|
|
10412
|
-
for (const branch of branchDirs) {
|
|
10413
|
-
const branchPath = (0, import_node_path10.join)(resolvedCacheDir, branch);
|
|
10414
|
-
let entryStat;
|
|
10415
|
-
try {
|
|
10416
|
-
entryStat = await (0, import_promises7.stat)(branchPath);
|
|
10417
|
-
} catch {
|
|
10418
|
-
continue;
|
|
10419
|
-
}
|
|
10420
|
-
if (!entryStat.isDirectory()) {
|
|
10421
|
-
await (0, import_promises7.rm)(branchPath, { force: true });
|
|
10422
|
-
continue;
|
|
10423
|
-
}
|
|
10424
|
-
let files;
|
|
10425
|
-
try {
|
|
10426
|
-
files = await (0, import_promises7.readdir)(branchPath);
|
|
10427
|
-
} catch {
|
|
10428
|
-
continue;
|
|
10429
|
-
}
|
|
10430
|
-
for (const file of files) {
|
|
10431
|
-
if (!file.endsWith(".json")) continue;
|
|
10432
|
-
await (0, import_promises7.rm)((0, import_node_path10.join)(branchPath, file), { force: true });
|
|
10433
|
-
purged++;
|
|
10434
|
-
}
|
|
10435
|
-
await (0, import_promises7.rm)(branchPath, { recursive: true, force: true }).catch(() => {
|
|
10436
|
-
});
|
|
10437
|
-
}
|
|
10085
|
+
const cacheDir = cfg.services?.cache?.dir ?? import_agent_qa_core11.DEFAULT_AGENT_QA_CACHE_DIR;
|
|
10086
|
+
const purged = await (0, import_agent_qa_runner_local2.purgeActionCache)((0, import_node_path10.resolve)(configDir, cacheDir));
|
|
10438
10087
|
json(res, { purged });
|
|
10439
10088
|
} catch (err) {
|
|
10440
10089
|
json(res, { error: err instanceof Error ? err.message : "Cache purge failed" }, 500);
|
|
@@ -10518,7 +10167,7 @@ function createRouter(dbOrDeps) {
|
|
|
10518
10167
|
return;
|
|
10519
10168
|
}
|
|
10520
10169
|
const workspaceEnv = await readWorkspaceEnvVars(workspacePaths);
|
|
10521
|
-
const result = await new import_agent_qa_runner_local2.DockerHookExecutor().execute(resolvedHook, {
|
|
10170
|
+
const result = await new import_agent_qa_runner_local2.DockerHookExecutor({ version: (0, import_agent_qa_core11.getAgentQaVersion)() }).execute(resolvedHook, {
|
|
10522
10171
|
env: {
|
|
10523
10172
|
...workspaceEnv,
|
|
10524
10173
|
...validation.overrides
|
|
@@ -10554,7 +10203,7 @@ function createRouter(dbOrDeps) {
|
|
|
10554
10203
|
variables: shown.variables,
|
|
10555
10204
|
sandbox: {
|
|
10556
10205
|
runtime: resolvedHook.runtime,
|
|
10557
|
-
image: import_agent_qa_core11.
|
|
10206
|
+
image: (0, import_agent_qa_core11.hookRunnerImage)(resolvedHook.runtime, (0, import_agent_qa_core11.getAgentQaVersion)()),
|
|
10558
10207
|
networkMode: resolvedHook.network ? "enabled" : "disabled",
|
|
10559
10208
|
dockerVersion: null,
|
|
10560
10209
|
networkLogsAvailable: shown.networkLogs.length > 0,
|
|
@@ -10758,12 +10407,8 @@ function createRouter(dbOrDeps) {
|
|
|
10758
10407
|
return;
|
|
10759
10408
|
}
|
|
10760
10409
|
if (req.method === "GET" && /^\/api\/live-editor\/sessions\/[^/]+\/live-url$/.test(path)) {
|
|
10761
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
10762
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
10763
|
-
return;
|
|
10764
|
-
}
|
|
10765
10410
|
if (!deps.mintEditorLiveUrl) {
|
|
10766
|
-
|
|
10411
|
+
json(res, { error: "live_view_unavailable" }, 503);
|
|
10767
10412
|
return;
|
|
10768
10413
|
}
|
|
10769
10414
|
let editorSessionId;
|
|
@@ -10772,33 +10417,32 @@ function createRouter(dbOrDeps) {
|
|
|
10772
10417
|
path.slice("/api/live-editor/sessions/".length, -"/live-url".length)
|
|
10773
10418
|
);
|
|
10774
10419
|
} catch {
|
|
10775
|
-
|
|
10420
|
+
json(res, { error: "unknown_session" }, 404);
|
|
10776
10421
|
return;
|
|
10777
10422
|
}
|
|
10778
10423
|
void deps.mintEditorLiveUrl(editorSessionId).then(
|
|
10779
10424
|
(result) => {
|
|
10780
|
-
|
|
10425
|
+
json(res, result);
|
|
10781
10426
|
},
|
|
10782
10427
|
(error) => {
|
|
10783
10428
|
const name = error instanceof Error ? error.name : "";
|
|
10784
10429
|
if (name === "UnknownLiveEditorSessionError") {
|
|
10785
|
-
|
|
10430
|
+
json(res, { error: "unknown_session" }, 404);
|
|
10786
10431
|
return;
|
|
10787
10432
|
}
|
|
10788
10433
|
if (name === "LiveViewUnavailableError") {
|
|
10789
|
-
|
|
10434
|
+
json(res, { error: "live_view_unavailable" }, 503);
|
|
10790
10435
|
return;
|
|
10791
10436
|
}
|
|
10792
|
-
|
|
10437
|
+
json(res, { error: "live_url_mint_failed" }, 500);
|
|
10793
10438
|
}
|
|
10794
10439
|
);
|
|
10795
10440
|
return;
|
|
10796
10441
|
}
|
|
10797
10442
|
const liveEditorAuthStateMatch = path.match(/^\/api\/live-editor\/sessions\/([^/]+)\/auth-state$/);
|
|
10798
10443
|
if (liveEditorAuthStateMatch && req.method === "POST") {
|
|
10799
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
10800
10444
|
if (!sessionManager) {
|
|
10801
|
-
|
|
10445
|
+
json(res, { error: "Live editor not available" }, 503);
|
|
10802
10446
|
return;
|
|
10803
10447
|
}
|
|
10804
10448
|
;
|
|
@@ -10809,35 +10453,34 @@ function createRouter(dbOrDeps) {
|
|
|
10809
10453
|
const body = await readJsonBody(req);
|
|
10810
10454
|
const parsedName = import_agent_qa_core11.AuthStateNameSchema.safeParse(typeof body?.name === "string" ? body.name : "");
|
|
10811
10455
|
if (!parsedName.success) {
|
|
10812
|
-
|
|
10456
|
+
json(res, { error: "Auth state name must be a lowercase slug." }, 400);
|
|
10813
10457
|
return;
|
|
10814
10458
|
}
|
|
10815
10459
|
stateName = parsedName.data;
|
|
10816
10460
|
session = sessionManager.getSession(decodeURIComponent(liveEditorAuthStateMatch[1]));
|
|
10817
10461
|
if (!session) {
|
|
10818
|
-
|
|
10462
|
+
json(res, { error: "Session not found" }, 404);
|
|
10819
10463
|
return;
|
|
10820
10464
|
}
|
|
10821
10465
|
if (typeof session.captureWebAuthState !== "function") {
|
|
10822
|
-
|
|
10466
|
+
json(res, {
|
|
10823
10467
|
error: `Could not save auth state "${stateName}" for target "${getSessionTargetNameForAuthState(session)}".`
|
|
10824
10468
|
}, 500);
|
|
10825
10469
|
return;
|
|
10826
10470
|
}
|
|
10827
10471
|
const authState = await session.captureWebAuthState(stateName, { replace: body?.replace === true });
|
|
10828
|
-
|
|
10472
|
+
json(res, { authState });
|
|
10829
10473
|
} catch (err) {
|
|
10830
10474
|
const targetName = getSessionTargetNameForAuthState(session);
|
|
10831
10475
|
const response = buildAuthStateSaveErrorMessage(stateName || "unknown", targetName, err);
|
|
10832
|
-
|
|
10476
|
+
json(res, { error: response.message }, response.status);
|
|
10833
10477
|
}
|
|
10834
10478
|
})();
|
|
10835
10479
|
return;
|
|
10836
10480
|
}
|
|
10837
10481
|
if (path === "/api/live-editor/sessions" && req.method === "POST") {
|
|
10838
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
10839
10482
|
if (!sessionManager) {
|
|
10840
|
-
|
|
10483
|
+
json(res, { error: "Live editor not available" }, 503);
|
|
10841
10484
|
return;
|
|
10842
10485
|
}
|
|
10843
10486
|
;
|
|
@@ -10845,16 +10488,10 @@ function createRouter(dbOrDeps) {
|
|
|
10845
10488
|
try {
|
|
10846
10489
|
const body = await readJsonBody(req);
|
|
10847
10490
|
const targetName = typeof body?.targetName === "string" ? body.targetName : void 0;
|
|
10848
|
-
const production = await resolveLiveTargetProduction(configManager, targetName);
|
|
10849
|
-
const productionRefusal = liveProductionRefusal(production);
|
|
10850
|
-
if (productionRefusal !== void 0) {
|
|
10851
|
-
jsonNoCors(res, { error: productionRefusal.message }, 403);
|
|
10852
|
-
return;
|
|
10853
|
-
}
|
|
10854
10491
|
const platform = body?.platform ?? "web";
|
|
10855
10492
|
const runtimeLLM = await resolveDashboardRuntimeLLM(configManager, llmConfig, authFetch);
|
|
10856
10493
|
if (!runtimeLLM.ok) {
|
|
10857
|
-
|
|
10494
|
+
json(res, { error: runtimeLLM.error }, 400);
|
|
10858
10495
|
return;
|
|
10859
10496
|
}
|
|
10860
10497
|
const setupHooks = Array.isArray(body?.setupHooks) ? body.setupHooks.filter((value) => typeof value === "string" && value.trim().length > 0) : [];
|
|
@@ -10873,14 +10510,14 @@ function createRouter(dbOrDeps) {
|
|
|
10873
10510
|
let resolvedHooks = /* @__PURE__ */ new Map();
|
|
10874
10511
|
let hookRegistryError;
|
|
10875
10512
|
if (!workspacePaths) {
|
|
10876
|
-
|
|
10513
|
+
json(res, { error: "Workspace path resolution not available" }, 500);
|
|
10877
10514
|
return;
|
|
10878
10515
|
}
|
|
10879
10516
|
try {
|
|
10880
10517
|
const envContent = await (0, import_promises7.readFile)(workspacePaths.envFile.absolutePath, "utf-8");
|
|
10881
10518
|
envVars = (0, import_agent_qa_core11.parseEnvFile)(envContent);
|
|
10882
10519
|
} catch {
|
|
10883
|
-
|
|
10520
|
+
json(res, { error: `workspace.envFile file could not be loaded: ${workspacePaths.envFile.workspaceRelativePath}` }, 400);
|
|
10884
10521
|
return;
|
|
10885
10522
|
}
|
|
10886
10523
|
try {
|
|
@@ -10888,7 +10525,7 @@ function createRouter(dbOrDeps) {
|
|
|
10888
10525
|
secretStore = import_agent_qa_runner2.SecretStore.fromEnvContent(secretsContent);
|
|
10889
10526
|
secretRedactor = new import_agent_qa_runner2.SecretRedactor(secretStore);
|
|
10890
10527
|
} catch {
|
|
10891
|
-
|
|
10528
|
+
json(res, { error: `workspace.secretsFile file could not be loaded: ${workspacePaths.secretsFile.workspaceRelativePath}` }, 400);
|
|
10892
10529
|
return;
|
|
10893
10530
|
}
|
|
10894
10531
|
const hookCatalog = await readWorkspaceHooks(configManager, deps.configPath);
|
|
@@ -10899,6 +10536,11 @@ function createRouter(dbOrDeps) {
|
|
|
10899
10536
|
...suiteFileManager === void 0 ? {} : { findSuite: (id) => suiteFileManager.findBySuiteId(id) }
|
|
10900
10537
|
};
|
|
10901
10538
|
const plannerLimits = await resolveLivePlannerLimits(configManager, entityRef, entityReaders);
|
|
10539
|
+
const navigationTimeoutMs = await resolveLiveNavigationTimeoutMs(
|
|
10540
|
+
configManager,
|
|
10541
|
+
entityRef,
|
|
10542
|
+
entityReaders
|
|
10543
|
+
);
|
|
10902
10544
|
const sessionSuiteRef = entityRef?.type === "suite" ? entityRef : void 0;
|
|
10903
10545
|
const plannerLimitsForTest = (testId) => resolveLivePlannerLimits(
|
|
10904
10546
|
configManager,
|
|
@@ -10909,7 +10551,6 @@ function createRouter(dbOrDeps) {
|
|
|
10909
10551
|
{
|
|
10910
10552
|
platform,
|
|
10911
10553
|
targetName,
|
|
10912
|
-
production: { productionTargetNames: production.productionTargetNames },
|
|
10913
10554
|
plannerLimits,
|
|
10914
10555
|
plannerLimitsForTest,
|
|
10915
10556
|
llmConfig: runtimeLLM.llmConfig,
|
|
@@ -10923,6 +10564,7 @@ function createRouter(dbOrDeps) {
|
|
|
10923
10564
|
hookRegistryError,
|
|
10924
10565
|
url: body?.url,
|
|
10925
10566
|
headless: body?.headless ?? false,
|
|
10567
|
+
navigationTimeoutMs,
|
|
10926
10568
|
useDeviceName: typeof body?.useDeviceName === "string" ? body.useDeviceName : void 0,
|
|
10927
10569
|
appState: body?.appState === "preserve" || body?.appState === "reset" ? body.appState : void 0,
|
|
10928
10570
|
bundleId: typeof body?.bundleId === "string" ? body.bundleId : void 0,
|
|
@@ -10932,38 +10574,37 @@ function createRouter(dbOrDeps) {
|
|
|
10932
10574
|
},
|
|
10933
10575
|
entityRef
|
|
10934
10576
|
);
|
|
10935
|
-
|
|
10577
|
+
json(res, { sessionId, sessionNumber }, 201);
|
|
10936
10578
|
} catch (err) {
|
|
10937
10579
|
const message = err instanceof Error ? err.message : String(err);
|
|
10938
|
-
const
|
|
10939
|
-
|
|
10580
|
+
const refused = err instanceof LivePlannerLimitsRefusedError || err instanceof LiveNavigationTimeoutRefusedError;
|
|
10581
|
+
const status = refused ? 400 : 500;
|
|
10582
|
+
json(res, { error: message }, status);
|
|
10940
10583
|
}
|
|
10941
10584
|
})();
|
|
10942
10585
|
return;
|
|
10943
10586
|
}
|
|
10944
10587
|
if (path === "/api/live-editor/sessions" && req.method === "GET") {
|
|
10945
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
10946
10588
|
if (!sessionManager) {
|
|
10947
|
-
|
|
10589
|
+
json(res, { error: "Live editor not available" }, 503);
|
|
10948
10590
|
return;
|
|
10949
10591
|
}
|
|
10950
|
-
|
|
10592
|
+
json(res, { sessions: sessionManager.listSessions() });
|
|
10951
10593
|
return;
|
|
10952
10594
|
}
|
|
10953
10595
|
const liveEditorSessionMatch = path.match(/^\/api\/live-editor\/sessions\/([^/]+)$/);
|
|
10954
10596
|
if (liveEditorSessionMatch && req.method === "DELETE") {
|
|
10955
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
10956
10597
|
if (!sessionManager) {
|
|
10957
|
-
|
|
10598
|
+
json(res, { error: "Live editor not available" }, 503);
|
|
10958
10599
|
return;
|
|
10959
10600
|
}
|
|
10960
10601
|
const sessionId = liveEditorSessionMatch[1];
|
|
10961
10602
|
(async () => {
|
|
10962
10603
|
const terminated = await sessionManager.terminateSession(sessionId);
|
|
10963
10604
|
if (terminated) {
|
|
10964
|
-
|
|
10605
|
+
json(res, { ok: true });
|
|
10965
10606
|
} else {
|
|
10966
|
-
|
|
10607
|
+
json(res, { error: "Session not found" }, 404);
|
|
10967
10608
|
}
|
|
10968
10609
|
})();
|
|
10969
10610
|
return;
|
|
@@ -11012,6 +10653,7 @@ function createRouter(dbOrDeps) {
|
|
|
11012
10653
|
// src/queue/job-queue.ts
|
|
11013
10654
|
var import_node_events = require("node:events");
|
|
11014
10655
|
var import_node_os2 = require("node:os");
|
|
10656
|
+
var import_agent_qa_core12 = require("@vostride/agent-qa-core");
|
|
11015
10657
|
var NOT_CANCELLABLE = { kind: "not-cancellable" };
|
|
11016
10658
|
var JobQueue = class extends import_node_events.EventEmitter {
|
|
11017
10659
|
db;
|
|
@@ -11034,8 +10676,7 @@ var JobQueue = class extends import_node_events.EventEmitter {
|
|
|
11034
10676
|
this.pollInterval = setInterval(() => this.dispatchReported("a poll"), intervalMs);
|
|
11035
10677
|
this.on("enqueued", () => this.dispatchReported("an enqueue"));
|
|
11036
10678
|
}
|
|
11037
|
-
//
|
|
11038
|
-
// still frees its slot, and nothing reads the closed database.
|
|
10679
|
+
// The server closes the database after this; a run settling later still frees its slot.
|
|
11039
10680
|
stop() {
|
|
11040
10681
|
this.stopped = true;
|
|
11041
10682
|
if (this.pollInterval) {
|
|
@@ -11121,7 +10762,10 @@ var JobQueue = class extends import_node_events.EventEmitter {
|
|
|
11121
10762
|
this.processNext();
|
|
11122
10763
|
} catch (error) {
|
|
11123
10764
|
const message = error instanceof Error ? error.message : String(error);
|
|
11124
|
-
|
|
10765
|
+
(0, import_agent_qa_core12.printRunText)(
|
|
10766
|
+
`Queue could not dispatch the next run after ${trigger}: ${message}`,
|
|
10767
|
+
(text) => console.error(text)
|
|
10768
|
+
);
|
|
11125
10769
|
}
|
|
11126
10770
|
}
|
|
11127
10771
|
getActiveCount() {
|
|
@@ -11130,9 +10774,7 @@ var JobQueue = class extends import_node_events.EventEmitter {
|
|
|
11130
10774
|
getConcurrency() {
|
|
11131
10775
|
return this.concurrency;
|
|
11132
10776
|
}
|
|
11133
|
-
//
|
|
11134
|
-
// 442-15 (GAP-D2): own is the stamp this handle wrote, or equal non-null start times.
|
|
11135
|
-
// 2026-10-17 (444-06, D-11): a running row is never written here; the run answers with its verdict.
|
|
10777
|
+
// Only the owner stops its running row, never written here; a gone owner's row is an orphan.
|
|
11136
10778
|
cancel(runId) {
|
|
11137
10779
|
const run = this.db.getRun(runId);
|
|
11138
10780
|
if (!run) return NOT_CANCELLABLE;
|
|
@@ -11162,21 +10804,7 @@ var JobQueue = class extends import_node_events.EventEmitter {
|
|
|
11162
10804
|
const run = this.db.getRun(runId);
|
|
11163
10805
|
return run && run.status !== "running" && run.status !== "pending" ? { kind: "finished", status: run.status } : NOT_CANCELLABLE;
|
|
11164
10806
|
}
|
|
11165
|
-
//
|
|
11166
|
-
// MODEL THAT MINTED ITS CHILDREN.
|
|
11167
|
-
//
|
|
11168
|
-
// ~19 lines stood here walking the run's child rows and cancelling each member row. Under
|
|
11169
|
-
// the one-run model a parallel suite is ONE run row with member handles (402-04) — nothing writes
|
|
11170
|
-
// the run row's parent-link column any more (405-07 then deleted the column outright), so the
|
|
11171
|
-
// walk could only ever have read an empty list while reading as live cancellation policy.
|
|
11172
|
-
// Cancelling the run cancels the run.
|
|
11173
|
-
//
|
|
11174
|
-
// WHAT THIS DOES NOT TOUCH: `parallel` below. D-11 keeps the queue's parallel flag as CROSS-RUN
|
|
11175
|
-
// concurrency of INDEPENDENT jobs, which is what `findNextEligible` has always used it for. What
|
|
11176
|
-
// was deleted is parallel-SUITE semantics, not the queue's ability to run two unrelated jobs.
|
|
11177
|
-
//
|
|
11178
|
-
// HISTORICAL ROWS: a pre-402 suite run still lists its members on the read side; cancelling
|
|
11179
|
-
// such a run now leaves those member rows in whatever state their own process left them.
|
|
10807
|
+
// `parallel` is cross-run concurrency of independent jobs; a suite is one run row.
|
|
11180
10808
|
findNextEligible() {
|
|
11181
10809
|
if (this.activeCount >= this.concurrency) return void 0;
|
|
11182
10810
|
const pending = this.db.getPendingRuns();
|
|
@@ -11203,17 +10831,17 @@ var import_promises8 = require("node:fs/promises");
|
|
|
11203
10831
|
var import_node_path11 = require("node:path");
|
|
11204
10832
|
var import_agent_qa_ids3 = require("@vostride/agent-qa-ids");
|
|
11205
10833
|
var import_yaml8 = require("yaml");
|
|
11206
|
-
var
|
|
10834
|
+
var import_agent_qa_core13 = require("@vostride/agent-qa-core");
|
|
11207
10835
|
var SuiteFileManager = class {
|
|
11208
10836
|
constructor(workspace, testFileManager) {
|
|
11209
10837
|
this.workspace = workspace;
|
|
11210
10838
|
this.testFileManager = testFileManager;
|
|
11211
10839
|
}
|
|
11212
10840
|
testFileManager;
|
|
11213
|
-
//
|
|
10841
|
+
// Each list call reads the workspace itself; each file is written whole, one writer at a time.
|
|
11214
10842
|
underLock = createPathLock();
|
|
11215
10843
|
async list() {
|
|
11216
|
-
const records = await (0,
|
|
10844
|
+
const records = await (0, import_agent_qa_core13.discoverWorkspaceFiles)({ workspace: this.workspace, kind: "suite" });
|
|
11217
10845
|
const files = [];
|
|
11218
10846
|
for (const record of records) {
|
|
11219
10847
|
try {
|
|
@@ -11275,7 +10903,7 @@ var SuiteFileManager = class {
|
|
|
11275
10903
|
if (!trimmedPath) {
|
|
11276
10904
|
throw new Error("Suite path is required");
|
|
11277
10905
|
}
|
|
11278
|
-
const target = await (0,
|
|
10906
|
+
const target = await (0, import_agent_qa_core13.resolveWorkspaceFileTarget)({
|
|
11279
10907
|
workspace: this.workspace,
|
|
11280
10908
|
kind: "suite",
|
|
11281
10909
|
filePath: trimmedPath,
|
|
@@ -11299,7 +10927,7 @@ var SuiteFileManager = class {
|
|
|
11299
10927
|
async validate(content) {
|
|
11300
10928
|
try {
|
|
11301
10929
|
const parsed = (0, import_yaml8.parse)(content);
|
|
11302
|
-
const result =
|
|
10930
|
+
const result = import_agent_qa_core13.SuiteDefinitionSchema.safeParse(parsed);
|
|
11303
10931
|
if (!result.success) {
|
|
11304
10932
|
return {
|
|
11305
10933
|
valid: false,
|
|
@@ -11337,7 +10965,7 @@ var SuiteFileManager = class {
|
|
|
11337
10965
|
}
|
|
11338
10966
|
}
|
|
11339
10967
|
async resolveTarget(relativePath, requireExisting) {
|
|
11340
|
-
return (0,
|
|
10968
|
+
return (0, import_agent_qa_core13.resolveWorkspaceFileTarget)({
|
|
11341
10969
|
workspace: this.workspace,
|
|
11342
10970
|
kind: "suite",
|
|
11343
10971
|
filePath: relativePath,
|
|
@@ -11349,7 +10977,7 @@ var SuiteFileManager = class {
|
|
|
11349
10977
|
// src/execution/appium-manager.ts
|
|
11350
10978
|
var import_node_http = require("node:http");
|
|
11351
10979
|
var import_node_child_process2 = require("node:child_process");
|
|
11352
|
-
var
|
|
10980
|
+
var import_agent_qa_core14 = require("@vostride/agent-qa-core");
|
|
11353
10981
|
var import_tree_kill = __toESM(require("tree-kill"), 1);
|
|
11354
10982
|
var import_picocolors = __toESM(require("picocolors"), 1);
|
|
11355
10983
|
var APPIUM_PREFIX = import_picocolors.default.magenta("[appium]");
|
|
@@ -11370,7 +10998,7 @@ var AppiumManager = class {
|
|
|
11370
10998
|
this.startupTimeoutMs = opts?.startupTimeoutMs ?? 3e4;
|
|
11371
10999
|
this.pollIntervalMs = opts?.pollIntervalMs ?? 500;
|
|
11372
11000
|
this.logLevel = opts?.logLevel ?? "normal";
|
|
11373
|
-
this.appiumResolver = opts?.appiumResolver ??
|
|
11001
|
+
this.appiumResolver = opts?.appiumResolver ?? import_agent_qa_core14.resolveAppiumExecutable;
|
|
11374
11002
|
}
|
|
11375
11003
|
async acquire() {
|
|
11376
11004
|
await this.ensureRunning();
|
|
@@ -11473,23 +11101,23 @@ var AppiumManager = class {
|
|
|
11473
11101
|
this.managed = true;
|
|
11474
11102
|
child.on("error", (err) => {
|
|
11475
11103
|
if (err.code === "ENOENT") {
|
|
11476
|
-
console.error(`${APPIUM_PREFIX} Appium not found. ${(0,
|
|
11104
|
+
console.error(`${APPIUM_PREFIX} Appium not found. ${(0, import_agent_qa_core14.formatAppiumInstallGuidance)()}`);
|
|
11477
11105
|
} else {
|
|
11478
|
-
|
|
11106
|
+
(0, import_agent_qa_core14.printRunText)(`${APPIUM_PREFIX} Error: ${err.message}`, (text) => console.error(text));
|
|
11479
11107
|
}
|
|
11480
11108
|
});
|
|
11481
11109
|
child.stdout?.on("data", (chunk) => {
|
|
11482
11110
|
if (this.logLevel !== "debug") return;
|
|
11483
11111
|
const lines = chunk.toString().split("\n").filter(Boolean);
|
|
11484
11112
|
for (const line of lines) {
|
|
11485
|
-
|
|
11113
|
+
(0, import_agent_qa_core14.printRunText)(`${APPIUM_PREFIX} ${import_picocolors.default.dim(line)}`, (text) => console.log(text));
|
|
11486
11114
|
}
|
|
11487
11115
|
});
|
|
11488
11116
|
child.stderr?.on("data", (chunk) => {
|
|
11489
11117
|
if (this.logLevel !== "debug") return;
|
|
11490
11118
|
const lines = chunk.toString().split("\n").filter(Boolean);
|
|
11491
11119
|
for (const line of lines) {
|
|
11492
|
-
|
|
11120
|
+
(0, import_agent_qa_core14.printRunText)(`${APPIUM_PREFIX} ${import_picocolors.default.dim(line)}`, (text) => console.error(text));
|
|
11493
11121
|
}
|
|
11494
11122
|
});
|
|
11495
11123
|
const start = Date.now();
|
|
@@ -11571,10 +11199,7 @@ var SessionManager = class {
|
|
|
11571
11199
|
this.deps = deps;
|
|
11572
11200
|
}
|
|
11573
11201
|
sessions = /* @__PURE__ */ new Map();
|
|
11574
|
-
//
|
|
11575
|
-
// Monotonic and ephemeral: resets on server restart per CONTEXT D-30.
|
|
11576
|
-
// Never decremented on terminate — a re-open of a terminated entity yields
|
|
11577
|
-
// the next number so "Session #3" after "#2" ends does not confuse users.
|
|
11202
|
+
// Monotonic per entity and reset on restart, so a reopened entity gets the next number.
|
|
11578
11203
|
counters = /* @__PURE__ */ new Map();
|
|
11579
11204
|
async createSession(config, entity) {
|
|
11580
11205
|
if (!config.llmConfig) {
|
|
@@ -11631,7 +11256,8 @@ var SessionManager = class {
|
|
|
11631
11256
|
};
|
|
11632
11257
|
|
|
11633
11258
|
// src/live-editor/ws-handler.ts
|
|
11634
|
-
var
|
|
11259
|
+
var import_node_util4 = require("node:util");
|
|
11260
|
+
var import_agent_qa_core15 = require("@vostride/agent-qa-core");
|
|
11635
11261
|
|
|
11636
11262
|
// src/live-editor/types.ts
|
|
11637
11263
|
var LIVE_EDITOR_UNKNOWN_SESSION_CLOSE_CODE = 4404;
|
|
@@ -11818,12 +11444,14 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
|
|
|
11818
11444
|
try {
|
|
11819
11445
|
raw = await testFileManager.read(msg.path);
|
|
11820
11446
|
} catch (readErr) {
|
|
11821
|
-
|
|
11447
|
+
const line = `[ws-handler] execute-test: read failed for path=${msg.path}`;
|
|
11448
|
+
(0, import_agent_qa_core15.printRunText)((0, import_node_util4.format)(line, readErr), () => console.error(line, readErr));
|
|
11822
11449
|
throw new Error(`Cannot read test file "${msg.path}": ${readErr?.message ?? String(readErr)}`);
|
|
11823
11450
|
}
|
|
11824
|
-
const parsed = (0,
|
|
11451
|
+
const parsed = (0, import_agent_qa_core15.parseTestFile)(raw, msg.path);
|
|
11825
11452
|
if (parsed.errors.length > 0) {
|
|
11826
|
-
|
|
11453
|
+
const line = `[ws-handler] execute-test: parse errors for ${msg.path}`;
|
|
11454
|
+
(0, import_agent_qa_core15.printRunText)((0, import_node_util4.format)(line, parsed.errors), () => console.error(line, parsed.errors));
|
|
11827
11455
|
throw new Error(`Parse error in ${msg.path}: ${parsed.errors[0].message}`);
|
|
11828
11456
|
}
|
|
11829
11457
|
const testDef = parsed.tests[0];
|
|
@@ -11833,7 +11461,13 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
|
|
|
11833
11461
|
const stepInstructions = testDef.steps.map(
|
|
11834
11462
|
(s) => typeof s === "string" ? s : s.step
|
|
11835
11463
|
);
|
|
11836
|
-
|
|
11464
|
+
const stepCount = stepInstructions.length;
|
|
11465
|
+
const setupCount = (testDef.setup ?? []).length;
|
|
11466
|
+
const teardownCount = (testDef.teardown ?? []).length;
|
|
11467
|
+
(0, import_agent_qa_core15.printRunText)(
|
|
11468
|
+
`[ws-handler] execute-test: starting "${testDef.name}" (${stepCount} steps, setup=${setupCount}, teardown=${teardownCount})`,
|
|
11469
|
+
(text) => console.log(text)
|
|
11470
|
+
);
|
|
11837
11471
|
reply({
|
|
11838
11472
|
type: "test-start",
|
|
11839
11473
|
test: {
|
|
@@ -11855,7 +11489,11 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
|
|
|
11855
11489
|
teardown: testDef.teardown ?? []
|
|
11856
11490
|
}
|
|
11857
11491
|
);
|
|
11858
|
-
|
|
11492
|
+
const errorPart = result.error ? ` error=${result.error}` : "";
|
|
11493
|
+
(0, import_agent_qa_core15.printRunText)(
|
|
11494
|
+
`[ws-handler] execute-test: finished "${testDef.name}" status=${result.status} duration=${result.duration}ms${errorPart}`,
|
|
11495
|
+
(text) => console.log(text)
|
|
11496
|
+
);
|
|
11859
11497
|
reply({
|
|
11860
11498
|
type: "test-complete",
|
|
11861
11499
|
test: {
|
|
@@ -11868,11 +11506,9 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
|
|
|
11868
11506
|
});
|
|
11869
11507
|
reply({ type: "session-state", state: session.getState() });
|
|
11870
11508
|
} catch (err) {
|
|
11871
|
-
|
|
11872
|
-
|
|
11873
|
-
|
|
11874
|
-
code: err?.code
|
|
11875
|
-
});
|
|
11509
|
+
const line = `[ws-handler] execute-test: caught error for path=${msg.path}`;
|
|
11510
|
+
const detail = { message: err?.message, stack: err?.stack, code: err?.code };
|
|
11511
|
+
(0, import_agent_qa_core15.printRunText)((0, import_node_util4.format)(line, detail), () => console.error(line, detail));
|
|
11876
11512
|
reply({
|
|
11877
11513
|
type: "test-error",
|
|
11878
11514
|
test: {
|
|
@@ -12023,11 +11659,9 @@ var LIVE_EMBED_STATES = [
|
|
|
12023
11659
|
"no-stream",
|
|
12024
11660
|
"expired",
|
|
12025
11661
|
"refused",
|
|
12026
|
-
//
|
|
12027
|
-
// because a person watching one member of a suite needs to know which of the two happened — and
|
|
12028
|
-
// neither may be shown as `live` over whichever frame happened to be last.
|
|
11662
|
+
// This member's stream ended while the run continues, so the viewer knows which one ended.
|
|
12029
11663
|
"stream-ended",
|
|
12030
|
-
//
|
|
11664
|
+
// An editor session ended; an editor subject has no run to end.
|
|
12031
11665
|
"session-ended",
|
|
12032
11666
|
"ended",
|
|
12033
11667
|
"lost"
|
|
@@ -12156,8 +11790,7 @@ function renderLiveEmbedPage(input) {
|
|
|
12156
11790
|
if (pending === null) return;
|
|
12157
11791
|
var header = pending;
|
|
12158
11792
|
pending = null;
|
|
12159
|
-
//
|
|
12160
|
-
// both paint in every host, not only in Chromium (428 RESEARCH A2).
|
|
11793
|
+
// No MIME type: the decoder sniffs, so JPEG and PNG frames paint in every host.
|
|
12161
11794
|
var blob = new Blob([event.data]);
|
|
12162
11795
|
createImageBitmap(blob).then(function (bitmap) {
|
|
12163
11796
|
if (canvas.width !== header.width || canvas.height !== header.height) {
|
|
@@ -12181,8 +11814,7 @@ function renderLiveEmbedPage(input) {
|
|
|
12181
11814
|
try { msg = JSON.parse(event.data); } catch (e) { return; }
|
|
12182
11815
|
|
|
12183
11816
|
if (msg.type === 'ready') {
|
|
12184
|
-
//
|
|
12185
|
-
// a transport guess (LIVE-04's prohibition). A snapshot paints on the same canvas (428 D-11).
|
|
11817
|
+
// The declared kind picks the painter, never a browser name, adapter or transport guess.
|
|
12186
11818
|
var kind = msg.stream ? msg.stream.kind : undefined;
|
|
12187
11819
|
if (kind === 'cdp-screencast' || kind === 'snapshot') {
|
|
12188
11820
|
setState('connecting', 'waiting for the first frame\u2026');
|
|
@@ -12229,8 +11861,7 @@ function renderLiveEmbedPage(input) {
|
|
|
12229
11861
|
report({ event: 'lost' });
|
|
12230
11862
|
});
|
|
12231
11863
|
|
|
12232
|
-
// A
|
|
12233
|
-
// so a renewal never costs the viewer the stream it is watching (D-02).
|
|
11864
|
+
// A re-minting host replaces the fragment; the socket renews in place and the stream continues.
|
|
12234
11865
|
window.addEventListener('hashchange', function () {
|
|
12235
11866
|
var next = location.hash.slice(1);
|
|
12236
11867
|
if (next === '' || socket.readyState !== 1) return;
|
|
@@ -12439,7 +12070,7 @@ function createFrameProxy(deps) {
|
|
|
12439
12070
|
}
|
|
12440
12071
|
|
|
12441
12072
|
// src/server/server.ts
|
|
12442
|
-
var
|
|
12073
|
+
var import_agent_qa_core16 = require("@vostride/agent-qa-core");
|
|
12443
12074
|
var MIME_TYPES = {
|
|
12444
12075
|
".html": "text/html; charset=utf-8",
|
|
12445
12076
|
".js": "application/javascript",
|
|
@@ -12458,9 +12089,10 @@ var MIME_TYPES = {
|
|
|
12458
12089
|
".webp": "image/webp"
|
|
12459
12090
|
};
|
|
12460
12091
|
var DashboardRemoteBindRefusedError = class extends Error {
|
|
12461
|
-
constructor(host) {
|
|
12092
|
+
constructor(host, port) {
|
|
12093
|
+
const p = String(port === 0 ? 3470 : port);
|
|
12462
12094
|
super(
|
|
12463
|
-
`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})
|
|
12095
|
+
`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}/.`
|
|
12464
12096
|
);
|
|
12465
12097
|
this.host = host;
|
|
12466
12098
|
this.name = "DashboardRemoteBindRefusedError";
|
|
@@ -12471,8 +12103,8 @@ async function startServer(opts) {
|
|
|
12471
12103
|
const { db, port = 3470, uiDir, workspacePaths, configPath, llmConfig, authFetch, agentHost, executionBackend, runService, runServiceReadiness, liveView, liveEditorSubjects, postProcessingRelay } = opts;
|
|
12472
12104
|
const runServiceAttach = opts.runServiceAttach ?? runService?.attach;
|
|
12473
12105
|
const host = opts.host?.trim() || DEFAULT_DASHBOARD_HOST;
|
|
12474
|
-
if (!(0,
|
|
12475
|
-
throw new DashboardRemoteBindRefusedError(host);
|
|
12106
|
+
if (!(0, import_agent_qa_core16.isLoopbackHost)(host)) {
|
|
12107
|
+
throw new DashboardRemoteBindRefusedError(host, port);
|
|
12476
12108
|
}
|
|
12477
12109
|
const resolvedConfigPath = configPath ?? (0, import_node_path12.resolve)("agent-qa.config.yaml");
|
|
12478
12110
|
const configManager = new ConfigManager(resolvedConfigPath, opts.declaredSecretMask);
|
|
@@ -12507,7 +12139,7 @@ async function startServer(opts) {
|
|
|
12507
12139
|
failureSummary = "Test timed out -- process was killed";
|
|
12508
12140
|
} else if (details.refusedBeforeStart) {
|
|
12509
12141
|
dbStatus = "failed";
|
|
12510
|
-
refusalProse = details.
|
|
12142
|
+
refusalProse = details.reason;
|
|
12511
12143
|
failureSummary = refusalProse ? `Refused before the run started: ${refusalProse}`.slice(0, 300) : "Refused before the run started";
|
|
12512
12144
|
} else if (status === "completed") {
|
|
12513
12145
|
dbStatus = "failed";
|
|
@@ -12588,7 +12220,10 @@ async function startServer(opts) {
|
|
|
12588
12220
|
mobileRuns.set(run.id, platform);
|
|
12589
12221
|
} catch (err) {
|
|
12590
12222
|
const message = err instanceof Error ? err.message : String(err);
|
|
12591
|
-
|
|
12223
|
+
(0, import_agent_qa_core16.printRunText)(
|
|
12224
|
+
`Failed to start Appium for run ${run.id}: ${message}`,
|
|
12225
|
+
(text) => console.error(text)
|
|
12226
|
+
);
|
|
12592
12227
|
db.updateRun(run.id, {
|
|
12593
12228
|
status: "failed",
|
|
12594
12229
|
failureSummary: `Appium server failed to start: ${message}`,
|
|
@@ -12608,19 +12243,9 @@ async function startServer(opts) {
|
|
|
12608
12243
|
runId: run.id,
|
|
12609
12244
|
args,
|
|
12610
12245
|
source: meta.isSuite ? "suite" : "dashboard",
|
|
12611
|
-
// 2026-10-01 (432-14, D-14): no confirm scope is read off the row any more. The browser that
|
|
12612
|
-
// triggered a run is not asked anything, so nothing about it has to survive the wait.
|
|
12613
12246
|
attributes: run.attributes,
|
|
12614
|
-
//
|
|
12615
|
-
// `in-process-backend.ts:211` reads `opts.timeout ?? defaultTimeout` and validates AFTER, so
|
|
12616
|
-
// any non-nullish nonsense here skips the default and leaves the run with no reclamation
|
|
12617
|
-
// timer at all. An unusable value falls back to the backend's default rather than a number
|
|
12618
|
-
// this call invented.
|
|
12247
|
+
// The backend takes `opts.timeout ?? defaultTimeout`, so an unusable value falls back here.
|
|
12619
12248
|
timeout: sanitizeQueuedExecutionTimeout(meta.timeout),
|
|
12620
|
-
// THE LEASE ABOVE IS UNCHANGED; ONLY ITS DELIVERY IS. `acquireLease` still runs where it ran,
|
|
12621
|
-
// still keys on this run id and still owns the bundled Appium's lifecycle (design-of-record).
|
|
12622
|
-
// What used to ride `AGENT_QA_APPIUM_URL` in a child's environment now travels as an
|
|
12623
|
-
// argument, because there is no child to carry an environment.
|
|
12624
12249
|
...isMobile ? { appiumUrl: appiumManager.getUrl() } : {}
|
|
12625
12250
|
});
|
|
12626
12251
|
});
|
|
@@ -12631,16 +12256,14 @@ async function startServer(opts) {
|
|
|
12631
12256
|
appiumManager,
|
|
12632
12257
|
configManager,
|
|
12633
12258
|
configPath: resolvedConfigPath,
|
|
12634
|
-
//
|
|
12259
|
+
// Each created session registers here, which is what the frame socket attaches.
|
|
12635
12260
|
...liveEditorSubjects === void 0 ? {} : { liveEditorSubjects }
|
|
12636
12261
|
});
|
|
12637
12262
|
const servesLiveView = liveView !== void 0 && (runServiceAttach !== void 0 || liveEditorSubjects !== void 0);
|
|
12638
12263
|
const apiRouter = createRouter({
|
|
12639
12264
|
db,
|
|
12640
12265
|
workspacePaths,
|
|
12641
|
-
//
|
|
12642
|
-
// dependency is an ABSENT field, never a present `undefined`, so the router's own 503 arm is
|
|
12643
|
-
// what decides, in one place.
|
|
12266
|
+
// Absent dependencies are absent fields, so the router's own 503 arm decides in one place.
|
|
12644
12267
|
...executionBackend === void 0 ? {} : { executionBackend },
|
|
12645
12268
|
jobQueue,
|
|
12646
12269
|
testFileManager,
|
|
@@ -12651,27 +12274,13 @@ async function startServer(opts) {
|
|
|
12651
12274
|
llmConfig,
|
|
12652
12275
|
authFetch,
|
|
12653
12276
|
sessionManager,
|
|
12654
|
-
//
|
|
12655
|
-
// never a present `undefined` — the router's own 501 arm is what decides, in one place.
|
|
12277
|
+
// Absent dependencies are absent fields, so the router's own 501 arm decides in one place.
|
|
12656
12278
|
...agentHost === void 0 ? {} : { agentHost },
|
|
12657
|
-
// 402-05 (D-15): the same conditional-spread house rule. Absent means a legacy mount rather
|
|
12658
|
-
// than an unconfigured service, and the router's own 503 arm is what decides, in one place.
|
|
12659
12279
|
...runServiceReadiness === void 0 ? {} : { runServiceReadiness },
|
|
12660
|
-
//
|
|
12661
|
-
// composed again. That derivation is the "one session registry serves both doors" claim in its
|
|
12662
|
-
// structural form — there is no second wiring a composition root could forget or point
|
|
12663
|
-
// somewhere else, and the two doors cannot end up attached to different registries. Absent
|
|
12664
|
-
// `runService` means no registry exists at all, so the same conditional-spread house rule
|
|
12665
|
-
// applies and the router's own 503 arm decides.
|
|
12280
|
+
// Read off `runService` by default, so both doors attach to one session registry.
|
|
12666
12281
|
...runServiceAttach === void 0 ? {} : { runServiceAttach },
|
|
12667
|
-
// 403 D-03: the mint route's port, on the same conditional-spread house rule — absent means
|
|
12668
|
-
// this host serves no live view and the route's own 503 arm decides, in one place.
|
|
12669
12282
|
...liveView === void 0 ? {} : { mintLiveUrl: liveView.mintLiveUrl },
|
|
12670
|
-
// 428 D-07: the editor mint, on the same house rule — absent means the editor mint route's own
|
|
12671
|
-
// 503 decides.
|
|
12672
12283
|
...liveView?.mintEditorLiveUrl === void 0 ? {} : { mintEditorLiveUrl: liveView.mintEditorLiveUrl },
|
|
12673
|
-
// 404-05 (D-16): the same conditional-spread house rule — absent means this host serves no
|
|
12674
|
-
// post-processing push channel and the stream arm's own 503 decides, in one place.
|
|
12675
12284
|
...postProcessingRelay === void 0 ? {} : { postProcessingRelay }
|
|
12676
12285
|
});
|
|
12677
12286
|
const runServiceRouter = runService === void 0 ? void 0 : createRunServiceRoutes(runService);
|
|
@@ -12691,13 +12300,15 @@ async function startServer(opts) {
|
|
|
12691
12300
|
apiRouter(req, res);
|
|
12692
12301
|
return;
|
|
12693
12302
|
}
|
|
12303
|
+
if (refuseNonLoopbackHost(req) !== void 0) {
|
|
12304
|
+
const address = server.address();
|
|
12305
|
+
const boundPort = typeof address === "object" && address !== null ? address.port : port;
|
|
12306
|
+
refuseForeignHostPage(res, boundPort);
|
|
12307
|
+
return;
|
|
12308
|
+
}
|
|
12694
12309
|
if (req.method === "OPTIONS") {
|
|
12695
|
-
res.writeHead(
|
|
12696
|
-
|
|
12697
|
-
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
12698
|
-
"Access-Control-Allow-Headers": "Content-Type"
|
|
12699
|
-
});
|
|
12700
|
-
res.end();
|
|
12310
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
12311
|
+
res.end("Not Found");
|
|
12701
12312
|
return;
|
|
12702
12313
|
}
|
|
12703
12314
|
if (req.method === "GET" && path.startsWith(LIVE_EMBED_PATH_PREFIX)) {
|
|
@@ -12741,8 +12352,7 @@ async function startServer(opts) {
|
|
|
12741
12352
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
12742
12353
|
res.writeHead(200, {
|
|
12743
12354
|
"Content-Type": contentType,
|
|
12744
|
-
"Content-Length": content.length
|
|
12745
|
-
"Access-Control-Allow-Origin": "*"
|
|
12355
|
+
"Content-Length": content.length
|
|
12746
12356
|
});
|
|
12747
12357
|
res.end(content);
|
|
12748
12358
|
return;
|
|
@@ -12754,8 +12364,7 @@ async function startServer(opts) {
|
|
|
12754
12364
|
const content = await (0, import_promises9.readFile)(indexPath);
|
|
12755
12365
|
res.writeHead(200, {
|
|
12756
12366
|
"Content-Type": "text/html",
|
|
12757
|
-
"Content-Length": content.length
|
|
12758
|
-
"Access-Control-Allow-Origin": "*"
|
|
12367
|
+
"Content-Length": content.length
|
|
12759
12368
|
});
|
|
12760
12369
|
res.end(content);
|
|
12761
12370
|
return;
|
|
@@ -12840,7 +12449,8 @@ async function startServer(opts) {
|
|
|
12840
12449
|
session.clearIdleTimer();
|
|
12841
12450
|
const handler = createSessionHandler(session, () => {
|
|
12842
12451
|
void sessionManager.terminateSession(sessionId).catch((error) => {
|
|
12843
|
-
|
|
12452
|
+
const line = `[live-editor] could not end session ${sessionId}`;
|
|
12453
|
+
(0, import_agent_qa_core16.printRunText)((0, import_node_util5.format)(line, error), () => console.error(line, error));
|
|
12844
12454
|
});
|
|
12845
12455
|
}, (event) => {
|
|
12846
12456
|
db.insertTokenEvent(event);
|
|
@@ -12873,7 +12483,7 @@ async function startServer(opts) {
|
|
|
12873
12483
|
server.listen(port, host, () => {
|
|
12874
12484
|
server.off("error", onError);
|
|
12875
12485
|
console.log(
|
|
12876
|
-
(0,
|
|
12486
|
+
(0, import_agent_qa_core16.isLoopbackHost)(host) ? `Dashboard running at http://localhost:${port}` : `Dashboard running at http://${host.includes(":") ? `[${host}]` : host}:${port}`
|
|
12877
12487
|
);
|
|
12878
12488
|
resolve7({
|
|
12879
12489
|
server,
|
|
@@ -12930,9 +12540,6 @@ var InProcessExecutionBackend = class extends import_node_events2.EventEmitter {
|
|
|
12930
12540
|
this.options = options;
|
|
12931
12541
|
this.retentionMs = options.retentionMs ?? DEFAULT_HANDLE_RETENTION_MS;
|
|
12932
12542
|
}
|
|
12933
|
-
async preflight(opts) {
|
|
12934
|
-
await this.options.preflightRun?.(opts);
|
|
12935
|
-
}
|
|
12936
12543
|
execute(opts) {
|
|
12937
12544
|
const handle = {
|
|
12938
12545
|
runId: opts.runId,
|
|
@@ -13008,9 +12615,7 @@ var InProcessExecutionBackend = class extends import_node_events2.EventEmitter {
|
|
|
13008
12615
|
settle(handle, error) {
|
|
13009
12616
|
if (handle.timeoutTimer) clearTimeout(handle.timeoutTimer);
|
|
13010
12617
|
let refusedBeforeStart = false;
|
|
13011
|
-
let neutralReason;
|
|
13012
12618
|
if (error !== void 0) {
|
|
13013
|
-
neutralReason = dashboardNeutralReasonOf(error);
|
|
13014
12619
|
const message = error instanceof Error ? error.message : String(error);
|
|
13015
12620
|
for (const line of message.split("\n")) handle.output.push(line);
|
|
13016
12621
|
refusedBeforeStart = handle.session === void 0;
|
|
@@ -13024,7 +12629,6 @@ var InProcessExecutionBackend = class extends import_node_events2.EventEmitter {
|
|
|
13024
12629
|
const details = {
|
|
13025
12630
|
...handle.completionReason === void 0 ? {} : { reason: handle.completionReason },
|
|
13026
12631
|
...refusedBeforeStart ? { refusedBeforeStart: true } : {},
|
|
13027
|
-
...neutralReason === void 0 ? {} : { neutralReason },
|
|
13028
12632
|
output: [...handle.output]
|
|
13029
12633
|
};
|
|
13030
12634
|
this.emit("run-settled", handle.runId, handle.status, details);
|
|
@@ -13034,12 +12638,6 @@ var InProcessExecutionBackend = class extends import_node_events2.EventEmitter {
|
|
|
13034
12638
|
cleanup.unref();
|
|
13035
12639
|
}
|
|
13036
12640
|
};
|
|
13037
|
-
function dashboardNeutralReasonOf(error) {
|
|
13038
|
-
if (typeof error !== "object" || error === null) return void 0;
|
|
13039
|
-
const carried = error.neutralReason;
|
|
13040
|
-
if (typeof carried !== "string" || carried.trim().length === 0) return void 0;
|
|
13041
|
-
return carried;
|
|
13042
|
-
}
|
|
13043
12641
|
function createInProcessExecutionBackend(options) {
|
|
13044
12642
|
return new InProcessExecutionBackend(options);
|
|
13045
12643
|
}
|
|
@@ -13175,7 +12773,6 @@ function createLiveEditorSubjects() {
|
|
|
13175
12773
|
renderLiveEmbedPage,
|
|
13176
12774
|
resolveDashboardDbPath,
|
|
13177
12775
|
resolveRunOwnerHost,
|
|
13178
|
-
runDenialOf,
|
|
13179
12776
|
runRowIdentity,
|
|
13180
12777
|
serializeConfigWrite,
|
|
13181
12778
|
servePostProcessingRead,
|