@vostride/agent-qa-dashboard 0.2.3 → 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/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.js
CHANGED
|
@@ -24,7 +24,6 @@ var MIGRATIONS = [
|
|
|
24
24
|
llm_provider TEXT,
|
|
25
25
|
parallel INTEGER DEFAULT 0,
|
|
26
26
|
error_log TEXT,
|
|
27
|
-
memory_log TEXT,
|
|
28
27
|
test_id TEXT,
|
|
29
28
|
suite_id TEXT,
|
|
30
29
|
owner_pid INTEGER,
|
|
@@ -47,9 +46,7 @@ var MIGRATIONS = [
|
|
|
47
46
|
variables TEXT,
|
|
48
47
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
49
48
|
hook_id TEXT,
|
|
50
|
-
--
|
|
51
|
-
-- optional testIndex/stepIndex; NULL means a run-scoped hook, which is a real state and
|
|
52
|
-
-- not a missing value.
|
|
49
|
+
-- Where in the run tree this hook ran; NULL means a run-scoped hook, not a missing value.
|
|
53
50
|
test_index INTEGER,
|
|
54
51
|
step_index INTEGER
|
|
55
52
|
)`,
|
|
@@ -63,10 +60,7 @@ var MIGRATIONS = [
|
|
|
63
60
|
)`,
|
|
64
61
|
`CREATE TABLE IF NOT EXISTS run_artifacts (
|
|
65
62
|
run_id TEXT PRIMARY KEY REFERENCES runs(id) ON DELETE CASCADE,
|
|
66
|
-
--
|
|
67
|
-
-- The set used to admit two more, named for the parent/child run tree 402-09 retired; no
|
|
68
|
-
-- writer has produced either since, so admitting them was the schema agreeing to store a
|
|
69
|
-
-- distinction the product no longer makes.
|
|
63
|
+
-- Two kinds, because no writer mints any other.
|
|
70
64
|
kind TEXT NOT NULL CHECK(kind IN ('test', 'unknown')),
|
|
71
65
|
schema_version INTEGER NOT NULL,
|
|
72
66
|
payload TEXT NOT NULL DEFAULT '{}',
|
|
@@ -74,17 +68,7 @@ var MIGRATIONS = [
|
|
|
74
68
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
75
69
|
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
76
70
|
)`,
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
// THE COLUMNS ARE THE PAYLOAD'S OWN NAMES, snake_cased and nothing else. No field is
|
|
80
|
-
// re-derived, renamed or re-interpreted on the way in — that is D-01's whole claim, and
|
|
81
|
-
// it is what makes the read a mirror rather than a second opinion. Nested payload objects
|
|
82
|
-
// (`planned_action`, `grounding`, `result`, `artifact_refs`) are `JSON.stringify` of the
|
|
83
|
-
// field VERBATIM; the projection never reaches inside one.
|
|
84
|
-
//
|
|
85
|
-
// `test_index` IS THE HANDLE (RESEARCH D4). `handleId(runId, testIndex)` is how this
|
|
86
|
-
// product spells run-scoped member identity, so minting a second id here would be a
|
|
87
|
-
// second spelling of the same fact.
|
|
71
|
+
// Columns are the payload's names snake_cased; `test_index` is the handle, so no second id.
|
|
88
72
|
`CREATE TABLE IF NOT EXISTS run_members (
|
|
89
73
|
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
90
74
|
test_index INTEGER NOT NULL,
|
|
@@ -99,15 +83,12 @@ var MIGRATIONS = [
|
|
|
99
83
|
duration_ms INTEGER,
|
|
100
84
|
failure_summary TEXT,
|
|
101
85
|
retry_count INTEGER,
|
|
102
|
-
--
|
|
86
|
+
-- Starts at 1 and rises on testRetryScheduled; never reset by an upsert.
|
|
103
87
|
attempts INTEGER NOT NULL DEFAULT 1,
|
|
104
88
|
recording_json TEXT,
|
|
105
89
|
PRIMARY KEY (run_id, test_index)
|
|
106
90
|
)`,
|
|
107
|
-
//
|
|
108
|
-
// declared these `INTEGER DEFAULT 0`, and that zero was a measurement nobody made — the exact
|
|
109
|
-
// lie this shape refuses to be able to tell. 405-04 landed the producer, so a step the runner
|
|
110
|
-
// measured carries real counts and a step it did not carries NULL, which stays honest.
|
|
91
|
+
// Token columns are nullable with no default, so a step nobody measured carries NULL, not 0.
|
|
111
92
|
`CREATE TABLE IF NOT EXISTS run_steps (
|
|
112
93
|
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
113
94
|
test_index INTEGER NOT NULL,
|
|
@@ -638,9 +619,7 @@ var DashboardDatabase = class {
|
|
|
638
619
|
if (owner) this.ownStamps.set(id, owner);
|
|
639
620
|
return id;
|
|
640
621
|
}
|
|
641
|
-
//
|
|
642
|
-
// running row; any other row is refused and left untouched. An adopt that names no name or
|
|
643
|
-
// attributes keeps the row's own (443-04).
|
|
622
|
+
// Inserts a fresh row or adopts this handle's own running row; any other row is left untouched.
|
|
644
623
|
claimRunningRow(id, fields) {
|
|
645
624
|
const stamp = this.ownStamps.get(id) ?? this.readOwnStamp();
|
|
646
625
|
const result = this.db.prepare(`
|
|
@@ -709,10 +688,6 @@ var DashboardDatabase = class {
|
|
|
709
688
|
fields.push("error_log = ?");
|
|
710
689
|
values.push(updates.errorLog);
|
|
711
690
|
}
|
|
712
|
-
if (updates.memoryLog !== void 0) {
|
|
713
|
-
fields.push("memory_log = ?");
|
|
714
|
-
values.push(updates.memoryLog);
|
|
715
|
-
}
|
|
716
691
|
if (updates.attributes !== void 0) {
|
|
717
692
|
fields.push("attributes = ?");
|
|
718
693
|
values.push(JSON.stringify(updates.attributes));
|
|
@@ -815,26 +790,8 @@ var DashboardDatabase = class {
|
|
|
815
790
|
this.ownStamps.set(id, stamp);
|
|
816
791
|
return true;
|
|
817
792
|
}
|
|
818
|
-
//
|
|
819
|
-
//
|
|
820
|
-
// SIX MEMBERS, AND EVERY ONE OF THEM MIRRORS. The columns are the bus payload's own names; the
|
|
821
|
-
// nested objects are `JSON.stringify` of the field verbatim. Nothing in this block decides what
|
|
822
|
-
// a status means, re-derives a duration or fills a token count — a projection that interpreted
|
|
823
|
-
// would be a second opinion about a run the event log already settled.
|
|
824
|
-
/**
|
|
825
|
-
* Write or refresh ONE member row, keyed by the handle `(run_id, test_index)`.
|
|
826
|
-
*
|
|
827
|
-
* AN UPSERT AND NOT TWO WRITES, because the member row is fed by two events at two times:
|
|
828
|
-
* `testStarted` knows the identity fields and `testFinished` knows the outcome — and D-18
|
|
829
|
-
* requires a member whose stream begins with a terminal event to render anyway, so the second
|
|
830
|
-
* one alone has to be able to create the row.
|
|
831
|
-
*
|
|
832
|
-
* ONLY SUPPLIED FIELDS MOVE. `COALESCE(excluded.x, run_members.x)` on every optional column, so
|
|
833
|
-
* a `testFinished` that carries no name cannot blank the name `testStarted` wrote. `attempts` is
|
|
834
|
-
* excluded from the update entirely: it is the D-17 retry ordinal, owned by
|
|
835
|
-
* `incrementRunMemberAttempts`, and an upsert that reset it would erase the fact the header
|
|
836
|
-
* renders.
|
|
837
|
-
*/
|
|
793
|
+
// The projection mirrors bus payload names verbatim and interprets nothing.
|
|
794
|
+
// An upsert because a member's first event may be terminal; `COALESCE` keeps unsupplied fields.
|
|
838
795
|
upsertRunMember(member) {
|
|
839
796
|
this.db.prepare(`
|
|
840
797
|
INSERT INTO run_members (
|
|
@@ -872,26 +829,13 @@ var DashboardDatabase = class {
|
|
|
872
829
|
member.recording === void 0 ? null : JSON.stringify(member.recording)
|
|
873
830
|
);
|
|
874
831
|
}
|
|
875
|
-
|
|
876
|
-
* Raise ONE member's attempt ordinal (D-17). A no-op when the member row does not exist yet —
|
|
877
|
-
* a retry scheduled before any identity event is a real ordering, and inventing a row here
|
|
878
|
-
* would put a member on the page that nothing has said anything about.
|
|
879
|
-
*/
|
|
832
|
+
// A no-op without a member row, so a retry before any identity event adds no member.
|
|
880
833
|
incrementRunMemberAttempts(runId, testIndex) {
|
|
881
834
|
this.db.prepare(
|
|
882
835
|
"UPDATE run_members SET attempts = attempts + 1 WHERE run_id = ? AND test_index = ?"
|
|
883
836
|
).run(runId, testIndex);
|
|
884
837
|
}
|
|
885
|
-
|
|
886
|
-
* Drop ONE HANDLE's projected step and sub-action rows (D-17).
|
|
887
|
-
*
|
|
888
|
-
* SCOPED TO THE HANDLE, which was the whole change from the v1 run-wide step delete this
|
|
889
|
-
* replaced (deleted outright by 405-07's census): that member wiped every step of the RUN on any
|
|
890
|
-
* member's retry, so in a suite the abandoned attempt of test 3 deleted the completed history of
|
|
891
|
-
* tests 0-2. The member row itself survives — its outcome and its attempt count are facts about
|
|
892
|
-
* the member, not about the attempt. So do its setup and teardown hook rows, which run once per
|
|
893
|
-
* member outside the retry loop; only the attempt's `inline` hook rows go (433-41, CR-01).
|
|
894
|
-
*/
|
|
838
|
+
// Scoped to the handle: the member row and its setup and teardown hooks survive a retry.
|
|
895
839
|
deleteMemberProjection(runId, testIndex) {
|
|
896
840
|
this.db.prepare("DELETE FROM run_steps WHERE run_id = ? AND test_index = ?").run(runId, testIndex);
|
|
897
841
|
this.db.prepare("DELETE FROM run_sub_actions WHERE run_id = ? AND test_index = ?").run(runId, testIndex);
|
|
@@ -899,12 +843,7 @@ var DashboardDatabase = class {
|
|
|
899
843
|
"DELETE FROM execution_logs WHERE run_id = ? AND test_index = ? AND phase = 'inline'"
|
|
900
844
|
).run(runId, testIndex);
|
|
901
845
|
}
|
|
902
|
-
|
|
903
|
-
* Write ONE finalized step row. `INSERT OR REPLACE` because a re-run of the same handle re-emits
|
|
904
|
-
* the same `(test_index, step_index)` and the LAST attempt is the one the page renders (D-17).
|
|
905
|
-
*
|
|
906
|
-
* The three token columns stay NULL when `tokenUsage` is absent — never 0 (D-05).
|
|
907
|
-
*/
|
|
846
|
+
// `INSERT OR REPLACE`, so a re-run's last attempt wins; tokens stay NULL when absent, never 0.
|
|
908
847
|
insertRunStep(step) {
|
|
909
848
|
this.db.prepare(`
|
|
910
849
|
INSERT OR REPLACE INTO run_steps (
|
|
@@ -964,14 +903,7 @@ var DashboardDatabase = class {
|
|
|
964
903
|
subAction.recordingOffsetMs ?? null
|
|
965
904
|
);
|
|
966
905
|
}
|
|
967
|
-
|
|
968
|
-
* THE run-detail read (D-03). `undefined` when no `runs` row exists — absence is a RESULT here,
|
|
969
|
-
* and turning it into a typed refusal is the `runs.detail` op's job, so the "not found"
|
|
970
|
-
* vocabulary lives in the function layer rather than in this store.
|
|
971
|
-
*
|
|
972
|
-
* A run that exists and projected nothing answers three empty arrays, which is a DIFFERENT fact
|
|
973
|
-
* and stays a different answer.
|
|
974
|
-
*/
|
|
906
|
+
// `undefined` when no run row exists; a run that projected nothing answers three empty arrays.
|
|
975
907
|
readRunDetailRows(runId) {
|
|
976
908
|
const run = this.db.prepare("SELECT id FROM runs WHERE id = ?").get(runId);
|
|
977
909
|
if (run === void 0) return void 0;
|
|
@@ -1111,7 +1043,7 @@ var DashboardDatabase = class {
|
|
|
1111
1043
|
offset: opts.offset
|
|
1112
1044
|
}));
|
|
1113
1045
|
}
|
|
1114
|
-
//
|
|
1046
|
+
// Members are read for the returned rows only, after pagination.
|
|
1115
1047
|
withMemberSummaries(runs) {
|
|
1116
1048
|
for (let offset = 0; offset < runs.length; offset += 400) {
|
|
1117
1049
|
const batch = runs.slice(offset, offset + 400);
|
|
@@ -1154,7 +1086,7 @@ var DashboardDatabase = class {
|
|
|
1154
1086
|
owner: { pid: row.owner_pid, incarnation: row.owner_incarnation, host: row.owner_host ?? "" }
|
|
1155
1087
|
}));
|
|
1156
1088
|
}
|
|
1157
|
-
//
|
|
1089
|
+
// One compare-and-set on the stamped owner; never read-then-write.
|
|
1158
1090
|
finalizeOwnRun(id, updates) {
|
|
1159
1091
|
const fields = ["status = ?", "ended_at = ?"];
|
|
1160
1092
|
const values = [updates.status, updates.endedAt];
|
|
@@ -1176,8 +1108,7 @@ var DashboardDatabase = class {
|
|
|
1176
1108
|
this.ownStamps.delete(id);
|
|
1177
1109
|
return true;
|
|
1178
1110
|
}
|
|
1179
|
-
//
|
|
1180
|
-
// moved the row off running; the owner columns alone decide whose row it is.
|
|
1111
|
+
// No status condition, as the verdict may have moved the row off running; owner columns decide.
|
|
1181
1112
|
closeOwnRun(id, updates) {
|
|
1182
1113
|
const stamp = this.ownStamps.get(id) ?? this.readOwnStamp();
|
|
1183
1114
|
const result = this.db.prepare(`
|
|
@@ -1188,8 +1119,7 @@ var DashboardDatabase = class {
|
|
|
1188
1119
|
this.ownStamps.delete(id);
|
|
1189
1120
|
return true;
|
|
1190
1121
|
}
|
|
1191
|
-
//
|
|
1192
|
-
// this handle's own write, never an inference about another process.
|
|
1122
|
+
// Own is the stamp this handle wrote, so a NULL start time here is this handle's own write.
|
|
1193
1123
|
isOwnStamp(id, owner) {
|
|
1194
1124
|
const stamp = this.ownStamps.get(id);
|
|
1195
1125
|
return stamp !== void 0 && stamp.pid === owner.pid && stamp.incarnation === owner.incarnation && stamp.host === owner.host;
|
|
@@ -1288,20 +1218,7 @@ var DashboardDatabase = class {
|
|
|
1288
1218
|
`).all(...params);
|
|
1289
1219
|
return rows.map((row) => ({ value: row.value, count: row.count }));
|
|
1290
1220
|
}
|
|
1291
|
-
|
|
1292
|
-
* Delete ONE run and everything the schema hangs off it (405-07, D-02/D-14 · T-405-27).
|
|
1293
|
-
*
|
|
1294
|
-
* ONE ID, AND THE CASCADE DOES THE REST. This used to open with a recursive `run_tree` CTE that
|
|
1295
|
-
* walked the child rows of a suite parent, then hand-delete their step and trace rows in a
|
|
1296
|
-
* transaction. Nothing has written a child row since 402-09 — one `runs` row per run, member
|
|
1297
|
-
* identity on `run_members` — so the walk could only ever return the one row it was seeded with,
|
|
1298
|
-
* and the hand-deletes named tables this schema no longer has. Every table that references a run
|
|
1299
|
-
* declares `ON DELETE CASCADE`, so the single statement below is the whole deletion.
|
|
1300
|
-
*
|
|
1301
|
-
* AND IT TOUCHES NO FILE. The two path arrays it used to collect fed the router's artifact
|
|
1302
|
-
* cleanup; both are gone. The run's directory under `.agent-qa/runs/` — its event log and its
|
|
1303
|
-
* content-addressed blobs — is the run's own record and is not the dashboard's to remove.
|
|
1304
|
-
*/
|
|
1221
|
+
// Every table referencing a run cascades on delete, and no file is touched.
|
|
1305
1222
|
deleteRun(id) {
|
|
1306
1223
|
const changes = this.db.prepare("DELETE FROM runs WHERE id = ?").run(id);
|
|
1307
1224
|
const deleted = Number(changes.changes ?? 0) > 0;
|
|
@@ -1388,15 +1305,7 @@ var DashboardDatabase = class {
|
|
|
1388
1305
|
getRunArtifactBundle(runId) {
|
|
1389
1306
|
return { artifact: this.getRunArtifact(runId) };
|
|
1390
1307
|
}
|
|
1391
|
-
|
|
1392
|
-
* The runs a self-heal happened on (405-07, D-02).
|
|
1393
|
-
*
|
|
1394
|
-
* READ OFF `run_steps.self_heal_occurred`, WHICH IS THE SURVIVING HEALING FACT. This scanned the
|
|
1395
|
-
* v1 wide step table for `status = 'healed'`; that table had no producer left and its readers
|
|
1396
|
-
* went with it. `selfHealOccurred` is the bus payload's own boolean, projected verbatim by the
|
|
1397
|
-
* subscriber — so the flake rate below is now computed from what the runner actually reported
|
|
1398
|
-
* rather than from a status word a retired projection wrote.
|
|
1399
|
-
*/
|
|
1308
|
+
// Read off `run_steps.self_heal_occurred`, the bus payload's own boolean.
|
|
1400
1309
|
getHealedRunIds(runIds) {
|
|
1401
1310
|
if (runIds.length === 0) return /* @__PURE__ */ new Set();
|
|
1402
1311
|
const placeholders = runIds.map(() => "?").join(", ");
|
|
@@ -1419,7 +1328,7 @@ var DashboardDatabase = class {
|
|
|
1419
1328
|
passed,
|
|
1420
1329
|
failed,
|
|
1421
1330
|
flaky,
|
|
1422
|
-
//
|
|
1331
|
+
// A group's rate is passed ÷ (passed + failed); neither is no rate.
|
|
1423
1332
|
passRate: passed + failed > 0 ? passed / (passed + failed) : null,
|
|
1424
1333
|
flakeRate: totalRuns > 0 ? flaky / totalRuns : 0,
|
|
1425
1334
|
avgDuration: totalRuns > 0 ? totalDuration / totalRuns : 0
|
|
@@ -1481,28 +1390,6 @@ var DashboardDatabase = class {
|
|
|
1481
1390
|
FROM run_steps
|
|
1482
1391
|
WHERE run_id IN (${runIds.map(() => "?").join(",")})
|
|
1483
1392
|
`).get(...runIds).total_tokens || 0 : 0;
|
|
1484
|
-
const memorySummary = {
|
|
1485
|
-
runs: 0,
|
|
1486
|
-
added: 0,
|
|
1487
|
-
confirmed: 0,
|
|
1488
|
-
deprecated: 0,
|
|
1489
|
-
deleted: 0,
|
|
1490
|
-
curatorTokens: 0
|
|
1491
|
-
};
|
|
1492
|
-
for (const run of runs) {
|
|
1493
|
-
if (!run.memoryLog) continue;
|
|
1494
|
-
memorySummary.runs++;
|
|
1495
|
-
try {
|
|
1496
|
-
const parsed = JSON.parse(run.memoryLog);
|
|
1497
|
-
memorySummary.added += Number(parsed.added ?? 0) || 0;
|
|
1498
|
-
memorySummary.confirmed += Number(parsed.confirmed ?? 0) || 0;
|
|
1499
|
-
memorySummary.deprecated += Number(parsed.deprecated ?? 0) || 0;
|
|
1500
|
-
memorySummary.deleted += Number(parsed.deleted ?? 0) || 0;
|
|
1501
|
-
const tokenUsage = parsed.tokenUsage && typeof parsed.tokenUsage === "object" ? parsed.tokenUsage : {};
|
|
1502
|
-
memorySummary.curatorTokens += Number(tokenUsage.totalTokens ?? 0) || 0;
|
|
1503
|
-
} catch {
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
1393
|
return {
|
|
1507
1394
|
totalRuns: summary.totalRuns,
|
|
1508
1395
|
passed: summary.passed,
|
|
@@ -1511,8 +1398,7 @@ var DashboardDatabase = class {
|
|
|
1511
1398
|
flakeRate: summary.flakeRate,
|
|
1512
1399
|
avgDuration: summary.avgDuration,
|
|
1513
1400
|
totalTokens,
|
|
1514
|
-
runs: daily
|
|
1515
|
-
memory: memorySummary
|
|
1401
|
+
runs: daily
|
|
1516
1402
|
};
|
|
1517
1403
|
}
|
|
1518
1404
|
getCostStats(opts = {}) {
|
|
@@ -1687,12 +1573,7 @@ var DashboardDatabase = class {
|
|
|
1687
1573
|
testRunUnmeasuredSteps: unmeasuredSteps
|
|
1688
1574
|
};
|
|
1689
1575
|
}
|
|
1690
|
-
|
|
1691
|
-
* The window's settled run ids, newest first, capped at `limit`, with their full count, and the
|
|
1692
|
-
* count of the window's runs still in flight (433-30, 433-42). A settled run has started and is
|
|
1693
|
-
* neither `pending` nor `running`; every other row in the window is in flight. Ids only: the
|
|
1694
|
-
* Insights knowledge-change read dispatches each through the agent host.
|
|
1695
|
-
*/
|
|
1576
|
+
// Settled run ids newest first, capped at `limit`, with the full and in-flight counts.
|
|
1696
1577
|
listInsightsWindowRunIds(opts) {
|
|
1697
1578
|
const conditions = [];
|
|
1698
1579
|
const params = [];
|
|
@@ -1848,7 +1729,7 @@ var DashboardDatabase = class {
|
|
|
1848
1729
|
const limit = opts.limit ?? 25;
|
|
1849
1730
|
return rows.slice(0, limit);
|
|
1850
1731
|
}
|
|
1851
|
-
//
|
|
1732
|
+
// Grouped by member file path; only pass- and fail-class outcomes count.
|
|
1852
1733
|
getFlakyTests(opts) {
|
|
1853
1734
|
const minRuns = opts?.minRuns ?? 3;
|
|
1854
1735
|
const limit = opts?.limit ?? 50;
|
|
@@ -1973,7 +1854,6 @@ var DashboardDatabase = class {
|
|
|
1973
1854
|
endedAt: r.ended_at,
|
|
1974
1855
|
failureSummary: r.failure_summary ?? null,
|
|
1975
1856
|
errorLog: r.error_log ?? null,
|
|
1976
|
-
memoryLog: r.memory_log ?? null,
|
|
1977
1857
|
testId: r.test_id ?? null,
|
|
1978
1858
|
suiteId: r.suite_id ?? null,
|
|
1979
1859
|
platform: r.platform ?? "web",
|
|
@@ -2301,11 +2181,7 @@ function createDashboardRunIngest(deps) {
|
|
|
2301
2181
|
...payload.status === void 0 ? {} : { status: payload.status },
|
|
2302
2182
|
...payload.durationMs === void 0 ? {} : { durationMs: payload.durationMs },
|
|
2303
2183
|
...payload.error === void 0 ? {} : { error: payload.error },
|
|
2304
|
-
//
|
|
2305
|
-
// columns 405-01 declared stop being always-NULL. Spread-when-defined, like every other
|
|
2306
|
-
// optional above it — the DB layer writes NULL for all three when the key is absent, and
|
|
2307
|
-
// a NULL row is what makes `runs.detail` able to say "M of K steps unmeasured" instead of
|
|
2308
|
-
// reporting a zero nobody measured.
|
|
2184
|
+
// Spread when defined, so an unmeasured step stays NULL and `runs.detail` counts it so.
|
|
2309
2185
|
...payload.tokenUsage === void 0 ? {} : { tokenUsage: payload.tokenUsage },
|
|
2310
2186
|
...payload.modelUsage === void 0 ? {} : { modelUsage: payload.modelUsage },
|
|
2311
2187
|
...payload.evidence === void 0 ? {} : { evidence: payload.evidence },
|
|
@@ -2355,8 +2231,7 @@ function createDashboardRunIngest(deps) {
|
|
|
2355
2231
|
stdout: readString(payload.stdout) ?? null,
|
|
2356
2232
|
stderr: readString(payload.stderr) ?? null,
|
|
2357
2233
|
variables: payload.variables ?? null,
|
|
2358
|
-
//
|
|
2359
|
-
// is a real state, and inventing handle 0 for it would put a setup hook inside a test.
|
|
2234
|
+
// Absent stays NULL: a run-scoped hook is a real state, not handle 0.
|
|
2360
2235
|
...payload.testIndex === void 0 ? {} : { testIndex: payload.testIndex },
|
|
2361
2236
|
...payload.stepIndex === void 0 ? {} : { stepIndex: payload.stepIndex }
|
|
2362
2237
|
});
|
|
@@ -2451,14 +2326,15 @@ function createDashboardRunIngest(deps) {
|
|
|
2451
2326
|
// src/server/server.ts
|
|
2452
2327
|
import { createServer } from "node:http";
|
|
2453
2328
|
import { randomBytes } from "node:crypto";
|
|
2454
|
-
import { readFile as readFile8, stat as
|
|
2455
|
-
import { join as
|
|
2329
|
+
import { readFile as readFile8, stat as stat6 } from "node:fs/promises";
|
|
2330
|
+
import { join as join4, extname, resolve as resolve6 } from "node:path";
|
|
2456
2331
|
import { cpus as cpus2 } from "node:os";
|
|
2332
|
+
import { format as format3 } from "node:util";
|
|
2457
2333
|
|
|
2458
2334
|
// src/server/routes.ts
|
|
2459
2335
|
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
2460
|
-
import { readFile as readFile6, writeFile as writeFile4
|
|
2461
|
-
import {
|
|
2336
|
+
import { readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
|
|
2337
|
+
import { isAbsolute as isAbsolute2, basename as basename5, resolve as resolve5, dirname as dirname9 } from "node:path";
|
|
2462
2338
|
|
|
2463
2339
|
// src/tests/test-file-manager.ts
|
|
2464
2340
|
import { readFile, mkdir, rm as rm2, stat as stat2 } from "node:fs/promises";
|
|
@@ -2631,7 +2507,7 @@ var TestFileManager = class {
|
|
|
2631
2507
|
constructor(workspace) {
|
|
2632
2508
|
this.workspace = workspace;
|
|
2633
2509
|
}
|
|
2634
|
-
//
|
|
2510
|
+
// Each list call reads the workspace itself; each file is written whole, one writer at a time.
|
|
2635
2511
|
underLock = createPathLock();
|
|
2636
2512
|
async list() {
|
|
2637
2513
|
const records = await discoverWorkspaceFiles({ workspace: this.workspace, kind: "test" });
|
|
@@ -2808,8 +2684,7 @@ var ConfigManager = class _ConfigManager {
|
|
|
2808
2684
|
this.configPath = configPath;
|
|
2809
2685
|
this.mask = mask;
|
|
2810
2686
|
}
|
|
2811
|
-
//
|
|
2812
|
-
// add one. A missing file holds none.
|
|
2687
|
+
// A whole document may keep a marker the file already holds, as often, but never add one.
|
|
2813
2688
|
static addsShownMarker(current, next) {
|
|
2814
2689
|
const held = shownMarkerCounts(current);
|
|
2815
2690
|
for (const [marker, count] of shownMarkerCounts(next)) {
|
|
@@ -2817,13 +2692,13 @@ var ConfigManager = class _ConfigManager {
|
|
|
2817
2692
|
}
|
|
2818
2693
|
return false;
|
|
2819
2694
|
}
|
|
2820
|
-
//
|
|
2695
|
+
// A half-edited file is named, never served as the partial config yaml recovers.
|
|
2821
2696
|
async readWithRevision() {
|
|
2822
2697
|
const read = await this.readParsed();
|
|
2823
2698
|
if (read.kind !== "ok") return read;
|
|
2824
2699
|
return { kind: "ok", config: read.config, raw: read.raw, revision: read.revision };
|
|
2825
2700
|
}
|
|
2826
|
-
//
|
|
2701
|
+
// The payload and the file view pass through the declared mask and nothing else.
|
|
2827
2702
|
async readMasked() {
|
|
2828
2703
|
const read = await this.readParsed();
|
|
2829
2704
|
if (read.kind === "absent") {
|
|
@@ -2867,8 +2742,7 @@ var ConfigManager = class _ConfigManager {
|
|
|
2867
2742
|
revision
|
|
2868
2743
|
};
|
|
2869
2744
|
}
|
|
2870
|
-
//
|
|
2871
|
-
// read the same bytes and drop each other's field.
|
|
2745
|
+
// One transaction per request, serialised so overlapping autosaves never drop each other's field.
|
|
2872
2746
|
async applyChanges(changes, options = {}) {
|
|
2873
2747
|
const marked = changeWithShownMarker(changes);
|
|
2874
2748
|
if (marked !== void 0) {
|
|
@@ -2883,13 +2757,11 @@ var ConfigManager = class _ConfigManager {
|
|
|
2883
2757
|
}
|
|
2884
2758
|
return this.serialized(() => this.applyChangesOnce(changes, options.capturePriors === true));
|
|
2885
2759
|
}
|
|
2886
|
-
//
|
|
2887
|
-
// that no longer holds what the change left there was written since, and is never overwritten.
|
|
2760
|
+
// An Undo restores newest first; a field written after the change is never overwritten.
|
|
2888
2761
|
async restorePriors(priors) {
|
|
2889
2762
|
return this.serialized(() => this.restorePriorsOnce(priors));
|
|
2890
2763
|
}
|
|
2891
|
-
//
|
|
2892
|
-
// bytes it was built from, read again in this path's turn. Anything else is somebody's edit.
|
|
2764
|
+
// A generated whole-file change publishes only over the bytes it is built from, re-read in turn.
|
|
2893
2765
|
async replaceDocument(expectedRevision, content) {
|
|
2894
2766
|
return this.serialized(async () => {
|
|
2895
2767
|
let current;
|
|
@@ -3016,8 +2888,7 @@ var ConfigManager = class _ConfigManager {
|
|
|
3016
2888
|
await this.writeAtomic(next);
|
|
3017
2889
|
return capturePriors ? { kind: "ok", revision: revisionOf(next), baseRevision, priors } : { kind: "ok", revision: revisionOf(next), baseRevision };
|
|
3018
2890
|
}
|
|
3019
|
-
//
|
|
3020
|
-
// (chmod, because writeFile's mode is filtered by the umask), and replaces it in one rename.
|
|
2891
|
+
// The temp file sits beside the real one and is chmodded, as the umask filters `writeFile` modes.
|
|
3021
2892
|
async writeAtomic(content) {
|
|
3022
2893
|
const target = await this.writeTarget();
|
|
3023
2894
|
const directory = dirname5(target);
|
|
@@ -4221,6 +4092,12 @@ var HookRegistryManager = class {
|
|
|
4221
4092
|
|
|
4222
4093
|
// src/server/body-parser.ts
|
|
4223
4094
|
var DEFAULT_MAX_BYTES = 1024 * 1024;
|
|
4095
|
+
function carriesNonJsonBody(req) {
|
|
4096
|
+
const hasBody = req.headers["transfer-encoding"] !== void 0 || Number(req.headers["content-length"] ?? "0") > 0;
|
|
4097
|
+
if (!hasBody) return false;
|
|
4098
|
+
const type = req.headers["content-type"];
|
|
4099
|
+
return type === void 0 || type.split(";", 1)[0].trim().toLowerCase() !== "application/json";
|
|
4100
|
+
}
|
|
4224
4101
|
function readJsonBody(req, maxBytes = DEFAULT_MAX_BYTES) {
|
|
4225
4102
|
return new Promise((resolve7, reject) => {
|
|
4226
4103
|
const chunks = [];
|
|
@@ -4255,12 +4132,16 @@ function readJsonBody(req, maxBytes = DEFAULT_MAX_BYTES) {
|
|
|
4255
4132
|
// src/live-editor/live-session.ts
|
|
4256
4133
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
4257
4134
|
import { dirname as dirname8, resolve as resolvePath2 } from "node:path";
|
|
4135
|
+
import { format } from "node:util";
|
|
4258
4136
|
import {
|
|
4259
4137
|
AUTH_STATE_SCHEMA_VERSION,
|
|
4260
4138
|
MobileSetupError as MobileSetupError2,
|
|
4261
4139
|
PlannerConfigSchema,
|
|
4262
4140
|
SuiteDefinitionSchema,
|
|
4263
4141
|
TestDefinitionSchema,
|
|
4142
|
+
TimeoutConfigSchema,
|
|
4143
|
+
getAgentQaVersion,
|
|
4144
|
+
printRunText,
|
|
4264
4145
|
readAuthStateMetadata,
|
|
4265
4146
|
resolveAuthStatePaths,
|
|
4266
4147
|
writeAuthStateFiles
|
|
@@ -4289,7 +4170,7 @@ import {
|
|
|
4289
4170
|
resolveVisionGate,
|
|
4290
4171
|
visionFromConfig
|
|
4291
4172
|
} from "@vostride/agent-qa-model";
|
|
4292
|
-
import {
|
|
4173
|
+
import { resolvePlannerLimits } from "@vostride/agent-qa-schema";
|
|
4293
4174
|
import { parse as parseYaml5 } from "yaml";
|
|
4294
4175
|
|
|
4295
4176
|
// src/llm-model-mapping.ts
|
|
@@ -4368,6 +4249,7 @@ import { dirname as dirname7, join as join3 } from "node:path";
|
|
|
4368
4249
|
import { parse as parseYaml4 } from "yaml";
|
|
4369
4250
|
import {
|
|
4370
4251
|
MobileSetupError,
|
|
4252
|
+
refuseRetiredLocalBindings,
|
|
4371
4253
|
resolveMobileRunConfig
|
|
4372
4254
|
} from "@vostride/agent-qa-core";
|
|
4373
4255
|
async function loadLocalBindings(configPath) {
|
|
@@ -4375,12 +4257,10 @@ async function loadLocalBindings(configPath) {
|
|
|
4375
4257
|
try {
|
|
4376
4258
|
const content = await readFile5(filePath, "utf-8");
|
|
4377
4259
|
const parsed = parseYaml4(content);
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
filePath
|
|
4383
|
-
} : { filePath };
|
|
4260
|
+
refuseRetiredLocalBindings(parsed, filePath);
|
|
4261
|
+
if (!parsed || typeof parsed !== "object") return { filePath };
|
|
4262
|
+
const bindings = parsed;
|
|
4263
|
+
return { devices: bindings.devices, apps: bindings.apps, filePath };
|
|
4384
4264
|
} catch (err) {
|
|
4385
4265
|
if (err.code === "ENOENT") return null;
|
|
4386
4266
|
throw err;
|
|
@@ -4470,44 +4350,15 @@ async function prepareMobileLiveSession(input) {
|
|
|
4470
4350
|
appActivity: resolved.app.appActivity,
|
|
4471
4351
|
deepLinkAppId: resolved.app.deepLinkAppId,
|
|
4472
4352
|
appPath: resolved.app.install?.path,
|
|
4473
|
-
browserstackApp: resolved.app.install?.browserstack,
|
|
4474
4353
|
appState: resolved.appState
|
|
4475
4354
|
}
|
|
4476
4355
|
};
|
|
4477
4356
|
}
|
|
4478
4357
|
|
|
4479
4358
|
// src/live-editor/live-session.ts
|
|
4480
|
-
var LiveProductionRefusedError = class extends Error {
|
|
4481
|
-
productionTargetNames;
|
|
4482
|
-
constructor(productionTargetNames) {
|
|
4483
|
-
super(buildProductionRefusal(productionTargetNames));
|
|
4484
|
-
this.name = "LiveProductionRefusedError";
|
|
4485
|
-
this.productionTargetNames = distinctInFirstOccurrenceOrder(productionTargetNames);
|
|
4486
|
-
}
|
|
4487
|
-
};
|
|
4488
4359
|
function isConfigRecord(value) {
|
|
4489
4360
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4490
4361
|
}
|
|
4491
|
-
function liveTargetName(targetName) {
|
|
4492
|
-
if (typeof targetName !== "string") return void 0;
|
|
4493
|
-
const trimmed = targetName.trim();
|
|
4494
|
-
return trimmed.length > 0 ? trimmed : void 0;
|
|
4495
|
-
}
|
|
4496
|
-
async function resolveLiveTargetProduction(configManager, targetName) {
|
|
4497
|
-
const name = liveTargetName(targetName);
|
|
4498
|
-
if (name === void 0 || configManager === void 0) {
|
|
4499
|
-
return { productionTargetNames: [], allowProduction: false };
|
|
4500
|
-
}
|
|
4501
|
-
const config = await readConfigValues(configManager);
|
|
4502
|
-
const registry = isConfigRecord(config.registry) ? config.registry : void 0;
|
|
4503
|
-
const targets = registry !== void 0 && isConfigRecord(registry.targets) ? registry.targets : void 0;
|
|
4504
|
-
const declared = targets !== void 0 && Object.hasOwn(targets, name) ? targets[name] : void 0;
|
|
4505
|
-
const production = isConfigRecord(declared) && declared.environment === "production";
|
|
4506
|
-
return {
|
|
4507
|
-
productionTargetNames: production ? [name] : [],
|
|
4508
|
-
allowProduction: config.allowProduction === true
|
|
4509
|
-
};
|
|
4510
|
-
}
|
|
4511
4362
|
async function readEntityUse(entity, readers) {
|
|
4512
4363
|
if (entity === void 0) return void 0;
|
|
4513
4364
|
const find = entity.type === "suite" ? readers.findSuite : readers.findTest;
|
|
@@ -4527,6 +4378,12 @@ var LivePlannerLimitsRefusedError = class extends Error {
|
|
|
4527
4378
|
this.name = "LivePlannerLimitsRefusedError";
|
|
4528
4379
|
}
|
|
4529
4380
|
};
|
|
4381
|
+
var LiveNavigationTimeoutRefusedError = class extends Error {
|
|
4382
|
+
constructor(message) {
|
|
4383
|
+
super(message);
|
|
4384
|
+
this.name = "LiveNavigationTimeoutRefusedError";
|
|
4385
|
+
}
|
|
4386
|
+
};
|
|
4530
4387
|
var LIVE_LIMIT_LEAVES = ["maxActions", "maxPlanningTurns"];
|
|
4531
4388
|
function shownLimitValue(value) {
|
|
4532
4389
|
return typeof value === "number" ? String(value) : JSON.stringify(value) ?? String(value);
|
|
@@ -4575,9 +4432,30 @@ async function resolveLivePlannerLimits(configManager, entities, readers) {
|
|
|
4575
4432
|
}
|
|
4576
4433
|
return resolvePlannerLimits(config.use, ...uses);
|
|
4577
4434
|
}
|
|
4578
|
-
function
|
|
4579
|
-
|
|
4580
|
-
|
|
4435
|
+
async function resolveLiveNavigationTimeoutMs(configManager, entities, readers) {
|
|
4436
|
+
const config = configManager === void 0 ? {} : await readConfigValues(configManager);
|
|
4437
|
+
const layers = [
|
|
4438
|
+
{ layer: "project config", use: config.use }
|
|
4439
|
+
];
|
|
4440
|
+
for (const entity of [].concat(entities ?? [])) {
|
|
4441
|
+
const use = await readEntityUse(entity, readers);
|
|
4442
|
+
layers.push({ layer: `${entity.type} file ${entity.id}`, use });
|
|
4443
|
+
}
|
|
4444
|
+
let navigation;
|
|
4445
|
+
for (const { layer, use } of layers) {
|
|
4446
|
+
if (!isConfigRecord(use) || !isConfigRecord(use.timeout)) continue;
|
|
4447
|
+
if (!Object.hasOwn(use.timeout, "navigation")) continue;
|
|
4448
|
+
const value = use.timeout.navigation;
|
|
4449
|
+
const parsed = TimeoutConfigSchema.shape.navigation.safeParse(value);
|
|
4450
|
+
if (!parsed.success) {
|
|
4451
|
+
const issue = parsed.error.issues[0]?.message ?? "invalid value";
|
|
4452
|
+
throw new LiveNavigationTimeoutRefusedError(
|
|
4453
|
+
`${layer} sets use.timeout.navigation to ${shownLimitValue(value)}: ${issue}. The CLI refuses this file too.`
|
|
4454
|
+
);
|
|
4455
|
+
}
|
|
4456
|
+
navigation = parsed.data;
|
|
4457
|
+
}
|
|
4458
|
+
return navigation;
|
|
4581
4459
|
}
|
|
4582
4460
|
var IDLE_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
4583
4461
|
var LIVE_HUB_CLOSE_LIMIT_MS = 2e3;
|
|
@@ -4713,8 +4591,7 @@ var LiveSession = class {
|
|
|
4713
4591
|
planner = null;
|
|
4714
4592
|
verifier = null;
|
|
4715
4593
|
modelProfiles = null;
|
|
4716
|
-
//
|
|
4717
|
-
// rooted at the session's `.agent-qa` runtime dir (the FsStorageStateStore rooting idiom).
|
|
4594
|
+
// Built lazily on first use, rooted at the session's `.agent-qa` runtime dir.
|
|
4718
4595
|
storageStateStore = null;
|
|
4719
4596
|
model = null;
|
|
4720
4597
|
variableStore = null;
|
|
@@ -4742,15 +4619,14 @@ var LiveSession = class {
|
|
|
4742
4619
|
mobileAppiumLease = null;
|
|
4743
4620
|
secretStore;
|
|
4744
4621
|
secretRedactor;
|
|
4745
|
-
//
|
|
4746
|
-
// setup hooks failed has no adapter and therefore no hub; its subject declares `none`.
|
|
4622
|
+
// One hub per session at handle 0, created once the adapter exists; no adapter means no hub.
|
|
4747
4623
|
liveHub = null;
|
|
4748
4624
|
attachment = null;
|
|
4749
4625
|
resolveEnded = () => {
|
|
4750
4626
|
};
|
|
4751
4627
|
ended;
|
|
4752
4628
|
cleanupRun = null;
|
|
4753
|
-
//
|
|
4629
|
+
// Which control socket is the current one; only its close may detach the session.
|
|
4754
4630
|
attachmentGeneration = 0;
|
|
4755
4631
|
releaseAttachment = null;
|
|
4756
4632
|
get modelName() {
|
|
@@ -4772,11 +4648,7 @@ var LiveSession = class {
|
|
|
4772
4648
|
hub.register(0, registration?.capability, registration?.fallback);
|
|
4773
4649
|
this.liveHub = hub;
|
|
4774
4650
|
}
|
|
4775
|
-
|
|
4776
|
-
* Make a control socket the current one. `release` is how the session lets go of it later: the
|
|
4777
|
-
* previous attachment's is called with `superseded` here (428 WR-01), so one socket drives the
|
|
4778
|
-
* session at a time.
|
|
4779
|
-
*/
|
|
4651
|
+
// The previous attachment's `release` gets `superseded`, so one socket drives the session.
|
|
4780
4652
|
beginAttachment(release) {
|
|
4781
4653
|
const previous = this.releaseAttachment;
|
|
4782
4654
|
this.attachmentGeneration += 1;
|
|
@@ -4795,11 +4667,7 @@ var LiveSession = class {
|
|
|
4795
4667
|
}
|
|
4796
4668
|
this.pendingMessages = [];
|
|
4797
4669
|
}
|
|
4798
|
-
|
|
4799
|
-
* A command's reply, for whichever control socket is attached now (428 CR-01). A command can
|
|
4800
|
-
* outlive the socket that issued it, and its outcome belongs to the page, not to that socket:
|
|
4801
|
-
* with none attached it is held and flushed to the next one.
|
|
4802
|
-
*/
|
|
4670
|
+
// A reply can outlive its socket, so it goes to the current socket or waits for the next one.
|
|
4803
4671
|
deliver(message) {
|
|
4804
4672
|
if (this.messageSink) {
|
|
4805
4673
|
this.messageSink(message);
|
|
@@ -4910,7 +4778,7 @@ var LiveSession = class {
|
|
|
4910
4778
|
continue;
|
|
4911
4779
|
}
|
|
4912
4780
|
const result = await runHooks([hookDef], {
|
|
4913
|
-
executor: new DockerHookExecutor(),
|
|
4781
|
+
executor: new DockerHookExecutor({ version: getAgentQaVersion() }),
|
|
4914
4782
|
env: this.getHookEnvVars(),
|
|
4915
4783
|
secretStore: this.secretStore,
|
|
4916
4784
|
secretRedactor: this.secretRedactor
|
|
@@ -4984,7 +4852,6 @@ var LiveSession = class {
|
|
|
4984
4852
|
device: config.device ? {
|
|
4985
4853
|
name: config.device.name,
|
|
4986
4854
|
platform,
|
|
4987
|
-
transport: "local",
|
|
4988
4855
|
match: config.device
|
|
4989
4856
|
} : void 0
|
|
4990
4857
|
};
|
|
@@ -5049,15 +4916,6 @@ var LiveSession = class {
|
|
|
5049
4916
|
}
|
|
5050
4917
|
}
|
|
5051
4918
|
async initialize(config) {
|
|
5052
|
-
const found = await resolveLiveTargetProduction(this.deps.configManager, config.targetName);
|
|
5053
|
-
const refusal = liveProductionRefusal({
|
|
5054
|
-
productionTargetNames: distinctInFirstOccurrenceOrder([
|
|
5055
|
-
...config.production?.productionTargetNames ?? [],
|
|
5056
|
-
...found.productionTargetNames
|
|
5057
|
-
]),
|
|
5058
|
-
allowProduction: found.allowProduction
|
|
5059
|
-
});
|
|
5060
|
-
if (refusal !== void 0) throw refusal;
|
|
5061
4919
|
this.platform = config.platform;
|
|
5062
4920
|
this.targetName = typeof config.targetName === "string" && config.targetName.trim().length > 0 ? config.targetName.trim() : null;
|
|
5063
4921
|
this._modelName = config.llmConfig.model ?? "unknown";
|
|
@@ -5092,7 +4950,11 @@ var LiveSession = class {
|
|
|
5092
4950
|
} else {
|
|
5093
4951
|
const { WebPlatformAdapter } = await import("@vostride/agent-qa-web");
|
|
5094
4952
|
const adapter = new WebPlatformAdapter();
|
|
5095
|
-
await adapter.setup({
|
|
4953
|
+
await adapter.setup({
|
|
4954
|
+
platform: "web",
|
|
4955
|
+
browser: { name: "chromium", headless: config.headless ?? true },
|
|
4956
|
+
...config.navigationTimeoutMs === void 0 ? {} : { timeouts: { navigation: config.navigationTimeoutMs } }
|
|
4957
|
+
});
|
|
5096
4958
|
this.adapter = adapter;
|
|
5097
4959
|
if (config.url) {
|
|
5098
4960
|
const resolvedUrl = this.resolveSecrets(config.url);
|
|
@@ -5154,7 +5016,7 @@ var LiveSession = class {
|
|
|
5154
5016
|
}
|
|
5155
5017
|
const startedAt = performance.now();
|
|
5156
5018
|
try {
|
|
5157
|
-
const result = await new DockerHookExecutor().execute(hookDef, {
|
|
5019
|
+
const result = await new DockerHookExecutor({ version: getAgentQaVersion() }).execute(hookDef, {
|
|
5158
5020
|
env: this.getHookEnvVars(),
|
|
5159
5021
|
secretEnv: resolveSecretEnv(this.secretStore),
|
|
5160
5022
|
secretRedactor: this.secretRedactor
|
|
@@ -5321,12 +5183,7 @@ var LiveSession = class {
|
|
|
5321
5183
|
};
|
|
5322
5184
|
return this.redact(result);
|
|
5323
5185
|
}
|
|
5324
|
-
//
|
|
5325
|
-
// lifecycle (executeStepCommand for single steps, executeTestCommand for
|
|
5326
|
-
// the per-test loop that runs many steps under one outer gate).
|
|
5327
|
-
// D-14 (Phase 360): lazily construct the run-scoped storage-state store, rooted at the session's
|
|
5328
|
-
// `.agent-qa` runtime dir. All fs writes live in runner-local (validate:arch storageNeutral);
|
|
5329
|
-
// the loop records only { name, fingerprint }. Absent configPath falls back to the cwd root.
|
|
5186
|
+
// Lazily built and rooted at the session's `.agent-qa` dir; fs writes stay in runner-local.
|
|
5330
5187
|
async getStorageStateStore() {
|
|
5331
5188
|
let store = this.storageStateStore;
|
|
5332
5189
|
if (!store) {
|
|
@@ -5465,7 +5322,9 @@ ${errorLines.join("\n")}`;
|
|
|
5465
5322
|
testLimits = this.plannerLimitsForTest !== void 0 && testDraft.testId.length > 0 ? await this.plannerLimitsForTest(testDraft.testId) : this.plannerLimits ?? resolvePlannerLimits();
|
|
5466
5323
|
} catch (error) {
|
|
5467
5324
|
if (!(error instanceof LivePlannerLimitsRefusedError)) throw error;
|
|
5468
|
-
|
|
5325
|
+
const line = `[live-session] executeTestCommand: ${error.message}`;
|
|
5326
|
+
const detail = { testName: testDraft.testName };
|
|
5327
|
+
printRunText(format(line, detail), () => console.warn(line, detail));
|
|
5469
5328
|
return this.redact({
|
|
5470
5329
|
status: "failed",
|
|
5471
5330
|
duration: performance.now() - start,
|
|
@@ -5485,11 +5344,13 @@ ${errorLines.join("\n")}`;
|
|
|
5485
5344
|
if (!setupResult.allPassed) {
|
|
5486
5345
|
const failedHook = setupHookPayloads.find((h) => h.status === "failed");
|
|
5487
5346
|
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";
|
|
5488
|
-
|
|
5347
|
+
const line = `[live-session] executeTestCommand: ${detail}`;
|
|
5348
|
+
const context = {
|
|
5489
5349
|
testName: testDraft.testName,
|
|
5490
5350
|
setupHooks: testDraft.setup,
|
|
5491
5351
|
failedHook: failedHook ? { name: failedHook.hookName, stderr: failedHook.stderr, error: failedHook.error } : null
|
|
5492
|
-
}
|
|
5352
|
+
};
|
|
5353
|
+
printRunText(format(line, context), () => console.warn(line, context));
|
|
5493
5354
|
return {
|
|
5494
5355
|
status: "failed",
|
|
5495
5356
|
duration: performance.now() - start,
|
|
@@ -5567,7 +5428,9 @@ ${errorLines.join("\n")}`;
|
|
|
5567
5428
|
topLevelError = failedHook ? `Teardown hook "${failedHook.hookName}" failed${failedHook.error ? `: ${failedHook.error}` : ""}` : "Teardown hook failed";
|
|
5568
5429
|
}
|
|
5569
5430
|
if (topLevelError) {
|
|
5570
|
-
|
|
5431
|
+
const line = `[live-session] executeTestCommand: ${topLevelError}`;
|
|
5432
|
+
const detail = { testName: testDraft.testName };
|
|
5433
|
+
printRunText(format(line, detail), () => console.warn(line, detail));
|
|
5571
5434
|
}
|
|
5572
5435
|
return this.redact({
|
|
5573
5436
|
status: finalStatus,
|
|
@@ -5583,10 +5446,7 @@ ${errorLines.join("\n")}`;
|
|
|
5583
5446
|
this.status = "idle";
|
|
5584
5447
|
}
|
|
5585
5448
|
}
|
|
5586
|
-
//
|
|
5587
|
-
// hook payloads (hook-complete) for inclusion in the LiveTestResultPayload.
|
|
5588
|
-
// The capture sits beside the message sink rather than wrapping it, so a
|
|
5589
|
-
// control socket that reattaches mid-hook keeps its sink (428 CR-01).
|
|
5449
|
+
// Captures dispatched hook payloads beside the message sink, so a reattached socket keeps it.
|
|
5590
5450
|
async runLifecycleHooksCapturing(phase, hookIds, sink, owner = { scope: "suite" }) {
|
|
5591
5451
|
this.hookCaptures.add(sink);
|
|
5592
5452
|
try {
|
|
@@ -5830,6 +5690,21 @@ function refuseUnparseableRequest(res) {
|
|
|
5830
5690
|
});
|
|
5831
5691
|
res.end(body);
|
|
5832
5692
|
}
|
|
5693
|
+
function refuseForeignHostPage(res, port) {
|
|
5694
|
+
if (res.headersSent) {
|
|
5695
|
+
res.destroy();
|
|
5696
|
+
return;
|
|
5697
|
+
}
|
|
5698
|
+
const p = String(port);
|
|
5699
|
+
const body = `The agent-qa dashboard answers only at localhost, 127.0.0.1 or [::1].
|
|
5700
|
+
Open http://localhost:${p}/ on the machine it runs on, or forward the port with ssh -L ${p}:localhost:${p} <host>.
|
|
5701
|
+
`;
|
|
5702
|
+
res.writeHead(403, {
|
|
5703
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
5704
|
+
"Content-Length": Buffer.byteLength(body)
|
|
5705
|
+
});
|
|
5706
|
+
res.end(body);
|
|
5707
|
+
}
|
|
5833
5708
|
function backstopServerError(res, req, error) {
|
|
5834
5709
|
const kind = error instanceof Error ? error.constructor.name : typeof error;
|
|
5835
5710
|
console.error(
|
|
@@ -5935,10 +5810,6 @@ function respondJson(res, status, data) {
|
|
|
5935
5810
|
res.end(body);
|
|
5936
5811
|
}
|
|
5937
5812
|
function serveRunEvidenceRead(req, res, deps) {
|
|
5938
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req)) {
|
|
5939
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
5940
|
-
return;
|
|
5941
|
-
}
|
|
5942
5813
|
const query = new URL(req.url ?? "/", "http://localhost").searchParams;
|
|
5943
5814
|
const entries = [["runId", deps.runId]];
|
|
5944
5815
|
const seen = /* @__PURE__ */ new Set(["runId"]);
|
|
@@ -5980,10 +5851,6 @@ function respondFeedbackRead(res, deps, functionId, input, parse) {
|
|
|
5980
5851
|
});
|
|
5981
5852
|
}
|
|
5982
5853
|
function serveRunMarksHistoryRead(req, res, deps) {
|
|
5983
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
|
|
5984
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
5985
|
-
return;
|
|
5986
|
-
}
|
|
5987
5854
|
if (new URL(req.url ?? "/", "http://localhost").search !== "") {
|
|
5988
5855
|
respondJson(res, 400, { error: "invalid_marks_input" });
|
|
5989
5856
|
return;
|
|
@@ -6002,10 +5869,6 @@ function serveRunMarksHistoryRead(req, res, deps) {
|
|
|
6002
5869
|
);
|
|
6003
5870
|
}
|
|
6004
5871
|
function serveRunMarkPreviewRead(req, res, deps) {
|
|
6005
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
|
|
6006
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
6007
|
-
return;
|
|
6008
|
-
}
|
|
6009
5872
|
void readJsonBody(req).then((body) => {
|
|
6010
5873
|
const parsed = RunsMarkInputSchema.safeParse(body);
|
|
6011
5874
|
if (!parsed.success) {
|
|
@@ -6028,10 +5891,6 @@ function serveRunMarkPreviewRead(req, res, deps) {
|
|
|
6028
5891
|
});
|
|
6029
5892
|
}
|
|
6030
5893
|
function serveRunDetailRead(req, res, deps) {
|
|
6031
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
|
|
6032
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
6033
|
-
return;
|
|
6034
|
-
}
|
|
6035
5894
|
void readRunDetail(deps.host, deps.runId).then(
|
|
6036
5895
|
(outcome) => {
|
|
6037
5896
|
if (outcome.kind === "refused") {
|
|
@@ -6183,10 +6042,6 @@ async function readRunAnalysis(host, runId) {
|
|
|
6183
6042
|
}
|
|
6184
6043
|
}
|
|
6185
6044
|
function serveRunAnalysisRead(req, res, deps) {
|
|
6186
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
|
|
6187
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
6188
|
-
return;
|
|
6189
|
-
}
|
|
6190
6045
|
void readRunAnalysis(deps.host, deps.runId).then(
|
|
6191
6046
|
(outcome) => {
|
|
6192
6047
|
if (outcome.kind === "refused") {
|
|
@@ -6410,10 +6265,7 @@ function serveRunEventStream(req, res, deps) {
|
|
|
6410
6265
|
else pending.push(frame);
|
|
6411
6266
|
};
|
|
6412
6267
|
const frameOf = (event) => (
|
|
6413
|
-
//
|
|
6414
|
-
// data IS the envelope's own JSON. No projection, no rename, no second vocabulary. The filter
|
|
6415
|
-
// DROPS envelopes; it never edits one, so a filtered stream's frames are byte-identical to the
|
|
6416
|
-
// firehose's — with gaps in the ids, which is what seq-as-id means once a filter exists.
|
|
6268
|
+
// The filter drops envelopes and never edits one, so filtered frames match the firehose bytes.
|
|
6417
6269
|
`id: ${String(event.seq)}
|
|
6418
6270
|
event: ${event.type}
|
|
6419
6271
|
data: ${JSON.stringify(event)}
|
|
@@ -6475,12 +6327,6 @@ data: ${JSON.stringify(event)}
|
|
|
6475
6327
|
deps.openStreams?.add(teardown);
|
|
6476
6328
|
req.on("close", teardown);
|
|
6477
6329
|
}
|
|
6478
|
-
function runDenialOf(error) {
|
|
6479
|
-
if (typeof error !== "object" || error === null) return void 0;
|
|
6480
|
-
const neutralReason = error.neutralReason;
|
|
6481
|
-
if (typeof neutralReason !== "string" || neutralReason.trim().length === 0) return void 0;
|
|
6482
|
-
return { error: "run_denied", neutralReason };
|
|
6483
|
-
}
|
|
6484
6330
|
function createRunServiceRoutes(ports) {
|
|
6485
6331
|
const openStreams = /* @__PURE__ */ new Set();
|
|
6486
6332
|
function handleStart(req, res) {
|
|
@@ -6511,12 +6357,7 @@ function createRunServiceRoutes(ports) {
|
|
|
6511
6357
|
ack = await ports.invokeRunStart(
|
|
6512
6358
|
parsed.data.patterns === void 0 ? {} : { patterns: parsed.data.patterns }
|
|
6513
6359
|
);
|
|
6514
|
-
} catch
|
|
6515
|
-
const denial = runDenialOf(error);
|
|
6516
|
-
if (denial !== void 0) {
|
|
6517
|
-
respondJson2(res, 403, denial);
|
|
6518
|
-
return;
|
|
6519
|
-
}
|
|
6360
|
+
} catch {
|
|
6520
6361
|
refuse(res, 500, "run_start_failed");
|
|
6521
6362
|
return;
|
|
6522
6363
|
}
|
|
@@ -6596,6 +6437,10 @@ function createRunServiceRoutes(ports) {
|
|
|
6596
6437
|
refuse(res, 403, "cross_site_refused");
|
|
6597
6438
|
return;
|
|
6598
6439
|
}
|
|
6440
|
+
if (carriesNonJsonBody(req)) {
|
|
6441
|
+
refuse(res, 415, "unsupported_media_type");
|
|
6442
|
+
return;
|
|
6443
|
+
}
|
|
6599
6444
|
const matched = matchRunServiceRoute(req.method, path);
|
|
6600
6445
|
if (matched === void 0) {
|
|
6601
6446
|
refuse(res, 404, "unknown_route");
|
|
@@ -6863,8 +6708,7 @@ data: ${JSON.stringify(again.body)}
|
|
|
6863
6708
|
// A failed re-read is not a reason to close a live channel: the next envelope will trigger
|
|
6864
6709
|
// another, and the page's own poll is the floor underneath both.
|
|
6865
6710
|
() => void 0
|
|
6866
|
-
//
|
|
6867
|
-
// failure arm never sees its own fulfilment arm's throw (UF-01, on the nested chain).
|
|
6711
|
+
// A throw from the arm above is a dead socket, and `.then`'s failure arm never sees it.
|
|
6868
6712
|
).catch(failStream);
|
|
6869
6713
|
});
|
|
6870
6714
|
heartbeat = setInterval(() => {
|
|
@@ -6903,10 +6747,6 @@ async function readRunKnowledge(host, runId) {
|
|
|
6903
6747
|
}
|
|
6904
6748
|
}
|
|
6905
6749
|
function serveRunKnowledgeRead(req, res, deps) {
|
|
6906
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
|
|
6907
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
6908
|
-
return;
|
|
6909
|
-
}
|
|
6910
6750
|
void readRunKnowledge(deps.host, deps.runId).then(
|
|
6911
6751
|
(outcome) => {
|
|
6912
6752
|
if (outcome.kind === "refused") {
|
|
@@ -6949,10 +6789,6 @@ async function markRun(host, runId, body) {
|
|
|
6949
6789
|
}
|
|
6950
6790
|
}
|
|
6951
6791
|
function registerRunMarkRoute(req, res, deps) {
|
|
6952
|
-
if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
|
|
6953
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
6954
|
-
return;
|
|
6955
|
-
}
|
|
6956
6792
|
void readJsonBody(req).then(async (body) => {
|
|
6957
6793
|
const outcome = await markRun(deps.host, deps.runId, body);
|
|
6958
6794
|
if (outcome.kind === "refused") {
|
|
@@ -6975,10 +6811,6 @@ import {
|
|
|
6975
6811
|
isPageWritableConfigPath
|
|
6976
6812
|
} from "@vostride/agent-qa-schema";
|
|
6977
6813
|
function registerConfigWriteRoute(req, res, deps) {
|
|
6978
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
6979
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
6980
|
-
return;
|
|
6981
|
-
}
|
|
6982
6814
|
void readJsonBody(req).then(async (body) => {
|
|
6983
6815
|
const parsed = ConfigWriteRequestSchema.safeParse(body);
|
|
6984
6816
|
if (!parsed.success) {
|
|
@@ -7036,10 +6868,6 @@ function registerConfigUndoRoute(req, res, deps) {
|
|
|
7036
6868
|
respondJson(res, 405, { error: "method_not_allowed" });
|
|
7037
6869
|
return;
|
|
7038
6870
|
}
|
|
7039
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
7040
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
7041
|
-
return;
|
|
7042
|
-
}
|
|
7043
6871
|
void readJsonBody(req).then(async (body) => {
|
|
7044
6872
|
const parsed = ConfigUndoRequestSchema.safeParse(body);
|
|
7045
6873
|
if (!parsed.success) {
|
|
@@ -7282,10 +7110,6 @@ async function connectName(request2, deps) {
|
|
|
7282
7110
|
});
|
|
7283
7111
|
}
|
|
7284
7112
|
function registerLlmConnectRoute(req, res, deps) {
|
|
7285
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
7286
|
-
respondJson(res, 403, { error: "cross_site_refused" });
|
|
7287
|
-
return;
|
|
7288
|
-
}
|
|
7289
7113
|
void readJsonBody(req).then(async (body) => {
|
|
7290
7114
|
const answer = await connectLlm(body, deps);
|
|
7291
7115
|
respondJson(res, answer.status, answer.body);
|
|
@@ -7297,9 +7121,9 @@ function registerLlmConnectRoute(req, res, deps) {
|
|
|
7297
7121
|
}
|
|
7298
7122
|
|
|
7299
7123
|
// src/server/routes.ts
|
|
7300
|
-
import { AuthStateNameSchema, buildInternalRunAttributes, mergeRunAttributes, readAuth as readAuth2, writeAuth as writeAuth2, removeAuth as removeAuth2, getAgentQaVersion, getAgentQaUpdateStatus, getLLMAuthProviderPlugin as getLLMAuthProviderPlugin2, listAuthStateMetadata, listLLMAuthProviderPlugins, ModelConfigSchema as ModelConfigSchema2, NameSlugSchema as NameSlugSchema2, TimeoutConfigSchema, AgentQaConfigSchema as AgentQaConfigSchema3, TestDefinitionSchema as TestDefinitionSchema2, SuiteDefinitionSchema as SuiteDefinitionSchema2, parseEnvFile, serializeEnvFile,
|
|
7124
|
+
import { AuthStateNameSchema, buildInternalRunAttributes, DEFAULT_AGENT_QA_CACHE_DIR, mergeRunAttributes, readAuth as readAuth2, writeAuth as writeAuth2, removeAuth as removeAuth2, getAgentQaVersion as getAgentQaVersion2, getAgentQaUpdateStatus, getLLMAuthProviderPlugin as getLLMAuthProviderPlugin2, listAuthStateMetadata, listLLMAuthProviderPlugins, ModelConfigSchema as ModelConfigSchema2, NameSlugSchema as NameSlugSchema2, TimeoutConfigSchema as TimeoutConfigSchema2, AgentQaConfigSchema as AgentQaConfigSchema3, TestDefinitionSchema as TestDefinitionSchema2, SuiteDefinitionSchema as SuiteDefinitionSchema2, parseEnvFile, serializeEnvFile, hookRunnerImage, validateUserRunAttributes, discoverWorkspaceFiles as discoverWorkspaceFiles3, isWorkspacePathMatch, resolveWorkspaceFileTarget as resolveWorkspaceFileTarget2, SizeString } from "@vostride/agent-qa-core";
|
|
7301
7125
|
import { SecretStore, SecretRedactor } from "@vostride/agent-qa-runner";
|
|
7302
|
-
import { DockerHookExecutor as DockerHookExecutor2 } from "@vostride/agent-qa-runner-local";
|
|
7126
|
+
import { DockerHookExecutor as DockerHookExecutor2, purgeActionCache } from "@vostride/agent-qa-runner-local";
|
|
7303
7127
|
import { createModel as createModel3, fetchLiveModels, getModelCatalog, listProviders, mergeCatalog, resolveVisionConfig } from "@vostride/agent-qa-model";
|
|
7304
7128
|
import { parse as parseYaml6 } from "yaml";
|
|
7305
7129
|
var LLM_PROVIDER_MODES = /* @__PURE__ */ new Set([
|
|
@@ -7607,25 +7431,10 @@ function llmConnectionTestTimeoutMs(config) {
|
|
|
7607
7431
|
return REMOTE_LLM_CONNECTION_TEST_TIMEOUT_MS;
|
|
7608
7432
|
}
|
|
7609
7433
|
function json(res, data, status = 200) {
|
|
7610
|
-
const body = JSON.stringify(data);
|
|
7611
|
-
res.writeHead(status, {
|
|
7612
|
-
"Content-Type": "application/json",
|
|
7613
|
-
"Access-Control-Allow-Origin": "*",
|
|
7614
|
-
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
7615
|
-
"Access-Control-Allow-Headers": "Content-Type"
|
|
7616
|
-
});
|
|
7617
|
-
res.end(body);
|
|
7618
|
-
}
|
|
7619
|
-
function jsonNoCors(res, data, status = 200) {
|
|
7620
7434
|
const body = JSON.stringify(data);
|
|
7621
7435
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
7622
7436
|
res.end(body);
|
|
7623
7437
|
}
|
|
7624
|
-
function refuseCrossSiteDisplayRead(req, res) {
|
|
7625
|
-
if (refuseCrossSiteRequest(req) === void 0) return false;
|
|
7626
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
7627
|
-
return true;
|
|
7628
|
-
}
|
|
7629
7438
|
var INSIGHTS_KNOWLEDGE_RUN_CAP = 200;
|
|
7630
7439
|
var INSIGHTS_KNOWLEDGE_CONCURRENCY = 4;
|
|
7631
7440
|
async function isPostProcessingSkipped(host, runId) {
|
|
@@ -7689,14 +7498,6 @@ async function readInsightsKnowledgeChanges(host, db, window) {
|
|
|
7689
7498
|
function notFound(res, message = "Not found") {
|
|
7690
7499
|
json(res, { error: message }, 404);
|
|
7691
7500
|
}
|
|
7692
|
-
function cors(res) {
|
|
7693
|
-
res.writeHead(204, {
|
|
7694
|
-
"Access-Control-Allow-Origin": "*",
|
|
7695
|
-
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
7696
|
-
"Access-Control-Allow-Headers": "Content-Type"
|
|
7697
|
-
});
|
|
7698
|
-
res.end();
|
|
7699
|
-
}
|
|
7700
7501
|
function isPlainRecord(value) {
|
|
7701
7502
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7702
7503
|
}
|
|
@@ -7749,7 +7550,7 @@ function normalizeRunFilterValue(value) {
|
|
|
7749
7550
|
function buildRunRequestAttributes(input) {
|
|
7750
7551
|
const internal = buildInternalRunAttributes({
|
|
7751
7552
|
trigger: input.trigger,
|
|
7752
|
-
runner:
|
|
7553
|
+
runner: "local"
|
|
7753
7554
|
});
|
|
7754
7555
|
const user = validateUserRunAttributes(input.userAttributes, "run attributes");
|
|
7755
7556
|
return mergeRunAttributes(internal, user);
|
|
@@ -7884,7 +7685,7 @@ function parseTimeoutConfigTestTimeoutMs(timeoutConfig) {
|
|
|
7884
7685
|
return transformedTimeout;
|
|
7885
7686
|
}
|
|
7886
7687
|
}
|
|
7887
|
-
const parsed =
|
|
7688
|
+
const parsed = TimeoutConfigSchema2.partial().safeParse(timeoutConfig);
|
|
7888
7689
|
if (!parsed.success) return void 0;
|
|
7889
7690
|
const testTimeout = parsed.data.test;
|
|
7890
7691
|
return typeof testTimeout === "number" && Number.isFinite(testTimeout) && testTimeout > 0 ? testTimeout : void 0;
|
|
@@ -8194,9 +7995,7 @@ var RESPONSE_SAFE_RUN_METADATA_KEYS = /* @__PURE__ */ new Set([
|
|
|
8194
7995
|
"timeoutSource",
|
|
8195
7996
|
"timeoutBaseMs",
|
|
8196
7997
|
"timeoutBufferMs",
|
|
8197
|
-
"suiteIndex"
|
|
8198
|
-
"farmSessionUrl",
|
|
8199
|
-
"runDevice"
|
|
7998
|
+
"suiteIndex"
|
|
8200
7999
|
]);
|
|
8201
8000
|
function sanitizeRunMetadataForResponse(metadata) {
|
|
8202
8001
|
if (metadata === null || metadata === void 0) return metadata ?? null;
|
|
@@ -8226,17 +8025,7 @@ var RESPONSE_SAFE_RUN_READ_METHODS = [
|
|
|
8226
8025
|
"getRunsByTest"
|
|
8227
8026
|
];
|
|
8228
8027
|
var RUN_READS_WITHOUT_ROWS = /* @__PURE__ */ new Set([
|
|
8229
|
-
//
|
|
8230
|
-
//
|
|
8231
|
-
// The five WRITES return nothing; they are here for the same reason `insertRun` is.
|
|
8232
|
-
//
|
|
8233
|
-
// `readRunDetailRows` IS A READ AND IS STILL HERE, and that is the judgement this set exists to
|
|
8234
|
-
// make explicit rather than to hide. It returns members, steps and sub-actions — the bus
|
|
8235
|
-
// payloads as the subscriber MASKED them (T-363-05) — and NO `RunRow` and no
|
|
8236
|
-
// `RunArtifactRow`. It therefore carries none of the run metadata the 401-23 scrub law exists
|
|
8237
|
-
// to strip, so wrapping it would be wrapping a shape that has nothing to strip. This router
|
|
8238
|
-
// does not call it either: the detail arm dispatches through the injected host, which is the
|
|
8239
|
-
// whole of D-03.
|
|
8028
|
+
// `readRunDetailRows` returns masked bus payloads and no run row, so it has no metadata to strip.
|
|
8240
8029
|
"deleteMemberProjection",
|
|
8241
8030
|
"incrementRunMemberAttempts",
|
|
8242
8031
|
"insertRunStep",
|
|
@@ -8247,23 +8036,23 @@ var RUN_READS_WITHOUT_ROWS = /* @__PURE__ */ new Set([
|
|
|
8247
8036
|
"finalizeRunArtifact",
|
|
8248
8037
|
"getRunsBySuiteIdCount",
|
|
8249
8038
|
"getRunsByTestCount",
|
|
8250
|
-
//
|
|
8039
|
+
// Run ids and a count, no row; each id is dispatched through the agent host.
|
|
8251
8040
|
"listInsightsWindowRunIds",
|
|
8252
8041
|
"insertPendingRun",
|
|
8253
8042
|
"insertRun",
|
|
8254
8043
|
"insertRunArtifact",
|
|
8255
8044
|
"listRunAttributeKeys",
|
|
8256
8045
|
"listRunAttributeValues",
|
|
8257
|
-
//
|
|
8046
|
+
// An owner (pid, incarnation, host), no run metadata.
|
|
8258
8047
|
"readRunOwner",
|
|
8259
|
-
//
|
|
8048
|
+
// Ids and owners only, no run metadata.
|
|
8260
8049
|
"listRunningRunOwners",
|
|
8261
|
-
//
|
|
8050
|
+
// Compare-and-set writes that return a boolean.
|
|
8262
8051
|
"finalizeOrphanRun",
|
|
8263
8052
|
"finalizeOwnRun",
|
|
8264
|
-
//
|
|
8053
|
+
// A compare-and-set write that returns a boolean, no run metadata.
|
|
8265
8054
|
"closeOwnRun",
|
|
8266
|
-
//
|
|
8055
|
+
// A compare-and-set write that returns a boolean, no run metadata.
|
|
8267
8056
|
"claimRunningRow",
|
|
8268
8057
|
"stageRunArtifact",
|
|
8269
8058
|
"updateRun",
|
|
@@ -8334,10 +8123,7 @@ function withResponseSafeRunReads(db) {
|
|
|
8334
8123
|
// `sanitizeArtifactForResponse`; a caller reaching for the bare row has no such step, so the
|
|
8335
8124
|
// projection is its only choke point. Same allowlist, so the two cannot drift.
|
|
8336
8125
|
getRunArtifact: { value: (runId) => scrubArtifact(db.getRunArtifact(runId)) },
|
|
8337
|
-
//
|
|
8338
|
-
// parent, whose `run` rows this override scrubbed; nothing has written a child row since
|
|
8339
|
-
// 402-09, and the shape went with the column. `getRunArtifact` above remains the choke point
|
|
8340
|
-
// for the payload itself, and this wrapper delegates to it.
|
|
8126
|
+
// One artifact; `getRunArtifact` is the choke point for its payload, and this delegates to it.
|
|
8341
8127
|
getRunArtifactBundle: {
|
|
8342
8128
|
value: (runId) => ({ artifact: scrubArtifact(db.getRunArtifactBundle(runId).artifact) })
|
|
8343
8129
|
}
|
|
@@ -8485,10 +8271,18 @@ function createRouter(dbOrDeps) {
|
|
|
8485
8271
|
json(res, { error: "Bad Request" }, 400);
|
|
8486
8272
|
return;
|
|
8487
8273
|
}
|
|
8274
|
+
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
8275
|
+
json(res, { error: "cross_site_refused" }, 403);
|
|
8276
|
+
return;
|
|
8277
|
+
}
|
|
8278
|
+
if (carriesNonJsonBody(req)) {
|
|
8279
|
+
json(res, { error: "unsupported_media_type" }, 415);
|
|
8280
|
+
return;
|
|
8281
|
+
}
|
|
8488
8282
|
const path = url.pathname;
|
|
8489
8283
|
if (path === "/api/config" && req.method === "PATCH") {
|
|
8490
8284
|
if (!configManager) {
|
|
8491
|
-
|
|
8285
|
+
json(res, { error: "config_unavailable" }, 503);
|
|
8492
8286
|
return;
|
|
8493
8287
|
}
|
|
8494
8288
|
registerConfigWriteRoute(req, res, { configManager, undoStore });
|
|
@@ -8508,7 +8302,7 @@ function createRouter(dbOrDeps) {
|
|
|
8508
8302
|
}
|
|
8509
8303
|
if (path === "/api/config/revision" && req.method === "GET") {
|
|
8510
8304
|
if (!configManager) {
|
|
8511
|
-
|
|
8305
|
+
json(res, { error: "config_unavailable" }, 503);
|
|
8512
8306
|
return;
|
|
8513
8307
|
}
|
|
8514
8308
|
registerConfigRevisionRoute(req, res, { configManager });
|
|
@@ -8516,7 +8310,7 @@ function createRouter(dbOrDeps) {
|
|
|
8516
8310
|
}
|
|
8517
8311
|
if (path === "/api/llm/connect" && req.method === "POST") {
|
|
8518
8312
|
if (!configManager) {
|
|
8519
|
-
|
|
8313
|
+
json(res, { error: "config_unavailable" }, 503);
|
|
8520
8314
|
return;
|
|
8521
8315
|
}
|
|
8522
8316
|
registerLlmConnectRoute(req, res, {
|
|
@@ -8528,16 +8322,6 @@ function createRouter(dbOrDeps) {
|
|
|
8528
8322
|
});
|
|
8529
8323
|
return;
|
|
8530
8324
|
}
|
|
8531
|
-
if (path === "/api/runs/trigger" || path === "/api/queue/enqueue") {
|
|
8532
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
8533
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
8534
|
-
return;
|
|
8535
|
-
}
|
|
8536
|
-
}
|
|
8537
|
-
if (req.method === "OPTIONS") {
|
|
8538
|
-
cors(res);
|
|
8539
|
-
return;
|
|
8540
|
-
}
|
|
8541
8325
|
if (path === "/api/auth-states" && req.method === "GET") {
|
|
8542
8326
|
if (!configManager || !deps.configPath) {
|
|
8543
8327
|
json(res, { error: "Auth-state metadata not available" }, 503);
|
|
@@ -8561,11 +8345,10 @@ function createRouter(dbOrDeps) {
|
|
|
8561
8345
|
return;
|
|
8562
8346
|
}
|
|
8563
8347
|
if (path === "/api/runs" && req.method === "GET") {
|
|
8564
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
8565
8348
|
const statuses = url.searchParams.getAll("status");
|
|
8566
8349
|
const unknownStatus = statuses.find((word) => !isRunStatusWord(word));
|
|
8567
8350
|
if (unknownStatus !== void 0) {
|
|
8568
|
-
|
|
8351
|
+
json(res, { error: `Unknown run status: ${unknownStatus}.` }, 400);
|
|
8569
8352
|
return;
|
|
8570
8353
|
}
|
|
8571
8354
|
const name = url.searchParams.get("name") ?? void 0;
|
|
@@ -8597,12 +8380,12 @@ function createRouter(dbOrDeps) {
|
|
|
8597
8380
|
)).sort((a, b) => a.localeCompare(b));
|
|
8598
8381
|
const filtered = platformFilteredRuns.filter((run) => matchesTargetFilter(run, target));
|
|
8599
8382
|
const runs = filtered.slice(offset, offset + limit);
|
|
8600
|
-
|
|
8383
|
+
json(res, {
|
|
8601
8384
|
runs,
|
|
8602
8385
|
total: filtered.length,
|
|
8603
8386
|
targets: targetOptions
|
|
8604
8387
|
});
|
|
8605
|
-
}).catch(() =>
|
|
8388
|
+
}).catch(() => json(res, { error: "Failed to list runs" }, 500));
|
|
8606
8389
|
return;
|
|
8607
8390
|
}
|
|
8608
8391
|
if (path === "/api/runs/attributes/keys" && req.method === "GET") {
|
|
@@ -8640,7 +8423,6 @@ function createRouter(dbOrDeps) {
|
|
|
8640
8423
|
}
|
|
8641
8424
|
const runArtifactMatch = path.match(/^\/api\/runs\/([^/]+)\/artifact$/);
|
|
8642
8425
|
if (runArtifactMatch && req.method === "GET") {
|
|
8643
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
8644
8426
|
const runId = decodeURIComponent(runArtifactMatch[1]);
|
|
8645
8427
|
const run = db.getRun(runId);
|
|
8646
8428
|
if (!run) {
|
|
@@ -8648,7 +8430,7 @@ function createRouter(dbOrDeps) {
|
|
|
8648
8430
|
return;
|
|
8649
8431
|
}
|
|
8650
8432
|
const bundle = db.getRunArtifactBundle(runId);
|
|
8651
|
-
|
|
8433
|
+
json(res, {
|
|
8652
8434
|
run,
|
|
8653
8435
|
artifact: sanitizeArtifactForResponse(bundle.artifact),
|
|
8654
8436
|
missingSections: getArtifactMissingSections(bundle.artifact)
|
|
@@ -8656,7 +8438,6 @@ function createRouter(dbOrDeps) {
|
|
|
8656
8438
|
return;
|
|
8657
8439
|
}
|
|
8658
8440
|
if (path.startsWith("/api/runs/") && path.endsWith("/execution-logs") && req.method === "GET") {
|
|
8659
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
8660
8441
|
const segments = path.split("/");
|
|
8661
8442
|
const id = segments[3];
|
|
8662
8443
|
const run = db.getRun(id);
|
|
@@ -8667,37 +8448,37 @@ function createRouter(dbOrDeps) {
|
|
|
8667
8448
|
const stepId = url.searchParams.get("stepId") ?? void 0;
|
|
8668
8449
|
const type = url.searchParams.get("type") ?? void 0;
|
|
8669
8450
|
const logs = db.getExecutionLogs({ runId: id, stepId, type });
|
|
8670
|
-
|
|
8451
|
+
json(res, { logs });
|
|
8671
8452
|
return;
|
|
8672
8453
|
}
|
|
8673
8454
|
if (path.startsWith("/api/runs/") && path.endsWith("/mark") && req.method === "POST") {
|
|
8674
8455
|
if (deps.agentHost === void 0) {
|
|
8675
|
-
|
|
8456
|
+
json(res, { error: "agent_host_unavailable" }, 503);
|
|
8676
8457
|
return;
|
|
8677
8458
|
}
|
|
8678
8459
|
let runId;
|
|
8679
8460
|
try {
|
|
8680
8461
|
runId = decodeURIComponent(path.slice("/api/runs/".length, -"/mark".length));
|
|
8681
8462
|
} catch {
|
|
8682
|
-
|
|
8463
|
+
json(res, { error: "invalid_mark_input" }, 400);
|
|
8683
8464
|
return;
|
|
8684
8465
|
}
|
|
8685
|
-
registerRunMarkRoute(req, res, { host: deps.agentHost, runId
|
|
8466
|
+
registerRunMarkRoute(req, res, { host: deps.agentHost, runId });
|
|
8686
8467
|
return;
|
|
8687
8468
|
}
|
|
8688
8469
|
if (req.method === "POST" && /^\/api\/runs\/[^/]+\/mark-preview$/.test(path)) {
|
|
8689
8470
|
if (deps.agentHost === void 0) {
|
|
8690
|
-
|
|
8471
|
+
json(res, { error: "agent_host_unavailable" }, 503);
|
|
8691
8472
|
return;
|
|
8692
8473
|
}
|
|
8693
8474
|
let runId;
|
|
8694
8475
|
try {
|
|
8695
8476
|
runId = decodeURIComponent(path.slice("/api/runs/".length, -"/mark-preview".length));
|
|
8696
8477
|
} catch {
|
|
8697
|
-
|
|
8478
|
+
json(res, { error: "invalid_run_id" }, 400);
|
|
8698
8479
|
return;
|
|
8699
8480
|
}
|
|
8700
|
-
serveRunMarkPreviewRead(req, res, { runId, host: deps.agentHost
|
|
8481
|
+
serveRunMarkPreviewRead(req, res, { runId, host: deps.agentHost });
|
|
8701
8482
|
return;
|
|
8702
8483
|
}
|
|
8703
8484
|
if (path.startsWith("/api/runs/") && path.endsWith("/cancel") && req.method === "POST") {
|
|
@@ -8776,44 +8557,39 @@ function createRouter(dbOrDeps) {
|
|
|
8776
8557
|
return;
|
|
8777
8558
|
}
|
|
8778
8559
|
if (req.method === "GET" && /^\/api\/runs\/[^/]+\/live-url$/.test(path)) {
|
|
8779
|
-
const crossSite = refuseCrossSiteRequest(req);
|
|
8780
|
-
if (crossSite !== void 0) {
|
|
8781
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
8782
|
-
return;
|
|
8783
|
-
}
|
|
8784
8560
|
if (!deps.mintLiveUrl) {
|
|
8785
|
-
|
|
8561
|
+
json(res, { error: "live_view_unavailable" }, 503);
|
|
8786
8562
|
return;
|
|
8787
8563
|
}
|
|
8788
8564
|
let liveUrlRunId;
|
|
8789
8565
|
try {
|
|
8790
8566
|
liveUrlRunId = decodeURIComponent(path.slice("/api/runs/".length, -"/live-url".length));
|
|
8791
8567
|
} catch {
|
|
8792
|
-
|
|
8568
|
+
json(res, { error: "unknown_run" }, 404);
|
|
8793
8569
|
return;
|
|
8794
8570
|
}
|
|
8795
8571
|
const rawHandle = handleParamOf(req);
|
|
8796
8572
|
const parsedHandle = parseHandleParam2(rawHandle);
|
|
8797
8573
|
if (parsedHandle.kind === "invalid") {
|
|
8798
|
-
|
|
8574
|
+
json(res, { error: "invalid_handle" }, 400);
|
|
8799
8575
|
return;
|
|
8800
8576
|
}
|
|
8801
8577
|
const handle = parsedHandle.kind === "absent" ? 0 : parsedHandle.index;
|
|
8802
8578
|
void deps.mintLiveUrl(liveUrlRunId, handle).then(
|
|
8803
8579
|
(result) => {
|
|
8804
|
-
|
|
8580
|
+
json(res, result);
|
|
8805
8581
|
},
|
|
8806
8582
|
(error) => {
|
|
8807
8583
|
const name = error instanceof Error ? error.name : "";
|
|
8808
8584
|
if (name === "UnknownRunError") {
|
|
8809
|
-
|
|
8585
|
+
json(res, { error: "unknown_run" }, 404);
|
|
8810
8586
|
return;
|
|
8811
8587
|
}
|
|
8812
8588
|
if (name === "LiveViewUnavailableError") {
|
|
8813
|
-
|
|
8589
|
+
json(res, { error: "live_view_unavailable" }, 503);
|
|
8814
8590
|
return;
|
|
8815
8591
|
}
|
|
8816
|
-
|
|
8592
|
+
json(res, { error: "live_url_mint_failed" }, 500);
|
|
8817
8593
|
}
|
|
8818
8594
|
);
|
|
8819
8595
|
return;
|
|
@@ -8842,32 +8618,32 @@ function createRouter(dbOrDeps) {
|
|
|
8842
8618
|
}
|
|
8843
8619
|
if (req.method === "GET" && /^\/api\/runs\/[^/]+\/evidence$/.test(path)) {
|
|
8844
8620
|
if (!deps.agentHost) {
|
|
8845
|
-
|
|
8621
|
+
json(res, { error: "agent_host_unavailable" }, 503);
|
|
8846
8622
|
return;
|
|
8847
8623
|
}
|
|
8848
8624
|
let runId;
|
|
8849
8625
|
try {
|
|
8850
8626
|
runId = decodeURIComponent(path.slice("/api/runs/".length, -"/evidence".length));
|
|
8851
8627
|
} catch {
|
|
8852
|
-
|
|
8628
|
+
json(res, { error: "invalid_run_id" }, 400);
|
|
8853
8629
|
return;
|
|
8854
8630
|
}
|
|
8855
|
-
serveRunEvidenceRead(req, res, { runId, host: deps.agentHost
|
|
8631
|
+
serveRunEvidenceRead(req, res, { runId, host: deps.agentHost });
|
|
8856
8632
|
return;
|
|
8857
8633
|
}
|
|
8858
8634
|
if (req.method === "GET" && /^\/api\/runs\/[^/]+\/marks$/.test(path)) {
|
|
8859
8635
|
if (!deps.agentHost) {
|
|
8860
|
-
|
|
8636
|
+
json(res, { error: "agent_host_unavailable" }, 503);
|
|
8861
8637
|
return;
|
|
8862
8638
|
}
|
|
8863
8639
|
let runId;
|
|
8864
8640
|
try {
|
|
8865
8641
|
runId = decodeURIComponent(path.slice("/api/runs/".length, -"/marks".length));
|
|
8866
8642
|
} catch {
|
|
8867
|
-
|
|
8643
|
+
json(res, { error: "invalid_run_id" }, 400);
|
|
8868
8644
|
return;
|
|
8869
8645
|
}
|
|
8870
|
-
serveRunMarksHistoryRead(req, res, { runId, host: deps.agentHost
|
|
8646
|
+
serveRunMarksHistoryRead(req, res, { runId, host: deps.agentHost });
|
|
8871
8647
|
return;
|
|
8872
8648
|
}
|
|
8873
8649
|
if (req.method === "GET" && /^\/api\/runs\/[^/]+\/detail$/.test(path)) {
|
|
@@ -8882,7 +8658,7 @@ function createRouter(dbOrDeps) {
|
|
|
8882
8658
|
json(res, { error: "invalid_run_id" }, 400);
|
|
8883
8659
|
return;
|
|
8884
8660
|
}
|
|
8885
|
-
serveRunDetailRead(req, res, { runId: detailRunId, host: deps.agentHost
|
|
8661
|
+
serveRunDetailRead(req, res, { runId: detailRunId, host: deps.agentHost });
|
|
8886
8662
|
return;
|
|
8887
8663
|
}
|
|
8888
8664
|
if (req.method === "GET" && /^\/api\/runs\/[^/]+\/analysis$/.test(path)) {
|
|
@@ -8897,7 +8673,7 @@ function createRouter(dbOrDeps) {
|
|
|
8897
8673
|
json(res, { error: "invalid_run_id" }, 400);
|
|
8898
8674
|
return;
|
|
8899
8675
|
}
|
|
8900
|
-
serveRunAnalysisRead(req, res, { runId: analysisRunId, host: deps.agentHost
|
|
8676
|
+
serveRunAnalysisRead(req, res, { runId: analysisRunId, host: deps.agentHost });
|
|
8901
8677
|
return;
|
|
8902
8678
|
}
|
|
8903
8679
|
if (req.method === "GET" && /^\/api\/runs\/[^/]+\/knowledge$/.test(path)) {
|
|
@@ -8914,8 +8690,7 @@ function createRouter(dbOrDeps) {
|
|
|
8914
8690
|
}
|
|
8915
8691
|
serveRunKnowledgeRead(req, res, {
|
|
8916
8692
|
runId: knowledgeRunId,
|
|
8917
|
-
host: deps.agentHost
|
|
8918
|
-
boundary: "human"
|
|
8693
|
+
host: deps.agentHost
|
|
8919
8694
|
});
|
|
8920
8695
|
return;
|
|
8921
8696
|
}
|
|
@@ -8965,14 +8740,13 @@ function createRouter(dbOrDeps) {
|
|
|
8965
8740
|
notFound(res);
|
|
8966
8741
|
return;
|
|
8967
8742
|
}
|
|
8968
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
8969
8743
|
const id = segments[3];
|
|
8970
8744
|
const run = db.getRun(id);
|
|
8971
8745
|
if (!run) {
|
|
8972
8746
|
notFound(res, "Run not found");
|
|
8973
8747
|
return;
|
|
8974
8748
|
}
|
|
8975
|
-
|
|
8749
|
+
json(res, { run });
|
|
8976
8750
|
return;
|
|
8977
8751
|
}
|
|
8978
8752
|
if (path === "/api/stats/costs" && req.method === "GET") {
|
|
@@ -8983,19 +8757,18 @@ function createRouter(dbOrDeps) {
|
|
|
8983
8757
|
return;
|
|
8984
8758
|
}
|
|
8985
8759
|
if (path === "/api/insights/knowledge-changes" && req.method === "GET") {
|
|
8986
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
8987
8760
|
if (!deps.agentHost) {
|
|
8988
|
-
|
|
8761
|
+
json(res, { status: "unavailable", reason: "agent_host_unavailable" }, 503);
|
|
8989
8762
|
return;
|
|
8990
8763
|
}
|
|
8991
8764
|
const from = url.searchParams.get("from") ?? void 0;
|
|
8992
8765
|
const to = url.searchParams.get("to") ?? void 0;
|
|
8993
8766
|
void readInsightsKnowledgeChanges(deps.agentHost, db, { from, to }).then(
|
|
8994
8767
|
(body) => {
|
|
8995
|
-
|
|
8768
|
+
json(res, body);
|
|
8996
8769
|
},
|
|
8997
8770
|
() => {
|
|
8998
|
-
|
|
8771
|
+
json(res, { status: "unavailable", reason: "read_failed" }, 500);
|
|
8999
8772
|
}
|
|
9000
8773
|
);
|
|
9001
8774
|
return;
|
|
@@ -9061,10 +8834,7 @@ function createRouter(dbOrDeps) {
|
|
|
9061
8834
|
serveRunEventStream(req, res, {
|
|
9062
8835
|
runId,
|
|
9063
8836
|
handleParam: handleParamOf(req),
|
|
9064
|
-
//
|
|
9065
|
-
// it from. `url.searchParams.get('lastEventId')` would resolve a repeated parameter to its
|
|
9066
|
-
// first value — the one answer `lastEventIdParamOf` exists to refuse — and a second
|
|
9067
|
-
// header/query precedence rule here would be a second contract for where a client resumes.
|
|
8837
|
+
// Read from the same pair as the authed door; `searchParams.get` takes a repeated first.
|
|
9068
8838
|
resume: resumePointOf(req.headers["last-event-id"], lastEventIdParamOf(req)),
|
|
9069
8839
|
attach: deps.runServiceAttach
|
|
9070
8840
|
});
|
|
@@ -9106,7 +8876,6 @@ function createRouter(dbOrDeps) {
|
|
|
9106
8876
|
try {
|
|
9107
8877
|
attributes = buildRunRequestAttributes({
|
|
9108
8878
|
trigger: "api",
|
|
9109
|
-
runner: "local",
|
|
9110
8879
|
userAttributes: body.attributes
|
|
9111
8880
|
});
|
|
9112
8881
|
} catch (err) {
|
|
@@ -9114,25 +8883,10 @@ function createRouter(dbOrDeps) {
|
|
|
9114
8883
|
return;
|
|
9115
8884
|
}
|
|
9116
8885
|
const args = normalizedFileTarget ? [normalizedFileTarget.executionPath] : [];
|
|
9117
|
-
const source = isSuiteFile ? "suite" : "dashboard";
|
|
9118
|
-
try {
|
|
9119
|
-
await executionBackend?.preflight?.({ args, source });
|
|
9120
|
-
} catch (error) {
|
|
9121
|
-
const denial = runDenialOf(error);
|
|
9122
|
-
if (denial !== void 0) {
|
|
9123
|
-
json(res, denial, 403);
|
|
9124
|
-
return;
|
|
9125
|
-
}
|
|
9126
|
-
json(res, { error: error instanceof Error ? error.message : String(error) }, 400);
|
|
9127
|
-
return;
|
|
9128
|
-
}
|
|
9129
8886
|
const runId = jobQueue.enqueue({
|
|
9130
8887
|
name: body.name,
|
|
9131
8888
|
filePath: normalizedFileTarget?.storagePath,
|
|
9132
|
-
//
|
|
9133
|
-
// with member handles, so a parent artifact kind would claim parenthood over rows
|
|
9134
|
-
// nothing creates. `isSuiteFile` still travels in `metadata.isSuite` below, and the run's
|
|
9135
|
-
// own `suite_id` plus the artifact's `source.kind` snapshot still say it is a suite.
|
|
8889
|
+
// No parent kind is minted; `metadata.isSuite` and `suite_id` say it is a suite.
|
|
9136
8890
|
kind: "test",
|
|
9137
8891
|
attributes,
|
|
9138
8892
|
priority: body.priority,
|
|
@@ -9215,17 +8969,6 @@ function createRouter(dbOrDeps) {
|
|
|
9215
8969
|
args.push("--disable-cache");
|
|
9216
8970
|
}
|
|
9217
8971
|
const isSuite = Boolean(isSuiteFile);
|
|
9218
|
-
try {
|
|
9219
|
-
await executionBackend?.preflight?.({ args, source: isSuite ? "suite" : "dashboard" });
|
|
9220
|
-
} catch (error) {
|
|
9221
|
-
const denial = runDenialOf(error);
|
|
9222
|
-
if (denial !== void 0) {
|
|
9223
|
-
json(res, denial, 403);
|
|
9224
|
-
return;
|
|
9225
|
-
}
|
|
9226
|
-
json(res, { error: error instanceof Error ? error.message : String(error) }, 400);
|
|
9227
|
-
return;
|
|
9228
|
-
}
|
|
9229
8972
|
let testName = body.file ? basename5(normalizedFileTarget?.storagePath ?? body.file, ".yaml") : "unknown";
|
|
9230
8973
|
let fileParallel;
|
|
9231
8974
|
let platform;
|
|
@@ -9268,11 +9011,10 @@ function createRouter(dbOrDeps) {
|
|
|
9268
9011
|
const runId = jobQueue.enqueue({
|
|
9269
9012
|
name: testName,
|
|
9270
9013
|
filePath: normalizedFileTarget?.storagePath ?? body.file,
|
|
9271
|
-
//
|
|
9014
|
+
// No parent kind is minted; see the enqueue route above.
|
|
9272
9015
|
kind: "test",
|
|
9273
9016
|
attributes: buildRunRequestAttributes({
|
|
9274
|
-
trigger: triggerSource.trigger
|
|
9275
|
-
runner: body.local === false ? "browserstack" : "local"
|
|
9017
|
+
trigger: triggerSource.trigger
|
|
9276
9018
|
}),
|
|
9277
9019
|
platform,
|
|
9278
9020
|
parallel: effectiveParallel,
|
|
@@ -9781,7 +9523,7 @@ function createRouter(dbOrDeps) {
|
|
|
9781
9523
|
return;
|
|
9782
9524
|
}
|
|
9783
9525
|
if (path === "/api/app-metadata" && req.method === "GET") {
|
|
9784
|
-
const version =
|
|
9526
|
+
const version = getAgentQaVersion2().trim() || "0.0.0";
|
|
9785
9527
|
(async () => {
|
|
9786
9528
|
try {
|
|
9787
9529
|
const status = await getAgentQaUpdateStatus();
|
|
@@ -9825,9 +9567,8 @@ function createRouter(dbOrDeps) {
|
|
|
9825
9567
|
return;
|
|
9826
9568
|
}
|
|
9827
9569
|
if (path === "/api/config" && req.method === "GET") {
|
|
9828
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
9829
9570
|
if (!configManager) {
|
|
9830
|
-
|
|
9571
|
+
json(res, { error: "Config management not available" }, 501);
|
|
9831
9572
|
return;
|
|
9832
9573
|
}
|
|
9833
9574
|
;
|
|
@@ -9835,7 +9576,7 @@ function createRouter(dbOrDeps) {
|
|
|
9835
9576
|
try {
|
|
9836
9577
|
const read = await configManager.readMasked();
|
|
9837
9578
|
if (read.kind === "file_invalid") {
|
|
9838
|
-
|
|
9579
|
+
json(res, { error: "file_invalid", message: read.message }, 409);
|
|
9839
9580
|
return;
|
|
9840
9581
|
}
|
|
9841
9582
|
const { config, revision, lines, fileIssues } = read;
|
|
@@ -9845,7 +9586,7 @@ function createRouter(dbOrDeps) {
|
|
|
9845
9586
|
const activeLlm = use?.llm ?? config.defaultLLM;
|
|
9846
9587
|
const defaultCfg = Array.isArray(llms) ? llms.find((c) => c?.name === activeLlm) : void 0;
|
|
9847
9588
|
const provider = defaultCfg?.provider ?? null;
|
|
9848
|
-
|
|
9589
|
+
json(res, {
|
|
9849
9590
|
config,
|
|
9850
9591
|
provider,
|
|
9851
9592
|
revision,
|
|
@@ -9853,45 +9594,17 @@ function createRouter(dbOrDeps) {
|
|
|
9853
9594
|
...fileIssues.length > 0 ? { fileIssues } : {}
|
|
9854
9595
|
});
|
|
9855
9596
|
} catch (err) {
|
|
9856
|
-
|
|
9597
|
+
json(res, { error: err instanceof Error ? err.message : "Failed to read config" }, 500);
|
|
9857
9598
|
}
|
|
9858
9599
|
})();
|
|
9859
9600
|
return;
|
|
9860
9601
|
}
|
|
9861
|
-
if (path === "/api/config/farm/test-connection" && req.method === "POST") {
|
|
9862
|
-
readJsonBody(req).then(async (body) => {
|
|
9863
|
-
if (body.provider !== "browserstack") {
|
|
9864
|
-
json(res, { error: "Only BrowserStack is currently supported" }, 400);
|
|
9865
|
-
return;
|
|
9866
|
-
}
|
|
9867
|
-
try {
|
|
9868
|
-
const auth = Buffer.from(`${body.username}:${body.accessKey}`).toString("base64");
|
|
9869
|
-
const response = await fetch("https://api-cloud.browserstack.com/app-automate/devices.json", {
|
|
9870
|
-
headers: { Authorization: `Basic ${auth}` }
|
|
9871
|
-
});
|
|
9872
|
-
if (response.ok) {
|
|
9873
|
-
json(res, { success: true });
|
|
9874
|
-
} else if (response.status === 401) {
|
|
9875
|
-
json(res, { success: false, error: "Invalid credentials" });
|
|
9876
|
-
} else {
|
|
9877
|
-
json(res, { success: false, error: `BrowserStack API returned ${response.status}` });
|
|
9878
|
-
}
|
|
9879
|
-
} catch (err) {
|
|
9880
|
-
json(res, { success: false, error: err instanceof Error ? err.message : "Connection failed" });
|
|
9881
|
-
}
|
|
9882
|
-
}).catch(() => json(res, { error: "Invalid request body" }, 400));
|
|
9883
|
-
return;
|
|
9884
|
-
}
|
|
9885
9602
|
if (path.startsWith("/api/auth/") && req.method === "DELETE") {
|
|
9886
9603
|
const segments = path.split("/");
|
|
9887
9604
|
if (segments.length === 4 && segments[2] === "auth") {
|
|
9888
9605
|
const providerName = decodeURIComponent(segments[3]);
|
|
9889
9606
|
if (providerName === "status" || providerName === "credential" || providerName === "oauth") {
|
|
9890
9607
|
} else {
|
|
9891
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
9892
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
9893
|
-
return;
|
|
9894
|
-
}
|
|
9895
9608
|
;
|
|
9896
9609
|
(async () => {
|
|
9897
9610
|
try {
|
|
@@ -9902,13 +9615,13 @@ function createRouter(dbOrDeps) {
|
|
|
9902
9615
|
return credential;
|
|
9903
9616
|
});
|
|
9904
9617
|
if (removed === void 0) {
|
|
9905
|
-
|
|
9618
|
+
json(res, { deleted: false });
|
|
9906
9619
|
return;
|
|
9907
9620
|
}
|
|
9908
9621
|
const undo = undoStore.put({ kind: "credential", name: providerName, credential: removed }, Date.now());
|
|
9909
|
-
|
|
9622
|
+
json(res, { deleted: true, undo });
|
|
9910
9623
|
} catch (err) {
|
|
9911
|
-
|
|
9624
|
+
json(res, { error: err instanceof Error ? err.message : "Failed to delete credential" }, 500);
|
|
9912
9625
|
}
|
|
9913
9626
|
})();
|
|
9914
9627
|
return;
|
|
@@ -10018,27 +9731,21 @@ function createRouter(dbOrDeps) {
|
|
|
10018
9731
|
const segments = path.split("/");
|
|
10019
9732
|
const provider = segments.length >= 6 ? decodeURIComponent(segments[4] ?? "") : "";
|
|
10020
9733
|
const action = segments.length >= 6 ? segments[5] : "";
|
|
10021
|
-
const authDoor = req.method === "POST" && (action === "start" || action === "exchange");
|
|
10022
|
-
if (authDoor && refuseCrossSiteRequest(req) !== void 0) {
|
|
10023
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
10024
|
-
return;
|
|
10025
|
-
}
|
|
10026
9734
|
const plugin = provider ? getLLMAuthProviderPlugin2(provider) : void 0;
|
|
10027
9735
|
if (!plugin) {
|
|
10028
|
-
;
|
|
10029
|
-
(authDoor ? jsonNoCors : json)(res, { error: `Auth plugin provider "${provider}" is not registered` }, 404);
|
|
9736
|
+
json(res, { error: `Auth plugin provider "${provider}" is not registered` }, 404);
|
|
10030
9737
|
return;
|
|
10031
9738
|
}
|
|
10032
9739
|
if (action === "start" && req.method === "POST") {
|
|
10033
9740
|
readJsonBody(req).then(async (body) => {
|
|
10034
9741
|
if (!plugin.startAuth) {
|
|
10035
|
-
|
|
9742
|
+
json(res, { error: `Provider "${provider}" does not support dashboard auth start` }, 400);
|
|
10036
9743
|
return;
|
|
10037
9744
|
}
|
|
10038
9745
|
try {
|
|
10039
9746
|
const target = await requirePluginAuthConfig(configManager, body.configName, plugin);
|
|
10040
9747
|
if (!target.ok) {
|
|
10041
|
-
|
|
9748
|
+
json(res, credentialRefusalBody(target), target.status);
|
|
10042
9749
|
return;
|
|
10043
9750
|
}
|
|
10044
9751
|
const started = await plugin.startAuth({
|
|
@@ -10074,16 +9781,16 @@ function createRouter(dbOrDeps) {
|
|
|
10074
9781
|
session.error = err instanceof Error ? err.message : "Authentication failed";
|
|
10075
9782
|
}).finally(() => cleanupPluginOAuthSession(session));
|
|
10076
9783
|
}
|
|
10077
|
-
|
|
9784
|
+
json(res, {
|
|
10078
9785
|
authorizeUrl: started.authorizeUrl,
|
|
10079
9786
|
sessionId,
|
|
10080
9787
|
mode: plugin.dashboardAuth.mode,
|
|
10081
9788
|
created: target.created
|
|
10082
9789
|
});
|
|
10083
9790
|
} catch (err) {
|
|
10084
|
-
|
|
9791
|
+
json(res, { error: err instanceof Error ? err.message : "Failed to start auth plugin flow" }, 500);
|
|
10085
9792
|
}
|
|
10086
|
-
}).catch(() =>
|
|
9793
|
+
}).catch(() => json(res, { error: "Invalid request body" }, 400));
|
|
10087
9794
|
return;
|
|
10088
9795
|
}
|
|
10089
9796
|
if (action === "result" && req.method === "GET") {
|
|
@@ -10111,22 +9818,22 @@ function createRouter(dbOrDeps) {
|
|
|
10111
9818
|
if (action === "exchange" && req.method === "POST") {
|
|
10112
9819
|
readJsonBody(req).then(async (body) => {
|
|
10113
9820
|
if (!plugin.exchangeCode) {
|
|
10114
|
-
|
|
9821
|
+
json(res, { error: `Provider "${provider}" does not support code exchange` }, 400);
|
|
10115
9822
|
return;
|
|
10116
9823
|
}
|
|
10117
9824
|
const sessionId = typeof body.sessionId === "string" ? body.sessionId.trim() : "";
|
|
10118
9825
|
const code = typeof body.code === "string" ? body.code.trim() : "";
|
|
10119
9826
|
if (!sessionId || !code) {
|
|
10120
|
-
|
|
9827
|
+
json(res, { error: "sessionId and code are required" }, 400);
|
|
10121
9828
|
return;
|
|
10122
9829
|
}
|
|
10123
9830
|
const session = pluginOAuthSessions.get(sessionId);
|
|
10124
9831
|
if (!session || session.providerId !== provider) {
|
|
10125
|
-
|
|
9832
|
+
json(res, { error: "Auth session not found" }, 404);
|
|
10126
9833
|
return;
|
|
10127
9834
|
}
|
|
10128
9835
|
if (session.status !== "pending") {
|
|
10129
|
-
|
|
9836
|
+
json(res, { error: "Auth session is no longer pending" }, 409);
|
|
10130
9837
|
return;
|
|
10131
9838
|
}
|
|
10132
9839
|
try {
|
|
@@ -10148,61 +9855,57 @@ function createRouter(dbOrDeps) {
|
|
|
10148
9855
|
session.error = CREDENTIAL_PROVIDER_CONFLICT.message;
|
|
10149
9856
|
cleanupPluginOAuthSession(session);
|
|
10150
9857
|
pluginOAuthSessions.delete(sessionId);
|
|
10151
|
-
|
|
9858
|
+
json(res, credentialRefusalBody(CREDENTIAL_PROVIDER_CONFLICT), CREDENTIAL_PROVIDER_CONFLICT.status);
|
|
10152
9859
|
return;
|
|
10153
9860
|
}
|
|
10154
9861
|
session.status = "completed";
|
|
10155
9862
|
cleanupPluginOAuthSession(session);
|
|
10156
9863
|
pluginOAuthSessions.delete(sessionId);
|
|
10157
|
-
|
|
9864
|
+
json(res, { status: "completed", saved: true });
|
|
10158
9865
|
} catch (err) {
|
|
10159
9866
|
session.status = "error";
|
|
10160
9867
|
session.error = err instanceof Error ? err.message : "Token exchange failed";
|
|
10161
9868
|
cleanupPluginOAuthSession(session);
|
|
10162
9869
|
pluginOAuthSessions.delete(sessionId);
|
|
10163
|
-
|
|
9870
|
+
json(res, { error: session.error }, 500);
|
|
10164
9871
|
}
|
|
10165
|
-
}).catch(() =>
|
|
9872
|
+
}).catch(() => json(res, { error: "Invalid request body" }, 400));
|
|
10166
9873
|
return;
|
|
10167
9874
|
}
|
|
10168
9875
|
json(res, { error: "Auth plugin route not found" }, 404);
|
|
10169
9876
|
return;
|
|
10170
9877
|
}
|
|
10171
9878
|
if (path === "/api/auth/credential" && req.method === "POST") {
|
|
10172
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
10173
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
10174
|
-
return;
|
|
10175
|
-
}
|
|
10176
9879
|
readJsonBody(req).then(async (body) => {
|
|
10177
9880
|
const provider = typeof body.provider === "string" ? body.provider.trim() : "";
|
|
10178
9881
|
const credentialType = typeof body.type === "string" ? body.type.trim() : "";
|
|
10179
9882
|
const secret = typeof body.secret === "string" ? body.secret.trim() : "";
|
|
10180
9883
|
if (typeof body.configName !== "string" || body.configName.trim() === "") {
|
|
10181
|
-
|
|
9884
|
+
json(res, { error: "configName is required" }, 400);
|
|
10182
9885
|
return;
|
|
10183
9886
|
}
|
|
10184
9887
|
if (!provider || !isKnownLLMProvider(provider)) {
|
|
10185
|
-
|
|
9888
|
+
json(res, { error: "valid provider is required" }, 400);
|
|
10186
9889
|
return;
|
|
10187
9890
|
}
|
|
10188
9891
|
if (credentialType !== "api-key" && credentialType !== "bearer-token") {
|
|
10189
|
-
|
|
9892
|
+
json(res, { error: "type must be api-key or bearer-token" }, 400);
|
|
10190
9893
|
return;
|
|
10191
9894
|
}
|
|
10192
9895
|
if (!secret) {
|
|
10193
|
-
|
|
9896
|
+
json(res, { error: "secret is required" }, 400);
|
|
10194
9897
|
return;
|
|
10195
9898
|
}
|
|
10196
9899
|
if (getLLMAuthProviderPlugin2(provider)) {
|
|
10197
|
-
|
|
9900
|
+
json(res, { error: "Subscription providers use OAuth login" }, 400);
|
|
10198
9901
|
return;
|
|
10199
9902
|
}
|
|
10200
9903
|
if (credentialType === "bearer-token" && provider !== "anthropic-compatible") {
|
|
10201
|
-
|
|
9904
|
+
json(res, { error: "bearer-token credentials are only supported for anthropic-compatible configs" }, 400);
|
|
10202
9905
|
return;
|
|
10203
9906
|
}
|
|
10204
9907
|
if (credentialType === "api-key" && !API_KEY_CREDENTIAL_PROVIDERS.has(provider)) {
|
|
10205
|
-
|
|
9908
|
+
json(res, { error: "api-key credentials are not supported for this provider" }, 400);
|
|
10206
9909
|
return;
|
|
10207
9910
|
}
|
|
10208
9911
|
try {
|
|
@@ -10217,35 +9920,31 @@ function createRouter(dbOrDeps) {
|
|
|
10217
9920
|
return checked;
|
|
10218
9921
|
});
|
|
10219
9922
|
if (!target.ok) {
|
|
10220
|
-
|
|
9923
|
+
json(res, credentialRefusalBody(target), target.status);
|
|
10221
9924
|
return;
|
|
10222
9925
|
}
|
|
10223
|
-
|
|
9926
|
+
json(res, { saved: true, created: target.created });
|
|
10224
9927
|
} catch (err) {
|
|
10225
|
-
|
|
9928
|
+
json(res, { error: err instanceof Error ? err.message : "Failed to save credential" }, 500);
|
|
10226
9929
|
}
|
|
10227
|
-
}).catch(() =>
|
|
9930
|
+
}).catch(() => json(res, { error: "Invalid request body" }, 400));
|
|
10228
9931
|
return;
|
|
10229
9932
|
}
|
|
10230
9933
|
if (path === "/api/auth/credential/move" && req.method === "POST") {
|
|
10231
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
10232
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
10233
|
-
return;
|
|
10234
|
-
}
|
|
10235
9934
|
readJsonBody(req).then(async (raw) => {
|
|
10236
9935
|
const parsed = LlmCredentialMoveRequestSchema.safeParse(raw);
|
|
10237
9936
|
if (!parsed.success) {
|
|
10238
|
-
|
|
9937
|
+
json(res, { error: "invalid_setup_input" }, 400);
|
|
10239
9938
|
return;
|
|
10240
9939
|
}
|
|
10241
9940
|
const from = checkCredentialName(parsed.data.from);
|
|
10242
9941
|
const to = checkCredentialName(parsed.data.to);
|
|
10243
9942
|
if (!from.ok || !to.ok) {
|
|
10244
|
-
|
|
9943
|
+
json(res, { error: "name_invalid" }, 400);
|
|
10245
9944
|
return;
|
|
10246
9945
|
}
|
|
10247
9946
|
if (!configManager) {
|
|
10248
|
-
|
|
9947
|
+
json(res, { error: "Config manager is required to move credentials" }, 400);
|
|
10249
9948
|
return;
|
|
10250
9949
|
}
|
|
10251
9950
|
try {
|
|
@@ -10263,25 +9962,21 @@ function createRouter(dbOrDeps) {
|
|
|
10263
9962
|
return void 0;
|
|
10264
9963
|
});
|
|
10265
9964
|
if (refusal) {
|
|
10266
|
-
|
|
9965
|
+
json(res, credentialRefusalBody(refusal), refusal.status);
|
|
10267
9966
|
return;
|
|
10268
9967
|
}
|
|
10269
|
-
|
|
9968
|
+
json(res, { moved: true });
|
|
10270
9969
|
} catch (err) {
|
|
10271
|
-
|
|
9970
|
+
json(res, { error: err instanceof Error ? err.message : "Failed to move credential" }, 500);
|
|
10272
9971
|
}
|
|
10273
|
-
}).catch(() =>
|
|
9972
|
+
}).catch(() => json(res, { error: "invalid_setup_input" }, 400));
|
|
10274
9973
|
return;
|
|
10275
9974
|
}
|
|
10276
9975
|
if (path === "/api/llm/test" && req.method === "POST") {
|
|
10277
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
10278
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
10279
|
-
return;
|
|
10280
|
-
}
|
|
10281
9976
|
readJsonBody(req).then(async (body) => {
|
|
10282
9977
|
const rawBody = body;
|
|
10283
9978
|
if ("apiKey" in rawBody) {
|
|
10284
|
-
|
|
9979
|
+
json(res, {
|
|
10285
9980
|
success: false,
|
|
10286
9981
|
error: "invalid_request",
|
|
10287
9982
|
message: "apiKey is not accepted in LLM test requests. Save credentials for the named config instead."
|
|
@@ -10291,7 +9986,7 @@ function createRouter(dbOrDeps) {
|
|
|
10291
9986
|
const providerName = typeof body.provider === "string" ? body.provider.trim() : "";
|
|
10292
9987
|
const modelName = typeof body.model === "string" ? body.model.trim() : "";
|
|
10293
9988
|
if (!providerName || !modelName) {
|
|
10294
|
-
|
|
9989
|
+
json(res, { success: false, error: "invalid_request", message: "provider and model are required" }, 400);
|
|
10295
9990
|
return;
|
|
10296
9991
|
}
|
|
10297
9992
|
const configName = typeof body.configName === "string" ? body.configName.trim() : "";
|
|
@@ -10316,7 +10011,7 @@ function createRouter(dbOrDeps) {
|
|
|
10316
10011
|
}
|
|
10317
10012
|
const parsedConfig = ModelConfigSchema2.safeParse(llmCandidate);
|
|
10318
10013
|
if (!parsedConfig.success) {
|
|
10319
|
-
|
|
10014
|
+
json(res, {
|
|
10320
10015
|
success: false,
|
|
10321
10016
|
error: "invalid_request",
|
|
10322
10017
|
message: parsedConfig.error.issues.map((issue) => `${issue.path.map(String).join(".") || "config"}: ${issue.message}`).join("; ")
|
|
@@ -10328,7 +10023,7 @@ function createRouter(dbOrDeps) {
|
|
|
10328
10023
|
const coreAuth = await import("@vostride/agent-qa-core");
|
|
10329
10024
|
const resolvedAuth = await coreAuth.resolveLLMAuth(configName, llmConfig2);
|
|
10330
10025
|
if (resolvedAuth.kind === "missing") {
|
|
10331
|
-
|
|
10026
|
+
json(res, {
|
|
10332
10027
|
success: false,
|
|
10333
10028
|
error: "missing_credential",
|
|
10334
10029
|
message: resolvedAuth.message
|
|
@@ -10357,7 +10052,7 @@ function createRouter(dbOrDeps) {
|
|
|
10357
10052
|
} finally {
|
|
10358
10053
|
clearTimeout(timeout);
|
|
10359
10054
|
}
|
|
10360
|
-
|
|
10055
|
+
json(res, {
|
|
10361
10056
|
success: true,
|
|
10362
10057
|
model: modelName,
|
|
10363
10058
|
provider: providerName,
|
|
@@ -10372,7 +10067,7 @@ function createRouter(dbOrDeps) {
|
|
|
10372
10067
|
const providerMessage = withDeclaredMask(declaredMask, (mask) => mask(unsavedMessage));
|
|
10373
10068
|
const errorClass = classifyProviderError2(providerMessage ?? unsavedMessage, statusCode);
|
|
10374
10069
|
const message = providerMessage === void 0 ? providerErrorSentence2(errorClass, "").trimEnd() : providerErrorSentence2(errorClass, providerMessage);
|
|
10375
|
-
|
|
10070
|
+
json(res, {
|
|
10376
10071
|
success: false,
|
|
10377
10072
|
error: errorClass,
|
|
10378
10073
|
message,
|
|
@@ -10382,7 +10077,7 @@ function createRouter(dbOrDeps) {
|
|
|
10382
10077
|
...unauthenticated ? { unauthenticated: true, authMessage } : {}
|
|
10383
10078
|
});
|
|
10384
10079
|
}
|
|
10385
|
-
}).catch(() =>
|
|
10080
|
+
}).catch(() => json(res, { success: false, error: "invalid_request", message: "Invalid request body" }, 400));
|
|
10386
10081
|
return;
|
|
10387
10082
|
}
|
|
10388
10083
|
if (path === "/api/cache/purge" && req.method === "POST") {
|
|
@@ -10391,56 +10086,15 @@ function createRouter(dbOrDeps) {
|
|
|
10391
10086
|
return;
|
|
10392
10087
|
}
|
|
10393
10088
|
readJsonBody(req).then(async (body) => {
|
|
10394
|
-
if (
|
|
10395
|
-
json(res, { error: "
|
|
10089
|
+
if (body.all !== true) {
|
|
10090
|
+
json(res, { error: "all is required" }, 400);
|
|
10396
10091
|
return;
|
|
10397
10092
|
}
|
|
10398
10093
|
try {
|
|
10399
10094
|
const cfg = await readConfigValues(configManager);
|
|
10400
|
-
const cacheDir = cfg.services?.cache?.dir ?? cfg.cache?.dir;
|
|
10401
|
-
if (!cacheDir) {
|
|
10402
|
-
json(res, { error: "Cache directory not configured" }, 400);
|
|
10403
|
-
return;
|
|
10404
|
-
}
|
|
10405
10095
|
const configDir = deps.configPath ? dirname9(resolve5(deps.configPath)) : process.cwd();
|
|
10406
|
-
const
|
|
10407
|
-
|
|
10408
|
-
try {
|
|
10409
|
-
branchDirs = await readdir(resolvedCacheDir);
|
|
10410
|
-
} catch (err) {
|
|
10411
|
-
if (err.code === "ENOENT") {
|
|
10412
|
-
json(res, { purged: 0 });
|
|
10413
|
-
return;
|
|
10414
|
-
}
|
|
10415
|
-
throw err;
|
|
10416
|
-
}
|
|
10417
|
-
let purged = 0;
|
|
10418
|
-
for (const branch of branchDirs) {
|
|
10419
|
-
const branchPath = join4(resolvedCacheDir, branch);
|
|
10420
|
-
let entryStat;
|
|
10421
|
-
try {
|
|
10422
|
-
entryStat = await stat5(branchPath);
|
|
10423
|
-
} catch {
|
|
10424
|
-
continue;
|
|
10425
|
-
}
|
|
10426
|
-
if (!entryStat.isDirectory()) {
|
|
10427
|
-
await rm5(branchPath, { force: true });
|
|
10428
|
-
continue;
|
|
10429
|
-
}
|
|
10430
|
-
let files;
|
|
10431
|
-
try {
|
|
10432
|
-
files = await readdir(branchPath);
|
|
10433
|
-
} catch {
|
|
10434
|
-
continue;
|
|
10435
|
-
}
|
|
10436
|
-
for (const file of files) {
|
|
10437
|
-
if (!file.endsWith(".json")) continue;
|
|
10438
|
-
await rm5(join4(branchPath, file), { force: true });
|
|
10439
|
-
purged++;
|
|
10440
|
-
}
|
|
10441
|
-
await rm5(branchPath, { recursive: true, force: true }).catch(() => {
|
|
10442
|
-
});
|
|
10443
|
-
}
|
|
10096
|
+
const cacheDir = cfg.services?.cache?.dir ?? DEFAULT_AGENT_QA_CACHE_DIR;
|
|
10097
|
+
const purged = await purgeActionCache(resolve5(configDir, cacheDir));
|
|
10444
10098
|
json(res, { purged });
|
|
10445
10099
|
} catch (err) {
|
|
10446
10100
|
json(res, { error: err instanceof Error ? err.message : "Cache purge failed" }, 500);
|
|
@@ -10524,7 +10178,7 @@ function createRouter(dbOrDeps) {
|
|
|
10524
10178
|
return;
|
|
10525
10179
|
}
|
|
10526
10180
|
const workspaceEnv = await readWorkspaceEnvVars(workspacePaths);
|
|
10527
|
-
const result = await new DockerHookExecutor2().execute(resolvedHook, {
|
|
10181
|
+
const result = await new DockerHookExecutor2({ version: getAgentQaVersion2() }).execute(resolvedHook, {
|
|
10528
10182
|
env: {
|
|
10529
10183
|
...workspaceEnv,
|
|
10530
10184
|
...validation.overrides
|
|
@@ -10560,7 +10214,7 @@ function createRouter(dbOrDeps) {
|
|
|
10560
10214
|
variables: shown.variables,
|
|
10561
10215
|
sandbox: {
|
|
10562
10216
|
runtime: resolvedHook.runtime,
|
|
10563
|
-
image:
|
|
10217
|
+
image: hookRunnerImage(resolvedHook.runtime, getAgentQaVersion2()),
|
|
10564
10218
|
networkMode: resolvedHook.network ? "enabled" : "disabled",
|
|
10565
10219
|
dockerVersion: null,
|
|
10566
10220
|
networkLogsAvailable: shown.networkLogs.length > 0,
|
|
@@ -10764,12 +10418,8 @@ function createRouter(dbOrDeps) {
|
|
|
10764
10418
|
return;
|
|
10765
10419
|
}
|
|
10766
10420
|
if (req.method === "GET" && /^\/api\/live-editor\/sessions\/[^/]+\/live-url$/.test(path)) {
|
|
10767
|
-
if (refuseCrossSiteRequest(req) !== void 0) {
|
|
10768
|
-
jsonNoCors(res, { error: "cross_site_refused" }, 403);
|
|
10769
|
-
return;
|
|
10770
|
-
}
|
|
10771
10421
|
if (!deps.mintEditorLiveUrl) {
|
|
10772
|
-
|
|
10422
|
+
json(res, { error: "live_view_unavailable" }, 503);
|
|
10773
10423
|
return;
|
|
10774
10424
|
}
|
|
10775
10425
|
let editorSessionId;
|
|
@@ -10778,33 +10428,32 @@ function createRouter(dbOrDeps) {
|
|
|
10778
10428
|
path.slice("/api/live-editor/sessions/".length, -"/live-url".length)
|
|
10779
10429
|
);
|
|
10780
10430
|
} catch {
|
|
10781
|
-
|
|
10431
|
+
json(res, { error: "unknown_session" }, 404);
|
|
10782
10432
|
return;
|
|
10783
10433
|
}
|
|
10784
10434
|
void deps.mintEditorLiveUrl(editorSessionId).then(
|
|
10785
10435
|
(result) => {
|
|
10786
|
-
|
|
10436
|
+
json(res, result);
|
|
10787
10437
|
},
|
|
10788
10438
|
(error) => {
|
|
10789
10439
|
const name = error instanceof Error ? error.name : "";
|
|
10790
10440
|
if (name === "UnknownLiveEditorSessionError") {
|
|
10791
|
-
|
|
10441
|
+
json(res, { error: "unknown_session" }, 404);
|
|
10792
10442
|
return;
|
|
10793
10443
|
}
|
|
10794
10444
|
if (name === "LiveViewUnavailableError") {
|
|
10795
|
-
|
|
10445
|
+
json(res, { error: "live_view_unavailable" }, 503);
|
|
10796
10446
|
return;
|
|
10797
10447
|
}
|
|
10798
|
-
|
|
10448
|
+
json(res, { error: "live_url_mint_failed" }, 500);
|
|
10799
10449
|
}
|
|
10800
10450
|
);
|
|
10801
10451
|
return;
|
|
10802
10452
|
}
|
|
10803
10453
|
const liveEditorAuthStateMatch = path.match(/^\/api\/live-editor\/sessions\/([^/]+)\/auth-state$/);
|
|
10804
10454
|
if (liveEditorAuthStateMatch && req.method === "POST") {
|
|
10805
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
10806
10455
|
if (!sessionManager) {
|
|
10807
|
-
|
|
10456
|
+
json(res, { error: "Live editor not available" }, 503);
|
|
10808
10457
|
return;
|
|
10809
10458
|
}
|
|
10810
10459
|
;
|
|
@@ -10815,35 +10464,34 @@ function createRouter(dbOrDeps) {
|
|
|
10815
10464
|
const body = await readJsonBody(req);
|
|
10816
10465
|
const parsedName = AuthStateNameSchema.safeParse(typeof body?.name === "string" ? body.name : "");
|
|
10817
10466
|
if (!parsedName.success) {
|
|
10818
|
-
|
|
10467
|
+
json(res, { error: "Auth state name must be a lowercase slug." }, 400);
|
|
10819
10468
|
return;
|
|
10820
10469
|
}
|
|
10821
10470
|
stateName = parsedName.data;
|
|
10822
10471
|
session = sessionManager.getSession(decodeURIComponent(liveEditorAuthStateMatch[1]));
|
|
10823
10472
|
if (!session) {
|
|
10824
|
-
|
|
10473
|
+
json(res, { error: "Session not found" }, 404);
|
|
10825
10474
|
return;
|
|
10826
10475
|
}
|
|
10827
10476
|
if (typeof session.captureWebAuthState !== "function") {
|
|
10828
|
-
|
|
10477
|
+
json(res, {
|
|
10829
10478
|
error: `Could not save auth state "${stateName}" for target "${getSessionTargetNameForAuthState(session)}".`
|
|
10830
10479
|
}, 500);
|
|
10831
10480
|
return;
|
|
10832
10481
|
}
|
|
10833
10482
|
const authState = await session.captureWebAuthState(stateName, { replace: body?.replace === true });
|
|
10834
|
-
|
|
10483
|
+
json(res, { authState });
|
|
10835
10484
|
} catch (err) {
|
|
10836
10485
|
const targetName = getSessionTargetNameForAuthState(session);
|
|
10837
10486
|
const response = buildAuthStateSaveErrorMessage(stateName || "unknown", targetName, err);
|
|
10838
|
-
|
|
10487
|
+
json(res, { error: response.message }, response.status);
|
|
10839
10488
|
}
|
|
10840
10489
|
})();
|
|
10841
10490
|
return;
|
|
10842
10491
|
}
|
|
10843
10492
|
if (path === "/api/live-editor/sessions" && req.method === "POST") {
|
|
10844
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
10845
10493
|
if (!sessionManager) {
|
|
10846
|
-
|
|
10494
|
+
json(res, { error: "Live editor not available" }, 503);
|
|
10847
10495
|
return;
|
|
10848
10496
|
}
|
|
10849
10497
|
;
|
|
@@ -10851,16 +10499,10 @@ function createRouter(dbOrDeps) {
|
|
|
10851
10499
|
try {
|
|
10852
10500
|
const body = await readJsonBody(req);
|
|
10853
10501
|
const targetName = typeof body?.targetName === "string" ? body.targetName : void 0;
|
|
10854
|
-
const production = await resolveLiveTargetProduction(configManager, targetName);
|
|
10855
|
-
const productionRefusal = liveProductionRefusal(production);
|
|
10856
|
-
if (productionRefusal !== void 0) {
|
|
10857
|
-
jsonNoCors(res, { error: productionRefusal.message }, 403);
|
|
10858
|
-
return;
|
|
10859
|
-
}
|
|
10860
10502
|
const platform = body?.platform ?? "web";
|
|
10861
10503
|
const runtimeLLM = await resolveDashboardRuntimeLLM(configManager, llmConfig, authFetch);
|
|
10862
10504
|
if (!runtimeLLM.ok) {
|
|
10863
|
-
|
|
10505
|
+
json(res, { error: runtimeLLM.error }, 400);
|
|
10864
10506
|
return;
|
|
10865
10507
|
}
|
|
10866
10508
|
const setupHooks = Array.isArray(body?.setupHooks) ? body.setupHooks.filter((value) => typeof value === "string" && value.trim().length > 0) : [];
|
|
@@ -10879,14 +10521,14 @@ function createRouter(dbOrDeps) {
|
|
|
10879
10521
|
let resolvedHooks = /* @__PURE__ */ new Map();
|
|
10880
10522
|
let hookRegistryError;
|
|
10881
10523
|
if (!workspacePaths) {
|
|
10882
|
-
|
|
10524
|
+
json(res, { error: "Workspace path resolution not available" }, 500);
|
|
10883
10525
|
return;
|
|
10884
10526
|
}
|
|
10885
10527
|
try {
|
|
10886
10528
|
const envContent = await readFile6(workspacePaths.envFile.absolutePath, "utf-8");
|
|
10887
10529
|
envVars = parseEnvFile(envContent);
|
|
10888
10530
|
} catch {
|
|
10889
|
-
|
|
10531
|
+
json(res, { error: `workspace.envFile file could not be loaded: ${workspacePaths.envFile.workspaceRelativePath}` }, 400);
|
|
10890
10532
|
return;
|
|
10891
10533
|
}
|
|
10892
10534
|
try {
|
|
@@ -10894,7 +10536,7 @@ function createRouter(dbOrDeps) {
|
|
|
10894
10536
|
secretStore = SecretStore.fromEnvContent(secretsContent);
|
|
10895
10537
|
secretRedactor = new SecretRedactor(secretStore);
|
|
10896
10538
|
} catch {
|
|
10897
|
-
|
|
10539
|
+
json(res, { error: `workspace.secretsFile file could not be loaded: ${workspacePaths.secretsFile.workspaceRelativePath}` }, 400);
|
|
10898
10540
|
return;
|
|
10899
10541
|
}
|
|
10900
10542
|
const hookCatalog = await readWorkspaceHooks(configManager, deps.configPath);
|
|
@@ -10905,6 +10547,11 @@ function createRouter(dbOrDeps) {
|
|
|
10905
10547
|
...suiteFileManager === void 0 ? {} : { findSuite: (id) => suiteFileManager.findBySuiteId(id) }
|
|
10906
10548
|
};
|
|
10907
10549
|
const plannerLimits = await resolveLivePlannerLimits(configManager, entityRef, entityReaders);
|
|
10550
|
+
const navigationTimeoutMs = await resolveLiveNavigationTimeoutMs(
|
|
10551
|
+
configManager,
|
|
10552
|
+
entityRef,
|
|
10553
|
+
entityReaders
|
|
10554
|
+
);
|
|
10908
10555
|
const sessionSuiteRef = entityRef?.type === "suite" ? entityRef : void 0;
|
|
10909
10556
|
const plannerLimitsForTest = (testId) => resolveLivePlannerLimits(
|
|
10910
10557
|
configManager,
|
|
@@ -10915,7 +10562,6 @@ function createRouter(dbOrDeps) {
|
|
|
10915
10562
|
{
|
|
10916
10563
|
platform,
|
|
10917
10564
|
targetName,
|
|
10918
|
-
production: { productionTargetNames: production.productionTargetNames },
|
|
10919
10565
|
plannerLimits,
|
|
10920
10566
|
plannerLimitsForTest,
|
|
10921
10567
|
llmConfig: runtimeLLM.llmConfig,
|
|
@@ -10929,6 +10575,7 @@ function createRouter(dbOrDeps) {
|
|
|
10929
10575
|
hookRegistryError,
|
|
10930
10576
|
url: body?.url,
|
|
10931
10577
|
headless: body?.headless ?? false,
|
|
10578
|
+
navigationTimeoutMs,
|
|
10932
10579
|
useDeviceName: typeof body?.useDeviceName === "string" ? body.useDeviceName : void 0,
|
|
10933
10580
|
appState: body?.appState === "preserve" || body?.appState === "reset" ? body.appState : void 0,
|
|
10934
10581
|
bundleId: typeof body?.bundleId === "string" ? body.bundleId : void 0,
|
|
@@ -10938,38 +10585,37 @@ function createRouter(dbOrDeps) {
|
|
|
10938
10585
|
},
|
|
10939
10586
|
entityRef
|
|
10940
10587
|
);
|
|
10941
|
-
|
|
10588
|
+
json(res, { sessionId, sessionNumber }, 201);
|
|
10942
10589
|
} catch (err) {
|
|
10943
10590
|
const message = err instanceof Error ? err.message : String(err);
|
|
10944
|
-
const
|
|
10945
|
-
|
|
10591
|
+
const refused = err instanceof LivePlannerLimitsRefusedError || err instanceof LiveNavigationTimeoutRefusedError;
|
|
10592
|
+
const status = refused ? 400 : 500;
|
|
10593
|
+
json(res, { error: message }, status);
|
|
10946
10594
|
}
|
|
10947
10595
|
})();
|
|
10948
10596
|
return;
|
|
10949
10597
|
}
|
|
10950
10598
|
if (path === "/api/live-editor/sessions" && req.method === "GET") {
|
|
10951
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
10952
10599
|
if (!sessionManager) {
|
|
10953
|
-
|
|
10600
|
+
json(res, { error: "Live editor not available" }, 503);
|
|
10954
10601
|
return;
|
|
10955
10602
|
}
|
|
10956
|
-
|
|
10603
|
+
json(res, { sessions: sessionManager.listSessions() });
|
|
10957
10604
|
return;
|
|
10958
10605
|
}
|
|
10959
10606
|
const liveEditorSessionMatch = path.match(/^\/api\/live-editor\/sessions\/([^/]+)$/);
|
|
10960
10607
|
if (liveEditorSessionMatch && req.method === "DELETE") {
|
|
10961
|
-
if (refuseCrossSiteDisplayRead(req, res)) return;
|
|
10962
10608
|
if (!sessionManager) {
|
|
10963
|
-
|
|
10609
|
+
json(res, { error: "Live editor not available" }, 503);
|
|
10964
10610
|
return;
|
|
10965
10611
|
}
|
|
10966
10612
|
const sessionId = liveEditorSessionMatch[1];
|
|
10967
10613
|
(async () => {
|
|
10968
10614
|
const terminated = await sessionManager.terminateSession(sessionId);
|
|
10969
10615
|
if (terminated) {
|
|
10970
|
-
|
|
10616
|
+
json(res, { ok: true });
|
|
10971
10617
|
} else {
|
|
10972
|
-
|
|
10618
|
+
json(res, { error: "Session not found" }, 404);
|
|
10973
10619
|
}
|
|
10974
10620
|
})();
|
|
10975
10621
|
return;
|
|
@@ -11018,6 +10664,7 @@ function createRouter(dbOrDeps) {
|
|
|
11018
10664
|
// src/queue/job-queue.ts
|
|
11019
10665
|
import { EventEmitter } from "node:events";
|
|
11020
10666
|
import { cpus } from "node:os";
|
|
10667
|
+
import { printRunText as printRunText2 } from "@vostride/agent-qa-core";
|
|
11021
10668
|
var NOT_CANCELLABLE = { kind: "not-cancellable" };
|
|
11022
10669
|
var JobQueue = class extends EventEmitter {
|
|
11023
10670
|
db;
|
|
@@ -11040,8 +10687,7 @@ var JobQueue = class extends EventEmitter {
|
|
|
11040
10687
|
this.pollInterval = setInterval(() => this.dispatchReported("a poll"), intervalMs);
|
|
11041
10688
|
this.on("enqueued", () => this.dispatchReported("an enqueue"));
|
|
11042
10689
|
}
|
|
11043
|
-
//
|
|
11044
|
-
// still frees its slot, and nothing reads the closed database.
|
|
10690
|
+
// The server closes the database after this; a run settling later still frees its slot.
|
|
11045
10691
|
stop() {
|
|
11046
10692
|
this.stopped = true;
|
|
11047
10693
|
if (this.pollInterval) {
|
|
@@ -11127,7 +10773,10 @@ var JobQueue = class extends EventEmitter {
|
|
|
11127
10773
|
this.processNext();
|
|
11128
10774
|
} catch (error) {
|
|
11129
10775
|
const message = error instanceof Error ? error.message : String(error);
|
|
11130
|
-
|
|
10776
|
+
printRunText2(
|
|
10777
|
+
`Queue could not dispatch the next run after ${trigger}: ${message}`,
|
|
10778
|
+
(text) => console.error(text)
|
|
10779
|
+
);
|
|
11131
10780
|
}
|
|
11132
10781
|
}
|
|
11133
10782
|
getActiveCount() {
|
|
@@ -11136,9 +10785,7 @@ var JobQueue = class extends EventEmitter {
|
|
|
11136
10785
|
getConcurrency() {
|
|
11137
10786
|
return this.concurrency;
|
|
11138
10787
|
}
|
|
11139
|
-
//
|
|
11140
|
-
// 442-15 (GAP-D2): own is the stamp this handle wrote, or equal non-null start times.
|
|
11141
|
-
// 2026-10-17 (444-06, D-11): a running row is never written here; the run answers with its verdict.
|
|
10788
|
+
// Only the owner stops its running row, never written here; a gone owner's row is an orphan.
|
|
11142
10789
|
cancel(runId) {
|
|
11143
10790
|
const run = this.db.getRun(runId);
|
|
11144
10791
|
if (!run) return NOT_CANCELLABLE;
|
|
@@ -11168,21 +10815,7 @@ var JobQueue = class extends EventEmitter {
|
|
|
11168
10815
|
const run = this.db.getRun(runId);
|
|
11169
10816
|
return run && run.status !== "running" && run.status !== "pending" ? { kind: "finished", status: run.status } : NOT_CANCELLABLE;
|
|
11170
10817
|
}
|
|
11171
|
-
//
|
|
11172
|
-
// MODEL THAT MINTED ITS CHILDREN.
|
|
11173
|
-
//
|
|
11174
|
-
// ~19 lines stood here walking the run's child rows and cancelling each member row. Under
|
|
11175
|
-
// the one-run model a parallel suite is ONE run row with member handles (402-04) — nothing writes
|
|
11176
|
-
// the run row's parent-link column any more (405-07 then deleted the column outright), so the
|
|
11177
|
-
// walk could only ever have read an empty list while reading as live cancellation policy.
|
|
11178
|
-
// Cancelling the run cancels the run.
|
|
11179
|
-
//
|
|
11180
|
-
// WHAT THIS DOES NOT TOUCH: `parallel` below. D-11 keeps the queue's parallel flag as CROSS-RUN
|
|
11181
|
-
// concurrency of INDEPENDENT jobs, which is what `findNextEligible` has always used it for. What
|
|
11182
|
-
// was deleted is parallel-SUITE semantics, not the queue's ability to run two unrelated jobs.
|
|
11183
|
-
//
|
|
11184
|
-
// HISTORICAL ROWS: a pre-402 suite run still lists its members on the read side; cancelling
|
|
11185
|
-
// such a run now leaves those member rows in whatever state their own process left them.
|
|
10818
|
+
// `parallel` is cross-run concurrency of independent jobs; a suite is one run row.
|
|
11186
10819
|
findNextEligible() {
|
|
11187
10820
|
if (this.activeCount >= this.concurrency) return void 0;
|
|
11188
10821
|
const pending = this.db.getPendingRuns();
|
|
@@ -11205,7 +10838,7 @@ var JobQueue = class extends EventEmitter {
|
|
|
11205
10838
|
};
|
|
11206
10839
|
|
|
11207
10840
|
// src/tests/suite-file-manager.ts
|
|
11208
|
-
import { readFile as readFile7, mkdir as mkdir4, stat as
|
|
10841
|
+
import { readFile as readFile7, mkdir as mkdir4, stat as stat5, rm as rm5 } from "node:fs/promises";
|
|
11209
10842
|
import { dirname as dirname10, basename as basename6 } from "node:path";
|
|
11210
10843
|
import { generateSuiteId } from "@vostride/agent-qa-ids";
|
|
11211
10844
|
import { parse as parseYaml7 } from "yaml";
|
|
@@ -11216,14 +10849,14 @@ var SuiteFileManager = class {
|
|
|
11216
10849
|
this.testFileManager = testFileManager;
|
|
11217
10850
|
}
|
|
11218
10851
|
testFileManager;
|
|
11219
|
-
//
|
|
10852
|
+
// Each list call reads the workspace itself; each file is written whole, one writer at a time.
|
|
11220
10853
|
underLock = createPathLock();
|
|
11221
10854
|
async list() {
|
|
11222
10855
|
const records = await discoverWorkspaceFiles4({ workspace: this.workspace, kind: "suite" });
|
|
11223
10856
|
const files = [];
|
|
11224
10857
|
for (const record of records) {
|
|
11225
10858
|
try {
|
|
11226
|
-
const s = await
|
|
10859
|
+
const s = await stat5(record.absolutePath);
|
|
11227
10860
|
if (s.isFile()) {
|
|
11228
10861
|
let suiteName = basename6(record.workspaceRelativePath);
|
|
11229
10862
|
let testCount = 0;
|
|
@@ -11300,7 +10933,7 @@ var SuiteFileManager = class {
|
|
|
11300
10933
|
}
|
|
11301
10934
|
async delete(relativePath) {
|
|
11302
10935
|
const target = await this.resolveTarget(relativePath, true);
|
|
11303
|
-
await this.underLock(target.absolutePath, () =>
|
|
10936
|
+
await this.underLock(target.absolutePath, () => rm5(target.absolutePath));
|
|
11304
10937
|
}
|
|
11305
10938
|
async validate(content) {
|
|
11306
10939
|
try {
|
|
@@ -11357,6 +10990,7 @@ import { request } from "node:http";
|
|
|
11357
10990
|
import { spawn } from "node:child_process";
|
|
11358
10991
|
import {
|
|
11359
10992
|
formatAppiumInstallGuidance,
|
|
10993
|
+
printRunText as printRunText3,
|
|
11360
10994
|
resolveAppiumExecutable
|
|
11361
10995
|
} from "@vostride/agent-qa-core";
|
|
11362
10996
|
import treeKill from "tree-kill";
|
|
@@ -11484,21 +11118,21 @@ var AppiumManager = class {
|
|
|
11484
11118
|
if (err.code === "ENOENT") {
|
|
11485
11119
|
console.error(`${APPIUM_PREFIX} Appium not found. ${formatAppiumInstallGuidance()}`);
|
|
11486
11120
|
} else {
|
|
11487
|
-
|
|
11121
|
+
printRunText3(`${APPIUM_PREFIX} Error: ${err.message}`, (text) => console.error(text));
|
|
11488
11122
|
}
|
|
11489
11123
|
});
|
|
11490
11124
|
child.stdout?.on("data", (chunk) => {
|
|
11491
11125
|
if (this.logLevel !== "debug") return;
|
|
11492
11126
|
const lines = chunk.toString().split("\n").filter(Boolean);
|
|
11493
11127
|
for (const line of lines) {
|
|
11494
|
-
|
|
11128
|
+
printRunText3(`${APPIUM_PREFIX} ${pc.dim(line)}`, (text) => console.log(text));
|
|
11495
11129
|
}
|
|
11496
11130
|
});
|
|
11497
11131
|
child.stderr?.on("data", (chunk) => {
|
|
11498
11132
|
if (this.logLevel !== "debug") return;
|
|
11499
11133
|
const lines = chunk.toString().split("\n").filter(Boolean);
|
|
11500
11134
|
for (const line of lines) {
|
|
11501
|
-
|
|
11135
|
+
printRunText3(`${APPIUM_PREFIX} ${pc.dim(line)}`, (text) => console.error(text));
|
|
11502
11136
|
}
|
|
11503
11137
|
});
|
|
11504
11138
|
const start = Date.now();
|
|
@@ -11580,10 +11214,7 @@ var SessionManager = class {
|
|
|
11580
11214
|
this.deps = deps;
|
|
11581
11215
|
}
|
|
11582
11216
|
sessions = /* @__PURE__ */ new Map();
|
|
11583
|
-
//
|
|
11584
|
-
// Monotonic and ephemeral: resets on server restart per CONTEXT D-30.
|
|
11585
|
-
// Never decremented on terminate — a re-open of a terminated entity yields
|
|
11586
|
-
// the next number so "Session #3" after "#2" ends does not confuse users.
|
|
11217
|
+
// Monotonic per entity and reset on restart, so a reopened entity gets the next number.
|
|
11587
11218
|
counters = /* @__PURE__ */ new Map();
|
|
11588
11219
|
async createSession(config, entity) {
|
|
11589
11220
|
if (!config.llmConfig) {
|
|
@@ -11640,7 +11271,8 @@ var SessionManager = class {
|
|
|
11640
11271
|
};
|
|
11641
11272
|
|
|
11642
11273
|
// src/live-editor/ws-handler.ts
|
|
11643
|
-
import {
|
|
11274
|
+
import { format as format2 } from "node:util";
|
|
11275
|
+
import { parseTestFile, printRunText as printRunText4 } from "@vostride/agent-qa-core";
|
|
11644
11276
|
|
|
11645
11277
|
// src/live-editor/types.ts
|
|
11646
11278
|
var LIVE_EDITOR_UNKNOWN_SESSION_CLOSE_CODE = 4404;
|
|
@@ -11827,12 +11459,14 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
|
|
|
11827
11459
|
try {
|
|
11828
11460
|
raw = await testFileManager.read(msg.path);
|
|
11829
11461
|
} catch (readErr) {
|
|
11830
|
-
|
|
11462
|
+
const line = `[ws-handler] execute-test: read failed for path=${msg.path}`;
|
|
11463
|
+
printRunText4(format2(line, readErr), () => console.error(line, readErr));
|
|
11831
11464
|
throw new Error(`Cannot read test file "${msg.path}": ${readErr?.message ?? String(readErr)}`);
|
|
11832
11465
|
}
|
|
11833
11466
|
const parsed = parseTestFile(raw, msg.path);
|
|
11834
11467
|
if (parsed.errors.length > 0) {
|
|
11835
|
-
|
|
11468
|
+
const line = `[ws-handler] execute-test: parse errors for ${msg.path}`;
|
|
11469
|
+
printRunText4(format2(line, parsed.errors), () => console.error(line, parsed.errors));
|
|
11836
11470
|
throw new Error(`Parse error in ${msg.path}: ${parsed.errors[0].message}`);
|
|
11837
11471
|
}
|
|
11838
11472
|
const testDef = parsed.tests[0];
|
|
@@ -11842,7 +11476,13 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
|
|
|
11842
11476
|
const stepInstructions = testDef.steps.map(
|
|
11843
11477
|
(s) => typeof s === "string" ? s : s.step
|
|
11844
11478
|
);
|
|
11845
|
-
|
|
11479
|
+
const stepCount = stepInstructions.length;
|
|
11480
|
+
const setupCount = (testDef.setup ?? []).length;
|
|
11481
|
+
const teardownCount = (testDef.teardown ?? []).length;
|
|
11482
|
+
printRunText4(
|
|
11483
|
+
`[ws-handler] execute-test: starting "${testDef.name}" (${stepCount} steps, setup=${setupCount}, teardown=${teardownCount})`,
|
|
11484
|
+
(text) => console.log(text)
|
|
11485
|
+
);
|
|
11846
11486
|
reply({
|
|
11847
11487
|
type: "test-start",
|
|
11848
11488
|
test: {
|
|
@@ -11864,7 +11504,11 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
|
|
|
11864
11504
|
teardown: testDef.teardown ?? []
|
|
11865
11505
|
}
|
|
11866
11506
|
);
|
|
11867
|
-
|
|
11507
|
+
const errorPart = result.error ? ` error=${result.error}` : "";
|
|
11508
|
+
printRunText4(
|
|
11509
|
+
`[ws-handler] execute-test: finished "${testDef.name}" status=${result.status} duration=${result.duration}ms${errorPart}`,
|
|
11510
|
+
(text) => console.log(text)
|
|
11511
|
+
);
|
|
11868
11512
|
reply({
|
|
11869
11513
|
type: "test-complete",
|
|
11870
11514
|
test: {
|
|
@@ -11877,11 +11521,9 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
|
|
|
11877
11521
|
});
|
|
11878
11522
|
reply({ type: "session-state", state: session.getState() });
|
|
11879
11523
|
} catch (err) {
|
|
11880
|
-
|
|
11881
|
-
|
|
11882
|
-
|
|
11883
|
-
code: err?.code
|
|
11884
|
-
});
|
|
11524
|
+
const line = `[ws-handler] execute-test: caught error for path=${msg.path}`;
|
|
11525
|
+
const detail = { message: err?.message, stack: err?.stack, code: err?.code };
|
|
11526
|
+
printRunText4(format2(line, detail), () => console.error(line, detail));
|
|
11885
11527
|
reply({
|
|
11886
11528
|
type: "test-error",
|
|
11887
11529
|
test: {
|
|
@@ -12032,11 +11674,9 @@ var LIVE_EMBED_STATES = [
|
|
|
12032
11674
|
"no-stream",
|
|
12033
11675
|
"expired",
|
|
12034
11676
|
"refused",
|
|
12035
|
-
//
|
|
12036
|
-
// because a person watching one member of a suite needs to know which of the two happened — and
|
|
12037
|
-
// neither may be shown as `live` over whichever frame happened to be last.
|
|
11677
|
+
// This member's stream ended while the run continues, so the viewer knows which one ended.
|
|
12038
11678
|
"stream-ended",
|
|
12039
|
-
//
|
|
11679
|
+
// An editor session ended; an editor subject has no run to end.
|
|
12040
11680
|
"session-ended",
|
|
12041
11681
|
"ended",
|
|
12042
11682
|
"lost"
|
|
@@ -12165,8 +11805,7 @@ function renderLiveEmbedPage(input) {
|
|
|
12165
11805
|
if (pending === null) return;
|
|
12166
11806
|
var header = pending;
|
|
12167
11807
|
pending = null;
|
|
12168
|
-
//
|
|
12169
|
-
// both paint in every host, not only in Chromium (428 RESEARCH A2).
|
|
11808
|
+
// No MIME type: the decoder sniffs, so JPEG and PNG frames paint in every host.
|
|
12170
11809
|
var blob = new Blob([event.data]);
|
|
12171
11810
|
createImageBitmap(blob).then(function (bitmap) {
|
|
12172
11811
|
if (canvas.width !== header.width || canvas.height !== header.height) {
|
|
@@ -12190,8 +11829,7 @@ function renderLiveEmbedPage(input) {
|
|
|
12190
11829
|
try { msg = JSON.parse(event.data); } catch (e) { return; }
|
|
12191
11830
|
|
|
12192
11831
|
if (msg.type === 'ready') {
|
|
12193
|
-
//
|
|
12194
|
-
// a transport guess (LIVE-04's prohibition). A snapshot paints on the same canvas (428 D-11).
|
|
11832
|
+
// The declared kind picks the painter, never a browser name, adapter or transport guess.
|
|
12195
11833
|
var kind = msg.stream ? msg.stream.kind : undefined;
|
|
12196
11834
|
if (kind === 'cdp-screencast' || kind === 'snapshot') {
|
|
12197
11835
|
setState('connecting', 'waiting for the first frame\u2026');
|
|
@@ -12238,8 +11876,7 @@ function renderLiveEmbedPage(input) {
|
|
|
12238
11876
|
report({ event: 'lost' });
|
|
12239
11877
|
});
|
|
12240
11878
|
|
|
12241
|
-
// A
|
|
12242
|
-
// so a renewal never costs the viewer the stream it is watching (D-02).
|
|
11879
|
+
// A re-minting host replaces the fragment; the socket renews in place and the stream continues.
|
|
12243
11880
|
window.addEventListener('hashchange', function () {
|
|
12244
11881
|
var next = location.hash.slice(1);
|
|
12245
11882
|
if (next === '' || socket.readyState !== 1) return;
|
|
@@ -12449,7 +12086,8 @@ function createFrameProxy(deps) {
|
|
|
12449
12086
|
|
|
12450
12087
|
// src/server/server.ts
|
|
12451
12088
|
import {
|
|
12452
|
-
isLoopbackHost as isLoopbackHost2
|
|
12089
|
+
isLoopbackHost as isLoopbackHost2,
|
|
12090
|
+
printRunText as printRunText5
|
|
12453
12091
|
} from "@vostride/agent-qa-core";
|
|
12454
12092
|
var MIME_TYPES = {
|
|
12455
12093
|
".html": "text/html; charset=utf-8",
|
|
@@ -12469,9 +12107,10 @@ var MIME_TYPES = {
|
|
|
12469
12107
|
".webp": "image/webp"
|
|
12470
12108
|
};
|
|
12471
12109
|
var DashboardRemoteBindRefusedError = class extends Error {
|
|
12472
|
-
constructor(host) {
|
|
12110
|
+
constructor(host, port) {
|
|
12111
|
+
const p = String(port === 0 ? 3470 : port);
|
|
12473
12112
|
super(
|
|
12474
|
-
`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})
|
|
12113
|
+
`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}/.`
|
|
12475
12114
|
);
|
|
12476
12115
|
this.host = host;
|
|
12477
12116
|
this.name = "DashboardRemoteBindRefusedError";
|
|
@@ -12483,7 +12122,7 @@ async function startServer(opts) {
|
|
|
12483
12122
|
const runServiceAttach = opts.runServiceAttach ?? runService?.attach;
|
|
12484
12123
|
const host = opts.host?.trim() || DEFAULT_DASHBOARD_HOST;
|
|
12485
12124
|
if (!isLoopbackHost2(host)) {
|
|
12486
|
-
throw new DashboardRemoteBindRefusedError(host);
|
|
12125
|
+
throw new DashboardRemoteBindRefusedError(host, port);
|
|
12487
12126
|
}
|
|
12488
12127
|
const resolvedConfigPath = configPath ?? resolve6("agent-qa.config.yaml");
|
|
12489
12128
|
const configManager = new ConfigManager(resolvedConfigPath, opts.declaredSecretMask);
|
|
@@ -12518,7 +12157,7 @@ async function startServer(opts) {
|
|
|
12518
12157
|
failureSummary = "Test timed out -- process was killed";
|
|
12519
12158
|
} else if (details.refusedBeforeStart) {
|
|
12520
12159
|
dbStatus = "failed";
|
|
12521
|
-
refusalProse = details.
|
|
12160
|
+
refusalProse = details.reason;
|
|
12522
12161
|
failureSummary = refusalProse ? `Refused before the run started: ${refusalProse}`.slice(0, 300) : "Refused before the run started";
|
|
12523
12162
|
} else if (status === "completed") {
|
|
12524
12163
|
dbStatus = "failed";
|
|
@@ -12599,7 +12238,10 @@ async function startServer(opts) {
|
|
|
12599
12238
|
mobileRuns.set(run.id, platform);
|
|
12600
12239
|
} catch (err) {
|
|
12601
12240
|
const message = err instanceof Error ? err.message : String(err);
|
|
12602
|
-
|
|
12241
|
+
printRunText5(
|
|
12242
|
+
`Failed to start Appium for run ${run.id}: ${message}`,
|
|
12243
|
+
(text) => console.error(text)
|
|
12244
|
+
);
|
|
12603
12245
|
db.updateRun(run.id, {
|
|
12604
12246
|
status: "failed",
|
|
12605
12247
|
failureSummary: `Appium server failed to start: ${message}`,
|
|
@@ -12619,19 +12261,9 @@ async function startServer(opts) {
|
|
|
12619
12261
|
runId: run.id,
|
|
12620
12262
|
args,
|
|
12621
12263
|
source: meta.isSuite ? "suite" : "dashboard",
|
|
12622
|
-
// 2026-10-01 (432-14, D-14): no confirm scope is read off the row any more. The browser that
|
|
12623
|
-
// triggered a run is not asked anything, so nothing about it has to survive the wait.
|
|
12624
12264
|
attributes: run.attributes,
|
|
12625
|
-
//
|
|
12626
|
-
// `in-process-backend.ts:211` reads `opts.timeout ?? defaultTimeout` and validates AFTER, so
|
|
12627
|
-
// any non-nullish nonsense here skips the default and leaves the run with no reclamation
|
|
12628
|
-
// timer at all. An unusable value falls back to the backend's default rather than a number
|
|
12629
|
-
// this call invented.
|
|
12265
|
+
// The backend takes `opts.timeout ?? defaultTimeout`, so an unusable value falls back here.
|
|
12630
12266
|
timeout: sanitizeQueuedExecutionTimeout(meta.timeout),
|
|
12631
|
-
// THE LEASE ABOVE IS UNCHANGED; ONLY ITS DELIVERY IS. `acquireLease` still runs where it ran,
|
|
12632
|
-
// still keys on this run id and still owns the bundled Appium's lifecycle (design-of-record).
|
|
12633
|
-
// What used to ride `AGENT_QA_APPIUM_URL` in a child's environment now travels as an
|
|
12634
|
-
// argument, because there is no child to carry an environment.
|
|
12635
12267
|
...isMobile ? { appiumUrl: appiumManager.getUrl() } : {}
|
|
12636
12268
|
});
|
|
12637
12269
|
});
|
|
@@ -12642,16 +12274,14 @@ async function startServer(opts) {
|
|
|
12642
12274
|
appiumManager,
|
|
12643
12275
|
configManager,
|
|
12644
12276
|
configPath: resolvedConfigPath,
|
|
12645
|
-
//
|
|
12277
|
+
// Each created session registers here, which is what the frame socket attaches.
|
|
12646
12278
|
...liveEditorSubjects === void 0 ? {} : { liveEditorSubjects }
|
|
12647
12279
|
});
|
|
12648
12280
|
const servesLiveView = liveView !== void 0 && (runServiceAttach !== void 0 || liveEditorSubjects !== void 0);
|
|
12649
12281
|
const apiRouter = createRouter({
|
|
12650
12282
|
db,
|
|
12651
12283
|
workspacePaths,
|
|
12652
|
-
//
|
|
12653
|
-
// dependency is an ABSENT field, never a present `undefined`, so the router's own 503 arm is
|
|
12654
|
-
// what decides, in one place.
|
|
12284
|
+
// Absent dependencies are absent fields, so the router's own 503 arm decides in one place.
|
|
12655
12285
|
...executionBackend === void 0 ? {} : { executionBackend },
|
|
12656
12286
|
jobQueue,
|
|
12657
12287
|
testFileManager,
|
|
@@ -12662,27 +12292,13 @@ async function startServer(opts) {
|
|
|
12662
12292
|
llmConfig,
|
|
12663
12293
|
authFetch,
|
|
12664
12294
|
sessionManager,
|
|
12665
|
-
//
|
|
12666
|
-
// never a present `undefined` — the router's own 501 arm is what decides, in one place.
|
|
12295
|
+
// Absent dependencies are absent fields, so the router's own 501 arm decides in one place.
|
|
12667
12296
|
...agentHost === void 0 ? {} : { agentHost },
|
|
12668
|
-
// 402-05 (D-15): the same conditional-spread house rule. Absent means a legacy mount rather
|
|
12669
|
-
// than an unconfigured service, and the router's own 503 arm is what decides, in one place.
|
|
12670
12297
|
...runServiceReadiness === void 0 ? {} : { runServiceReadiness },
|
|
12671
|
-
//
|
|
12672
|
-
// composed again. That derivation is the "one session registry serves both doors" claim in its
|
|
12673
|
-
// structural form — there is no second wiring a composition root could forget or point
|
|
12674
|
-
// somewhere else, and the two doors cannot end up attached to different registries. Absent
|
|
12675
|
-
// `runService` means no registry exists at all, so the same conditional-spread house rule
|
|
12676
|
-
// applies and the router's own 503 arm decides.
|
|
12298
|
+
// Read off `runService` by default, so both doors attach to one session registry.
|
|
12677
12299
|
...runServiceAttach === void 0 ? {} : { runServiceAttach },
|
|
12678
|
-
// 403 D-03: the mint route's port, on the same conditional-spread house rule — absent means
|
|
12679
|
-
// this host serves no live view and the route's own 503 arm decides, in one place.
|
|
12680
12300
|
...liveView === void 0 ? {} : { mintLiveUrl: liveView.mintLiveUrl },
|
|
12681
|
-
// 428 D-07: the editor mint, on the same house rule — absent means the editor mint route's own
|
|
12682
|
-
// 503 decides.
|
|
12683
12301
|
...liveView?.mintEditorLiveUrl === void 0 ? {} : { mintEditorLiveUrl: liveView.mintEditorLiveUrl },
|
|
12684
|
-
// 404-05 (D-16): the same conditional-spread house rule — absent means this host serves no
|
|
12685
|
-
// post-processing push channel and the stream arm's own 503 decides, in one place.
|
|
12686
12302
|
...postProcessingRelay === void 0 ? {} : { postProcessingRelay }
|
|
12687
12303
|
});
|
|
12688
12304
|
const runServiceRouter = runService === void 0 ? void 0 : createRunServiceRoutes(runService);
|
|
@@ -12702,13 +12318,15 @@ async function startServer(opts) {
|
|
|
12702
12318
|
apiRouter(req, res);
|
|
12703
12319
|
return;
|
|
12704
12320
|
}
|
|
12321
|
+
if (refuseNonLoopbackHost(req) !== void 0) {
|
|
12322
|
+
const address = server.address();
|
|
12323
|
+
const boundPort = typeof address === "object" && address !== null ? address.port : port;
|
|
12324
|
+
refuseForeignHostPage(res, boundPort);
|
|
12325
|
+
return;
|
|
12326
|
+
}
|
|
12705
12327
|
if (req.method === "OPTIONS") {
|
|
12706
|
-
res.writeHead(
|
|
12707
|
-
|
|
12708
|
-
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
12709
|
-
"Access-Control-Allow-Headers": "Content-Type"
|
|
12710
|
-
});
|
|
12711
|
-
res.end();
|
|
12328
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
12329
|
+
res.end("Not Found");
|
|
12712
12330
|
return;
|
|
12713
12331
|
}
|
|
12714
12332
|
if (req.method === "GET" && path.startsWith(LIVE_EMBED_PATH_PREFIX)) {
|
|
@@ -12743,17 +12361,16 @@ async function startServer(opts) {
|
|
|
12743
12361
|
return;
|
|
12744
12362
|
}
|
|
12745
12363
|
if (uiDir) {
|
|
12746
|
-
const filePath = path === "/" ?
|
|
12364
|
+
const filePath = path === "/" ? join4(uiDir, "index.html") : join4(uiDir, path);
|
|
12747
12365
|
try {
|
|
12748
|
-
const fileStat = await
|
|
12366
|
+
const fileStat = await stat6(filePath);
|
|
12749
12367
|
if (fileStat.isFile()) {
|
|
12750
12368
|
const content = await readFile8(filePath);
|
|
12751
12369
|
const ext = extname(filePath);
|
|
12752
12370
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
12753
12371
|
res.writeHead(200, {
|
|
12754
12372
|
"Content-Type": contentType,
|
|
12755
|
-
"Content-Length": content.length
|
|
12756
|
-
"Access-Control-Allow-Origin": "*"
|
|
12373
|
+
"Content-Length": content.length
|
|
12757
12374
|
});
|
|
12758
12375
|
res.end(content);
|
|
12759
12376
|
return;
|
|
@@ -12761,12 +12378,11 @@ async function startServer(opts) {
|
|
|
12761
12378
|
} catch {
|
|
12762
12379
|
}
|
|
12763
12380
|
try {
|
|
12764
|
-
const indexPath =
|
|
12381
|
+
const indexPath = join4(uiDir, "index.html");
|
|
12765
12382
|
const content = await readFile8(indexPath);
|
|
12766
12383
|
res.writeHead(200, {
|
|
12767
12384
|
"Content-Type": "text/html",
|
|
12768
|
-
"Content-Length": content.length
|
|
12769
|
-
"Access-Control-Allow-Origin": "*"
|
|
12385
|
+
"Content-Length": content.length
|
|
12770
12386
|
});
|
|
12771
12387
|
res.end(content);
|
|
12772
12388
|
return;
|
|
@@ -12851,7 +12467,8 @@ async function startServer(opts) {
|
|
|
12851
12467
|
session.clearIdleTimer();
|
|
12852
12468
|
const handler = createSessionHandler(session, () => {
|
|
12853
12469
|
void sessionManager.terminateSession(sessionId).catch((error) => {
|
|
12854
|
-
|
|
12470
|
+
const line = `[live-editor] could not end session ${sessionId}`;
|
|
12471
|
+
printRunText5(format3(line, error), () => console.error(line, error));
|
|
12855
12472
|
});
|
|
12856
12473
|
}, (event) => {
|
|
12857
12474
|
db.insertTokenEvent(event);
|
|
@@ -12941,9 +12558,6 @@ var InProcessExecutionBackend = class extends EventEmitter2 {
|
|
|
12941
12558
|
this.options = options;
|
|
12942
12559
|
this.retentionMs = options.retentionMs ?? DEFAULT_HANDLE_RETENTION_MS;
|
|
12943
12560
|
}
|
|
12944
|
-
async preflight(opts) {
|
|
12945
|
-
await this.options.preflightRun?.(opts);
|
|
12946
|
-
}
|
|
12947
12561
|
execute(opts) {
|
|
12948
12562
|
const handle = {
|
|
12949
12563
|
runId: opts.runId,
|
|
@@ -13019,9 +12633,7 @@ var InProcessExecutionBackend = class extends EventEmitter2 {
|
|
|
13019
12633
|
settle(handle, error) {
|
|
13020
12634
|
if (handle.timeoutTimer) clearTimeout(handle.timeoutTimer);
|
|
13021
12635
|
let refusedBeforeStart = false;
|
|
13022
|
-
let neutralReason;
|
|
13023
12636
|
if (error !== void 0) {
|
|
13024
|
-
neutralReason = dashboardNeutralReasonOf(error);
|
|
13025
12637
|
const message = error instanceof Error ? error.message : String(error);
|
|
13026
12638
|
for (const line of message.split("\n")) handle.output.push(line);
|
|
13027
12639
|
refusedBeforeStart = handle.session === void 0;
|
|
@@ -13035,7 +12647,6 @@ var InProcessExecutionBackend = class extends EventEmitter2 {
|
|
|
13035
12647
|
const details = {
|
|
13036
12648
|
...handle.completionReason === void 0 ? {} : { reason: handle.completionReason },
|
|
13037
12649
|
...refusedBeforeStart ? { refusedBeforeStart: true } : {},
|
|
13038
|
-
...neutralReason === void 0 ? {} : { neutralReason },
|
|
13039
12650
|
output: [...handle.output]
|
|
13040
12651
|
};
|
|
13041
12652
|
this.emit("run-settled", handle.runId, handle.status, details);
|
|
@@ -13045,12 +12656,6 @@ var InProcessExecutionBackend = class extends EventEmitter2 {
|
|
|
13045
12656
|
cleanup.unref();
|
|
13046
12657
|
}
|
|
13047
12658
|
};
|
|
13048
|
-
function dashboardNeutralReasonOf(error) {
|
|
13049
|
-
if (typeof error !== "object" || error === null) return void 0;
|
|
13050
|
-
const carried = error.neutralReason;
|
|
13051
|
-
if (typeof carried !== "string" || carried.trim().length === 0) return void 0;
|
|
13052
|
-
return carried;
|
|
13053
|
-
}
|
|
13054
12659
|
function createInProcessExecutionBackend(options) {
|
|
13055
12660
|
return new InProcessExecutionBackend(options);
|
|
13056
12661
|
}
|
|
@@ -13185,7 +12790,6 @@ export {
|
|
|
13185
12790
|
renderLiveEmbedPage,
|
|
13186
12791
|
resolveDashboardDbPath,
|
|
13187
12792
|
resolveRunOwnerHost,
|
|
13188
|
-
runDenialOf,
|
|
13189
12793
|
runRowIdentity,
|
|
13190
12794
|
serializeConfigWrite,
|
|
13191
12795
|
servePostProcessingRead,
|