@vostride/agent-qa-dashboard 0.1.21 → 0.2.1
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 +2094 -0
- package/dist/index-Ds4Lv4mT.d.ts +2094 -0
- package/dist/index.cjs +10276 -7913
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +546 -83
- package/dist/index.d.ts +546 -83
- package/dist/index.js +10280 -7912
- package/dist/index.js.map +1 -1
- package/dist/server/index.cjs +8508 -6180
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +4 -4
- package/dist/server/index.d.ts +4 -4
- package/dist/server/index.js +8614 -6228
- package/dist/server/index.js.map +1 -1
- package/package.json +16 -13
- package/dist/index-ygQLgXzT.d.cts +0 -1168
- package/dist/index-ygQLgXzT.d.ts +0 -1168
|
@@ -0,0 +1,2094 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse, Server } from 'node:http';
|
|
2
|
+
import { RunAttributes, ResolvedWorkspacePaths, ResolvedAppiumExecutable, HookDefinition, AuthStateMetadata } from '@vostride/agent-qa-core';
|
|
3
|
+
import { RunDetailMember, RunDetailStep, RunDetailSubAction, RunDetailRows, RunStatus, RunArtifactKind, RunArtifactStageInput, RunArtifactPayload, RunArtifactFinalizeInput, LiveStreamDeclaration, LiveFrame, LiveFrameEndReason, LiveFrameSubscription, FunctionCaller, RunEventEnvelope, RunStartAcknowledgement, RunSession, RunServiceReadiness, RunsLiveUrlResult, RunServiceDenialBody, MaskFn, LiveViewSubjectRef, LiveUrlVerdict, EditorLiveUrlResult, ConfigWriteIssue, ConfigChange, ConfigPathSegment, ConfigWriteRefusal, PlannerLimits } from '@vostride/agent-qa-schema';
|
|
4
|
+
import { EventEmitter } from 'node:events';
|
|
5
|
+
import { SecretStore, SecretRedactor, ConsoleLogEntry, NetworkLogEntry, StepAnnotation, StepPhaseEvent, StepResult } from '@vostride/agent-qa-runner';
|
|
6
|
+
import { Node } from 'yaml';
|
|
7
|
+
|
|
8
|
+
interface RunOwner {
|
|
9
|
+
readonly pid: number;
|
|
10
|
+
readonly incarnation: string | null;
|
|
11
|
+
readonly host: string;
|
|
12
|
+
}
|
|
13
|
+
interface RunOwnerProbe {
|
|
14
|
+
isAlive(pid: number): boolean;
|
|
15
|
+
incarnationOf(pid: number): string | undefined;
|
|
16
|
+
}
|
|
17
|
+
interface RunOwnership {
|
|
18
|
+
readonly self: RunOwner;
|
|
19
|
+
readonly probe: RunOwnerProbe;
|
|
20
|
+
}
|
|
21
|
+
interface RunOwnershipInput {
|
|
22
|
+
readonly self: Omit<RunOwner, 'host'> & {
|
|
23
|
+
readonly host?: string;
|
|
24
|
+
};
|
|
25
|
+
readonly probe: RunOwnerProbe;
|
|
26
|
+
}
|
|
27
|
+
declare function completeRunOwnership(input: RunOwnershipInput): RunOwnership;
|
|
28
|
+
type RunOwnerState = 'self' | 'alive' | 'gone' | 'unknown';
|
|
29
|
+
declare function classifyRunOwner(owner: RunOwner, ownership: RunOwnership): RunOwnerState;
|
|
30
|
+
declare const orphanSummary: (pid: number) => string;
|
|
31
|
+
declare const ownedElsewhere: (pid: number) => string;
|
|
32
|
+
declare function defaultRunOwnership(): RunOwnership;
|
|
33
|
+
|
|
34
|
+
interface RunRow {
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
filePath: string | null;
|
|
38
|
+
status: string;
|
|
39
|
+
priority: number;
|
|
40
|
+
duration: number;
|
|
41
|
+
attributes: RunAttributes;
|
|
42
|
+
environment: string | null;
|
|
43
|
+
metadata: Record<string, unknown> | null;
|
|
44
|
+
startedAt: string | null;
|
|
45
|
+
endedAt: string | null;
|
|
46
|
+
failureSummary: string | null;
|
|
47
|
+
errorLog: string | null;
|
|
48
|
+
memoryLog: string | null;
|
|
49
|
+
testId: string | null;
|
|
50
|
+
suiteId: string | null;
|
|
51
|
+
platform: string;
|
|
52
|
+
testFileContent: string | null;
|
|
53
|
+
modelName: string | null;
|
|
54
|
+
llmProvider: string | null;
|
|
55
|
+
parallel: boolean;
|
|
56
|
+
createdAt: string;
|
|
57
|
+
members?: RunMemberSummary[];
|
|
58
|
+
memberOutcome?: string | null;
|
|
59
|
+
}
|
|
60
|
+
interface TestRef {
|
|
61
|
+
testId?: string;
|
|
62
|
+
filePath?: string;
|
|
63
|
+
}
|
|
64
|
+
interface RunMemberSummary {
|
|
65
|
+
testIndex: number;
|
|
66
|
+
testId: string | null;
|
|
67
|
+
filePath: string | null;
|
|
68
|
+
suiteId: string | null;
|
|
69
|
+
name: string | null;
|
|
70
|
+
outcome: string | null;
|
|
71
|
+
}
|
|
72
|
+
interface RunArtifactRow {
|
|
73
|
+
runId: string;
|
|
74
|
+
kind: RunArtifactKind;
|
|
75
|
+
schemaVersion: number;
|
|
76
|
+
payload: RunArtifactPayload;
|
|
77
|
+
finalizedAt: string | null;
|
|
78
|
+
createdAt: string;
|
|
79
|
+
updatedAt: string;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* ONE RUN'S ARTIFACT, AND NOTHING ELSE (405-07, D-02).
|
|
83
|
+
*
|
|
84
|
+
* `children` stood here and is gone. It walked the run row's now-deleted parent link for the child
|
|
85
|
+
* rows of a suite parent — a shape nothing has written since 402-09 put ONE `runs` row per run and moved member
|
|
86
|
+
* identity onto `run_members`. A structurally-always-empty array on a response is worse than an
|
|
87
|
+
* absent one: a reader cannot tell "this run has no children" from "children stopped being a fact
|
|
88
|
+
* this product records", so the page renders an empty section forever and nobody notices.
|
|
89
|
+
*/
|
|
90
|
+
interface RunArtifactBundle {
|
|
91
|
+
artifact: RunArtifactRow | null;
|
|
92
|
+
}
|
|
93
|
+
interface ExecutionLogRow {
|
|
94
|
+
id: string;
|
|
95
|
+
runId: string;
|
|
96
|
+
stepId: string | null;
|
|
97
|
+
/**
|
|
98
|
+
* 405 D-01: where in the run tree this hook ran, from `HookFinishedPayload`'s own optional
|
|
99
|
+
* fields. `null` means a RUN-SCOPED hook — a real state, not a missing value.
|
|
100
|
+
*/
|
|
101
|
+
testIndex: number | null;
|
|
102
|
+
stepIndex: number | null;
|
|
103
|
+
type: 'hook' | 'appium-script' | 'runjs';
|
|
104
|
+
name: string;
|
|
105
|
+
hookId: string | null;
|
|
106
|
+
phase: 'setup' | 'teardown' | 'inline';
|
|
107
|
+
status: 'passed' | 'failed';
|
|
108
|
+
duration: number;
|
|
109
|
+
stdout: string | null;
|
|
110
|
+
stderr: string | null;
|
|
111
|
+
returnData: unknown | null;
|
|
112
|
+
variables: Record<string, string> | null;
|
|
113
|
+
createdAt: string;
|
|
114
|
+
}
|
|
115
|
+
interface InsertExecutionLogInput {
|
|
116
|
+
id: string;
|
|
117
|
+
runId: string;
|
|
118
|
+
stepId?: string | null;
|
|
119
|
+
/** 405 D-01: the handle and step this hook belongs to; absent means run-scoped. */
|
|
120
|
+
testIndex?: number | null;
|
|
121
|
+
stepIndex?: number | null;
|
|
122
|
+
type: 'hook' | 'appium-script' | 'runjs';
|
|
123
|
+
name: string;
|
|
124
|
+
hookId?: string | null;
|
|
125
|
+
phase: 'setup' | 'teardown' | 'inline';
|
|
126
|
+
status: 'passed' | 'failed';
|
|
127
|
+
duration: number;
|
|
128
|
+
stdout?: string | null;
|
|
129
|
+
stderr?: string | null;
|
|
130
|
+
returnData?: unknown | null;
|
|
131
|
+
variables?: Record<string, string> | null;
|
|
132
|
+
}
|
|
133
|
+
interface RunsFilter {
|
|
134
|
+
statuses?: readonly string[];
|
|
135
|
+
name?: string;
|
|
136
|
+
platform?: string;
|
|
137
|
+
from?: string;
|
|
138
|
+
to?: string;
|
|
139
|
+
attributePredicates?: AttributePredicate[];
|
|
140
|
+
limit?: number;
|
|
141
|
+
offset?: number;
|
|
142
|
+
}
|
|
143
|
+
interface StatsOptions {
|
|
144
|
+
from?: string;
|
|
145
|
+
to?: string;
|
|
146
|
+
attributePredicates?: AttributePredicate[];
|
|
147
|
+
}
|
|
148
|
+
type InsightsBreakdownDimension = 'test' | 'suite' | 'platform';
|
|
149
|
+
interface InsightsBreakdownRow {
|
|
150
|
+
key: string;
|
|
151
|
+
label: string;
|
|
152
|
+
runs: number;
|
|
153
|
+
passRate: number | null;
|
|
154
|
+
flakeRate: number;
|
|
155
|
+
avgDuration: number;
|
|
156
|
+
passed: number;
|
|
157
|
+
failed: number;
|
|
158
|
+
filePath?: string;
|
|
159
|
+
suiteId?: string;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* What a run deletion removed (405-07, D-14 · T-405-27).
|
|
163
|
+
*
|
|
164
|
+
* ROWS ONLY. The two path arrays that stood here fed the router's filesystem cleanup, and both the
|
|
165
|
+
* cleanup and the columns it read are gone: refs are the only artifact addressing now, and the run
|
|
166
|
+
* directory under `.agent-qa/runs/` is the run's own record — the event log and the blobs — which
|
|
167
|
+
* the dashboard never owned and must not delete. `deletedRunIds` survives as a one-element list
|
|
168
|
+
* rather than collapsing to a boolean, because it is the shape the response already publishes.
|
|
169
|
+
*/
|
|
170
|
+
interface DeleteRunResult {
|
|
171
|
+
deleted: boolean;
|
|
172
|
+
deletedRunIds: string[];
|
|
173
|
+
}
|
|
174
|
+
interface AttributePredicate {
|
|
175
|
+
key: string;
|
|
176
|
+
value: string;
|
|
177
|
+
mode: 'exact' | 'regex';
|
|
178
|
+
}
|
|
179
|
+
interface AttributeKeySuggestion {
|
|
180
|
+
key: string;
|
|
181
|
+
count: number;
|
|
182
|
+
}
|
|
183
|
+
interface AttributeValueSuggestion {
|
|
184
|
+
value: string;
|
|
185
|
+
count: number;
|
|
186
|
+
}
|
|
187
|
+
declare class UnsupportedUserVersionError extends Error {
|
|
188
|
+
readonly version: number;
|
|
189
|
+
readonly schemaVersion: number;
|
|
190
|
+
constructor(version: number, schemaVersion: number);
|
|
191
|
+
}
|
|
192
|
+
declare class RunsDatabaseVersionRefusedError extends Error {
|
|
193
|
+
readonly dbPath: string;
|
|
194
|
+
readonly version: number;
|
|
195
|
+
constructor(dbPath: string, refusal: UnsupportedUserVersionError);
|
|
196
|
+
}
|
|
197
|
+
declare class DashboardDatabase {
|
|
198
|
+
private db;
|
|
199
|
+
readonly runOwnership: RunOwnership;
|
|
200
|
+
readonly dbPath: string;
|
|
201
|
+
private readonly ownStamps;
|
|
202
|
+
constructor({ dbPath, runOwnership, }?: {
|
|
203
|
+
dbPath?: string;
|
|
204
|
+
runOwnership?: RunOwnershipInput;
|
|
205
|
+
});
|
|
206
|
+
private runMigrations;
|
|
207
|
+
insertRun(run: {
|
|
208
|
+
id?: string;
|
|
209
|
+
name: string;
|
|
210
|
+
filePath?: string;
|
|
211
|
+
status: string;
|
|
212
|
+
duration: number;
|
|
213
|
+
attributes?: RunAttributes;
|
|
214
|
+
environment?: string;
|
|
215
|
+
metadata?: Record<string, unknown>;
|
|
216
|
+
startedAt: string;
|
|
217
|
+
endedAt: string;
|
|
218
|
+
failureSummary?: string;
|
|
219
|
+
platform?: string;
|
|
220
|
+
testFileContent?: string;
|
|
221
|
+
modelName?: string;
|
|
222
|
+
llmProvider?: string;
|
|
223
|
+
testId?: string;
|
|
224
|
+
suiteId?: string;
|
|
225
|
+
}): string;
|
|
226
|
+
claimRunningRow(id: string, fields: {
|
|
227
|
+
name?: string;
|
|
228
|
+
filePath?: string;
|
|
229
|
+
attributes?: RunAttributes;
|
|
230
|
+
startedAt: string;
|
|
231
|
+
platform?: string;
|
|
232
|
+
testId?: string;
|
|
233
|
+
suiteId?: string;
|
|
234
|
+
}): boolean;
|
|
235
|
+
updateRun(id: string, updates: {
|
|
236
|
+
name?: string;
|
|
237
|
+
status?: string;
|
|
238
|
+
duration?: number;
|
|
239
|
+
startedAt?: string;
|
|
240
|
+
endedAt?: string;
|
|
241
|
+
failureSummary?: string;
|
|
242
|
+
errorLog?: string;
|
|
243
|
+
memoryLog?: string;
|
|
244
|
+
attributes?: RunAttributes;
|
|
245
|
+
platform?: string;
|
|
246
|
+
testId?: string;
|
|
247
|
+
suiteId?: string;
|
|
248
|
+
}): void;
|
|
249
|
+
updateRunMetadata(id: string, updates: {
|
|
250
|
+
attributes?: RunAttributes;
|
|
251
|
+
metadata?: Record<string, unknown>;
|
|
252
|
+
platform?: string;
|
|
253
|
+
testFileContent?: string;
|
|
254
|
+
modelName?: string;
|
|
255
|
+
llmProvider?: string;
|
|
256
|
+
}): void;
|
|
257
|
+
insertPendingRun(run: {
|
|
258
|
+
id?: string;
|
|
259
|
+
name: string;
|
|
260
|
+
filePath?: string;
|
|
261
|
+
attributes?: RunAttributes;
|
|
262
|
+
priority?: number;
|
|
263
|
+
platform?: string;
|
|
264
|
+
testFileContent?: string;
|
|
265
|
+
modelName?: string;
|
|
266
|
+
llmProvider?: string;
|
|
267
|
+
metadata?: Record<string, unknown>;
|
|
268
|
+
parallel?: boolean;
|
|
269
|
+
}): string;
|
|
270
|
+
getNextPendingRun(): RunRow | undefined;
|
|
271
|
+
getPendingRuns(): RunRow[];
|
|
272
|
+
updateRunStatus(id: string, status: string): boolean;
|
|
273
|
+
/**
|
|
274
|
+
* Write or refresh ONE member row, keyed by the handle `(run_id, test_index)`.
|
|
275
|
+
*
|
|
276
|
+
* AN UPSERT AND NOT TWO WRITES, because the member row is fed by two events at two times:
|
|
277
|
+
* `testStarted` knows the identity fields and `testFinished` knows the outcome — and D-18
|
|
278
|
+
* requires a member whose stream begins with a terminal event to render anyway, so the second
|
|
279
|
+
* one alone has to be able to create the row.
|
|
280
|
+
*
|
|
281
|
+
* ONLY SUPPLIED FIELDS MOVE. `COALESCE(excluded.x, run_members.x)` on every optional column, so
|
|
282
|
+
* a `testFinished` that carries no name cannot blank the name `testStarted` wrote. `attempts` is
|
|
283
|
+
* excluded from the update entirely: it is the D-17 retry ordinal, owned by
|
|
284
|
+
* `incrementRunMemberAttempts`, and an upsert that reset it would erase the fact the header
|
|
285
|
+
* renders.
|
|
286
|
+
*/
|
|
287
|
+
upsertRunMember(member: {
|
|
288
|
+
runId: string;
|
|
289
|
+
testIndex: number;
|
|
290
|
+
testId?: string;
|
|
291
|
+
name?: string;
|
|
292
|
+
filePath?: string;
|
|
293
|
+
suiteId?: string;
|
|
294
|
+
suiteName?: string;
|
|
295
|
+
suiteIndex?: number;
|
|
296
|
+
totalSteps?: number;
|
|
297
|
+
outcome?: string;
|
|
298
|
+
durationMs?: number;
|
|
299
|
+
failureSummary?: string;
|
|
300
|
+
retryCount?: number;
|
|
301
|
+
recording?: RunDetailMember['recording'];
|
|
302
|
+
}): void;
|
|
303
|
+
/**
|
|
304
|
+
* Raise ONE member's attempt ordinal (D-17). A no-op when the member row does not exist yet —
|
|
305
|
+
* a retry scheduled before any identity event is a real ordering, and inventing a row here
|
|
306
|
+
* would put a member on the page that nothing has said anything about.
|
|
307
|
+
*/
|
|
308
|
+
incrementRunMemberAttempts(runId: string, testIndex: number): void;
|
|
309
|
+
/**
|
|
310
|
+
* Drop ONE HANDLE's projected step and sub-action rows (D-17).
|
|
311
|
+
*
|
|
312
|
+
* SCOPED TO THE HANDLE, which was the whole change from the v1 run-wide step delete this
|
|
313
|
+
* replaced (deleted outright by 405-07's census): that member wiped every step of the RUN on any
|
|
314
|
+
* member's retry, so in a suite the abandoned attempt of test 3 deleted the completed history of
|
|
315
|
+
* tests 0-2. The member row itself survives — its outcome and its attempt count are facts about
|
|
316
|
+
* the member, not about the attempt. So do its setup and teardown hook rows, which run once per
|
|
317
|
+
* member outside the retry loop; only the attempt's `inline` hook rows go (433-41, CR-01).
|
|
318
|
+
*/
|
|
319
|
+
deleteMemberProjection(runId: string, testIndex: number): void;
|
|
320
|
+
/**
|
|
321
|
+
* Write ONE finalized step row. `INSERT OR REPLACE` because a re-run of the same handle re-emits
|
|
322
|
+
* the same `(test_index, step_index)` and the LAST attempt is the one the page renders (D-17).
|
|
323
|
+
*
|
|
324
|
+
* The three token columns stay NULL when `tokenUsage` is absent — never 0 (D-05).
|
|
325
|
+
*/
|
|
326
|
+
insertRunStep(step: {
|
|
327
|
+
runId: string;
|
|
328
|
+
testIndex: number;
|
|
329
|
+
stepIndex: number;
|
|
330
|
+
stepText?: string;
|
|
331
|
+
cacheStatus: string;
|
|
332
|
+
verifierVerdict: unknown;
|
|
333
|
+
selfHealOccurred: boolean;
|
|
334
|
+
briefDigest: string;
|
|
335
|
+
status?: string;
|
|
336
|
+
durationMs?: number;
|
|
337
|
+
error?: string;
|
|
338
|
+
tokenUsage?: RunDetailStep['tokenUsage'];
|
|
339
|
+
modelUsage?: RunDetailStep['modelUsage'];
|
|
340
|
+
evidence?: RunDetailStep['evidence'];
|
|
341
|
+
variableSnapshot?: RunDetailStep['variableSnapshot'];
|
|
342
|
+
}): void;
|
|
343
|
+
/**
|
|
344
|
+
* Write ONE sub-action row. The four nested payload fields are stringified VERBATIM — this
|
|
345
|
+
* method never reaches inside `plannedAction`, `grounding`, `result` or `artifactRefs`.
|
|
346
|
+
*/
|
|
347
|
+
insertRunSubAction(subAction: {
|
|
348
|
+
runId: string;
|
|
349
|
+
testIndex: number;
|
|
350
|
+
stepIndex: number;
|
|
351
|
+
subActionIndex: number;
|
|
352
|
+
plan: string;
|
|
353
|
+
plannedAction: unknown;
|
|
354
|
+
grounding?: unknown;
|
|
355
|
+
result: unknown;
|
|
356
|
+
screenStateHash: string;
|
|
357
|
+
screenStateHashAfter?: string;
|
|
358
|
+
artifactRefs?: readonly unknown[];
|
|
359
|
+
ariaEvidence?: RunDetailSubAction['ariaEvidence'];
|
|
360
|
+
recordingOffsetMs?: RunDetailSubAction['recordingOffsetMs'];
|
|
361
|
+
}): void;
|
|
362
|
+
/**
|
|
363
|
+
* THE run-detail read (D-03). `undefined` when no `runs` row exists — absence is a RESULT here,
|
|
364
|
+
* and turning it into a typed refusal is the `runs.detail` op's job, so the "not found"
|
|
365
|
+
* vocabulary lives in the function layer rather than in this store.
|
|
366
|
+
*
|
|
367
|
+
* A run that exists and projected nothing answers three empty arrays, which is a DIFFERENT fact
|
|
368
|
+
* and stays a different answer.
|
|
369
|
+
*/
|
|
370
|
+
readRunDetailRows(runId: string): RunDetailRows | undefined;
|
|
371
|
+
private appendAttributePredicateConditions;
|
|
372
|
+
private applyRegexAttributePredicates;
|
|
373
|
+
private queryRunsWithAttributePredicates;
|
|
374
|
+
private buildTrendsFromRuns;
|
|
375
|
+
getRuns(opts?: RunsFilter): RunRow[];
|
|
376
|
+
private withMemberSummaries;
|
|
377
|
+
readRunOwner(id: string): RunOwner | undefined;
|
|
378
|
+
listRunningRunOwners(): Array<{
|
|
379
|
+
id: string;
|
|
380
|
+
startedAt?: string;
|
|
381
|
+
owner: RunOwner;
|
|
382
|
+
}>;
|
|
383
|
+
finalizeOwnRun(id: string, updates: {
|
|
384
|
+
status: RunStatus;
|
|
385
|
+
endedAt: string;
|
|
386
|
+
duration?: number;
|
|
387
|
+
failureSummary?: string;
|
|
388
|
+
}): boolean;
|
|
389
|
+
closeOwnRun(id: string, updates: {
|
|
390
|
+
status: RunStatus;
|
|
391
|
+
endedAt: string;
|
|
392
|
+
}): boolean;
|
|
393
|
+
isOwnStamp(id: string, owner: RunOwner): boolean;
|
|
394
|
+
private readOwnStamp;
|
|
395
|
+
finalizeOrphanRun(id: string, owner: RunOwner): boolean;
|
|
396
|
+
getRun(id: string): RunRow | undefined;
|
|
397
|
+
/** Read-only repair for old last-member totals. Complete measured member work is cumulative,
|
|
398
|
+
* including parallel members; it is neither recording length nor run wall-clock time.
|
|
399
|
+
* Incomplete legacy projections retain their stored fallback. Bound queries to returned rows,
|
|
400
|
+
* after pagination, with one aggregate per batch rather than a lookup for every list row.
|
|
401
|
+
*/
|
|
402
|
+
private withMemberExecutionDuration;
|
|
403
|
+
listRunAttributeKeys(opts?: {
|
|
404
|
+
limit?: number;
|
|
405
|
+
q?: string;
|
|
406
|
+
}): AttributeKeySuggestion[];
|
|
407
|
+
listRunAttributeValues(key: string, opts?: {
|
|
408
|
+
limit?: number;
|
|
409
|
+
q?: string;
|
|
410
|
+
}): AttributeValueSuggestion[];
|
|
411
|
+
/**
|
|
412
|
+
* Delete ONE run and everything the schema hangs off it (405-07, D-02/D-14 · T-405-27).
|
|
413
|
+
*
|
|
414
|
+
* ONE ID, AND THE CASCADE DOES THE REST. This used to open with a recursive `run_tree` CTE that
|
|
415
|
+
* walked the child rows of a suite parent, then hand-delete their step and trace rows in a
|
|
416
|
+
* transaction. Nothing has written a child row since 402-09 — one `runs` row per run, member
|
|
417
|
+
* identity on `run_members` — so the walk could only ever return the one row it was seeded with,
|
|
418
|
+
* and the hand-deletes named tables this schema no longer has. Every table that references a run
|
|
419
|
+
* declares `ON DELETE CASCADE`, so the single statement below is the whole deletion.
|
|
420
|
+
*
|
|
421
|
+
* AND IT TOUCHES NO FILE. The two path arrays it used to collect fed the router's artifact
|
|
422
|
+
* cleanup; both are gone. The run's directory under `.agent-qa/runs/` — its event log and its
|
|
423
|
+
* content-addressed blobs — is the run's own record and is not the dashboard's to remove.
|
|
424
|
+
*/
|
|
425
|
+
deleteRun(id: string): DeleteRunResult;
|
|
426
|
+
insertRunArtifact(input: {
|
|
427
|
+
runId: string;
|
|
428
|
+
kind?: RunArtifactKind;
|
|
429
|
+
payload?: RunArtifactStageInput;
|
|
430
|
+
}): RunArtifactRow;
|
|
431
|
+
stageRunArtifact(runId: string, patch: RunArtifactStageInput): RunArtifactRow;
|
|
432
|
+
finalizeRunArtifact(runId: string, updates?: RunArtifactFinalizeInput): RunArtifactRow;
|
|
433
|
+
getRunArtifact(runId: string): RunArtifactRow | null;
|
|
434
|
+
getRunArtifactBundle(runId: string): RunArtifactBundle;
|
|
435
|
+
/**
|
|
436
|
+
* The runs a self-heal happened on (405-07, D-02).
|
|
437
|
+
*
|
|
438
|
+
* READ OFF `run_steps.self_heal_occurred`, WHICH IS THE SURVIVING HEALING FACT. This scanned the
|
|
439
|
+
* v1 wide step table for `status = 'healed'`; that table had no producer left and its readers
|
|
440
|
+
* went with it. `selfHealOccurred` is the bus payload's own boolean, projected verbatim by the
|
|
441
|
+
* subscriber — so the flake rate below is now computed from what the runner actually reported
|
|
442
|
+
* rather than from a status word a retired projection wrote.
|
|
443
|
+
*/
|
|
444
|
+
private getHealedRunIds;
|
|
445
|
+
private summarizeAnalyticsRuns;
|
|
446
|
+
private listRunsForInsights;
|
|
447
|
+
getStats(opts?: StatsOptions): {
|
|
448
|
+
totalRuns: number;
|
|
449
|
+
passed: number;
|
|
450
|
+
failed: number;
|
|
451
|
+
flaky: number;
|
|
452
|
+
flakeRate: number;
|
|
453
|
+
avgDuration: number;
|
|
454
|
+
totalTokens: number;
|
|
455
|
+
runs: {
|
|
456
|
+
date: string;
|
|
457
|
+
passed: number;
|
|
458
|
+
failed: number;
|
|
459
|
+
healed: number;
|
|
460
|
+
duration: number;
|
|
461
|
+
}[];
|
|
462
|
+
memory: {
|
|
463
|
+
runs: number;
|
|
464
|
+
added: number;
|
|
465
|
+
confirmed: number;
|
|
466
|
+
deprecated: number;
|
|
467
|
+
deleted: number;
|
|
468
|
+
curatorTokens: number;
|
|
469
|
+
};
|
|
470
|
+
};
|
|
471
|
+
getCostStats(opts?: StatsOptions): {
|
|
472
|
+
runs: {
|
|
473
|
+
runId: string;
|
|
474
|
+
name: string;
|
|
475
|
+
startedAt: string | null;
|
|
476
|
+
promptTokens: number;
|
|
477
|
+
completionTokens: number;
|
|
478
|
+
totalTokens: number;
|
|
479
|
+
}[];
|
|
480
|
+
totals: {
|
|
481
|
+
promptTokens: number;
|
|
482
|
+
completionTokens: number;
|
|
483
|
+
totalTokens: number;
|
|
484
|
+
};
|
|
485
|
+
};
|
|
486
|
+
insertTokenEvent(event: {
|
|
487
|
+
modelName: string;
|
|
488
|
+
promptTokens: number;
|
|
489
|
+
completionTokens: number;
|
|
490
|
+
source: 'test-run' | 'live-editor';
|
|
491
|
+
}): string;
|
|
492
|
+
getTokenEventStats(opts?: {
|
|
493
|
+
from?: string;
|
|
494
|
+
to?: string;
|
|
495
|
+
}): {
|
|
496
|
+
byModel: {
|
|
497
|
+
date: string;
|
|
498
|
+
model: string;
|
|
499
|
+
promptTokens: number;
|
|
500
|
+
completionTokens: number;
|
|
501
|
+
}[];
|
|
502
|
+
bySource: Record<string, {
|
|
503
|
+
promptTokens: number;
|
|
504
|
+
completionTokens: number;
|
|
505
|
+
}>;
|
|
506
|
+
totals: {
|
|
507
|
+
promptTokens: number;
|
|
508
|
+
completionTokens: number;
|
|
509
|
+
};
|
|
510
|
+
testRunUnmeasuredSteps: number;
|
|
511
|
+
};
|
|
512
|
+
/**
|
|
513
|
+
* The window's settled run ids, newest first, capped at `limit`, with their full count, and the
|
|
514
|
+
* count of the window's runs still in flight (433-30, 433-42). A settled run has started and is
|
|
515
|
+
* neither `pending` nor `running`; every other row in the window is in flight. Ids only: the
|
|
516
|
+
* Insights knowledge-change read dispatches each through the agent host.
|
|
517
|
+
*/
|
|
518
|
+
listInsightsWindowRunIds(opts: {
|
|
519
|
+
from?: string;
|
|
520
|
+
to?: string;
|
|
521
|
+
limit: number;
|
|
522
|
+
}): {
|
|
523
|
+
runIds: string[];
|
|
524
|
+
total: number;
|
|
525
|
+
inFlight: number;
|
|
526
|
+
};
|
|
527
|
+
getRunsByTest(test: TestRef, opts?: {
|
|
528
|
+
limit?: number;
|
|
529
|
+
offset?: number;
|
|
530
|
+
attributePredicates?: AttributePredicate[];
|
|
531
|
+
}): RunRow[];
|
|
532
|
+
getRunsByTestCount(test: TestRef, opts?: {
|
|
533
|
+
attributePredicates?: AttributePredicate[];
|
|
534
|
+
}): number;
|
|
535
|
+
getTestTrends(test: TestRef, opts?: {
|
|
536
|
+
from?: string;
|
|
537
|
+
attributePredicates?: AttributePredicate[];
|
|
538
|
+
}): {
|
|
539
|
+
daily: {
|
|
540
|
+
date: string;
|
|
541
|
+
passed: number;
|
|
542
|
+
failed: number;
|
|
543
|
+
total: number;
|
|
544
|
+
avgDuration: number;
|
|
545
|
+
}[];
|
|
546
|
+
passRate: number | null;
|
|
547
|
+
totalRuns: number;
|
|
548
|
+
avgDuration: number;
|
|
549
|
+
};
|
|
550
|
+
private withMemberOutcome;
|
|
551
|
+
getRunsBySuiteId(suiteId: string, opts?: {
|
|
552
|
+
limit?: number;
|
|
553
|
+
offset?: number;
|
|
554
|
+
attributePredicates?: AttributePredicate[];
|
|
555
|
+
}): RunRow[];
|
|
556
|
+
getRunsBySuiteIdCount(suiteId: string, opts?: {
|
|
557
|
+
attributePredicates?: AttributePredicate[];
|
|
558
|
+
}): number;
|
|
559
|
+
getSuiteTrends(suiteId: string, opts?: {
|
|
560
|
+
from?: string;
|
|
561
|
+
attributePredicates?: AttributePredicate[];
|
|
562
|
+
}): {
|
|
563
|
+
daily: {
|
|
564
|
+
date: string;
|
|
565
|
+
passed: number;
|
|
566
|
+
failed: number;
|
|
567
|
+
total: number;
|
|
568
|
+
avgDuration: number;
|
|
569
|
+
}[];
|
|
570
|
+
passRate: number | null;
|
|
571
|
+
totalRuns: number;
|
|
572
|
+
avgDuration: number;
|
|
573
|
+
};
|
|
574
|
+
getInsightsBreakdown(dimension: InsightsBreakdownDimension, opts?: StatsOptions & {
|
|
575
|
+
limit?: number;
|
|
576
|
+
}): InsightsBreakdownRow[];
|
|
577
|
+
getFlakyTests(opts?: {
|
|
578
|
+
minRuns?: number;
|
|
579
|
+
limit?: number;
|
|
580
|
+
}): {
|
|
581
|
+
name: string;
|
|
582
|
+
filePath: string | null;
|
|
583
|
+
totalRuns: number;
|
|
584
|
+
passRate: number;
|
|
585
|
+
flakyScore: number;
|
|
586
|
+
}[];
|
|
587
|
+
insertExecutionLog(data: InsertExecutionLogInput): void;
|
|
588
|
+
getExecutionLogs(filter: {
|
|
589
|
+
runId?: string;
|
|
590
|
+
stepId?: string;
|
|
591
|
+
type?: string;
|
|
592
|
+
}): ExecutionLogRow[];
|
|
593
|
+
close(): void;
|
|
594
|
+
private parseRunAttributes;
|
|
595
|
+
private mapRunRow;
|
|
596
|
+
private mapRunArtifactRow;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
interface ExecuteOptions {
|
|
600
|
+
runId: string;
|
|
601
|
+
args: string[];
|
|
602
|
+
source: string;
|
|
603
|
+
attributes?: RunAttributes;
|
|
604
|
+
timeout?: number;
|
|
605
|
+
/**
|
|
606
|
+
* The Appium lease URL for a mobile run, AS DATA (401-07, D-01/D-12).
|
|
607
|
+
*
|
|
608
|
+
* The bundled Appium spawn is design-of-record and stays exactly where it is; what changed is the
|
|
609
|
+
* DELIVERY. It used to ride `AGENT_QA_APPIUM_URL` in a child's environment, and a child env is
|
|
610
|
+
* something only a child has. The run happens in this process now, so the lease travels as an
|
|
611
|
+
* argument and is handed to the run through an injected resolver.
|
|
612
|
+
*/
|
|
613
|
+
appiumUrl?: string;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* A run has reached a terminal state (401-07, D-04/D-12).
|
|
617
|
+
*
|
|
618
|
+
* THE TYPED SUCCESSOR TO THE CHILD'S `close` EVENT. The host reconciles a db row still reading
|
|
619
|
+
* `running`, releases the run's Appium lease and frees the queue slot off this — exactly what it did
|
|
620
|
+
* off the process exit — but the status is now the bus's own ground truth and the `details` carry
|
|
621
|
+
* the failure's REASON rather than a scraped tail of stderr.
|
|
622
|
+
*
|
|
623
|
+
* `'run-settled'` is now the WHOLE of this emitter's contract. A second event carried the retired
|
|
624
|
+
* live feed's frames and went with it (405-07, D-19).
|
|
625
|
+
*/
|
|
626
|
+
interface RunSettledDetails {
|
|
627
|
+
/**
|
|
628
|
+
* A human-readable reason when the run did not simply finish. 2026-09-30, 432-09 (D-15): a
|
|
629
|
+
* refusal is this plain reason. No remedy list rides the settle; nothing asks the reader to act.
|
|
630
|
+
*/
|
|
631
|
+
readonly reason?: string;
|
|
632
|
+
/**
|
|
633
|
+
* The run NEVER BEGAN — the gate or the host refused it (401-18, G-401-16).
|
|
634
|
+
*
|
|
635
|
+
* A FACT, NOT AN INFERENCE. `settle` has always known this — a rejection with no started session
|
|
636
|
+
* is a denial, a refused request field or an input error — and threw the knowledge away, leaving
|
|
637
|
+
* the host's reconciliation to guess from a regex over the diagnostic lines. It guessed
|
|
638
|
+
* `Process exited before test completed`, which is a sentence about a process, for a run where
|
|
639
|
+
* nothing was ever started to exit.
|
|
640
|
+
*
|
|
641
|
+
* DELIBERATELY A BOOLEAN AND NOT A CLASS. Which refusal it was — `fail-closed-denied`, `blocked`,
|
|
642
|
+
* `session-pin-denied`, an unrecognised request field, a plain input error — is not something the
|
|
643
|
+
* reconciliation branches on, and a prose name for it here would be the same defect one layer
|
|
644
|
+
* down. If a class is ever needed it arrives as a closed-set value owned by `schema`.
|
|
645
|
+
*
|
|
646
|
+
* ABSENT ON A RUN THAT STARTED, including one that started and then failed. That asymmetry is the
|
|
647
|
+
* scope guarantee: every other arm of the reconciliation reads exactly what it read before.
|
|
648
|
+
*/
|
|
649
|
+
readonly refusedBeforeStart?: true;
|
|
650
|
+
/**
|
|
651
|
+
* The refusal said without naming any surface's affordance (401-18, G-401-16).
|
|
652
|
+
*
|
|
653
|
+
* The gate composed two sentences per decision: the canonical one, which named the session trust
|
|
654
|
+
* flag and landed in the execution-audit row, and the neutral one, which stops at the classification and the outcome.
|
|
655
|
+
* A browser reader was being handed the first, so the record's only account of why a run never
|
|
656
|
+
* happened told them to pass a flag to a command they were not running.
|
|
657
|
+
*
|
|
658
|
+
* READ STRUCTURALLY OFF THE REJECTION, and ABSENT where the
|
|
659
|
+
* rejection carries no neutral form — an input error is not a gate decision and has no second
|
|
660
|
+
* phrasing. The consumer then falls back to the rejection's own message, which for those refusals
|
|
661
|
+
* names no surface's affordance either.
|
|
662
|
+
*/
|
|
663
|
+
readonly neutralReason?: string;
|
|
664
|
+
/** Diagnostic lines — a rejection's own message. The typed successor to scraped stderr. */
|
|
665
|
+
readonly output: readonly string[];
|
|
666
|
+
}
|
|
667
|
+
type RunSettledStatus = 'completed' | 'failed' | 'cancelled' | 'timeout';
|
|
668
|
+
/** A trigger's request as the queue would carry it, before anything is recorded (433-34). */
|
|
669
|
+
interface PreflightOptions {
|
|
670
|
+
readonly args: string[];
|
|
671
|
+
readonly source: 'dashboard' | 'suite';
|
|
672
|
+
}
|
|
673
|
+
interface ExecutionBackend extends EventEmitter {
|
|
674
|
+
execute(opts: ExecuteOptions): void;
|
|
675
|
+
/**
|
|
676
|
+
* Ask the run core whether this request may start, before a row is minted or anything enqueued
|
|
677
|
+
* (433-34, G-433-10).
|
|
678
|
+
*
|
|
679
|
+
* The composing host answers with the core's own production rule over the members' declared
|
|
680
|
+
* targets; this package decides nothing about production. A refusal rejects with the core's
|
|
681
|
+
* denial, which carries `neutralReason`. Absent on a backend whose host composes no preflight.
|
|
682
|
+
*/
|
|
683
|
+
preflight?(opts: PreflightOptions): Promise<void>;
|
|
684
|
+
kill(runId: string): boolean;
|
|
685
|
+
killAll(): void;
|
|
686
|
+
/**
|
|
687
|
+
* The runs this backend currently has in flight, as `GET /api/execution/active` serves them.
|
|
688
|
+
*
|
|
689
|
+
* `testName` IS ON THE INTERFACE (405-07, D-19). It used to be produced by scanning a per-run
|
|
690
|
+
* event buffer for a `testStarted` frame — a read that only the concrete class could perform,
|
|
691
|
+
* through a member the interface did not declare. It is now recorded on the run handle by the
|
|
692
|
+
* backend's own bus subscription, so the field is part of the contract every implementation
|
|
693
|
+
* answers rather than a property of one implementation's internal storage.
|
|
694
|
+
*
|
|
695
|
+
* ABSENT MEANS NO MEMBER HAS STARTED YET, which is a real state on a run between its start and
|
|
696
|
+
* its first `testStarted`; it is never filled with the run id or a placeholder.
|
|
697
|
+
*/
|
|
698
|
+
getActiveExecutions(): {
|
|
699
|
+
runId: string;
|
|
700
|
+
status: string;
|
|
701
|
+
startedAt: string;
|
|
702
|
+
duration: number;
|
|
703
|
+
testName?: string;
|
|
704
|
+
}[];
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/** The frame-facing slice of a `RunSession`, for an editor session. */
|
|
708
|
+
interface LiveEditorAttachment {
|
|
709
|
+
streamCapabilityOf(handle: number): LiveStreamDeclaration;
|
|
710
|
+
subscribeFrames(handle: number, onFrame: (frame: LiveFrame) => void, onEnd?: (reason: LiveFrameEndReason) => void): LiveFrameSubscription | undefined;
|
|
711
|
+
/** Settles when the editor session is over. The frame socket then says `sessionEnded`. */
|
|
712
|
+
readonly ended: Promise<void>;
|
|
713
|
+
}
|
|
714
|
+
interface LiveEditorSubjects {
|
|
715
|
+
register(sessionId: string, attachment: LiveEditorAttachment): void;
|
|
716
|
+
unregister(sessionId: string): void;
|
|
717
|
+
attach(sessionId: string): LiveEditorAttachment | undefined;
|
|
718
|
+
/** The session's declared stream (its handle 0), or undefined when no such session is held. */
|
|
719
|
+
capabilityOf(sessionId: string): LiveStreamDeclaration | undefined;
|
|
720
|
+
}
|
|
721
|
+
declare function createLiveEditorSubjects(): LiveEditorSubjects;
|
|
722
|
+
|
|
723
|
+
type ModelAdapter = 'openai-responses' | 'anthropic-messages';
|
|
724
|
+
interface DashboardLLMConfig {
|
|
725
|
+
provider: string;
|
|
726
|
+
model: string;
|
|
727
|
+
baseURL?: string;
|
|
728
|
+
providerHeaders?: Record<string, string>;
|
|
729
|
+
apiKey?: string;
|
|
730
|
+
authToken?: string;
|
|
731
|
+
modelAdapter?: ModelAdapter;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* The composed function registry, OPAQUE to this package.
|
|
736
|
+
*
|
|
737
|
+
* Deliberately not structurally described. The registry's real type lives in `agent`, this package
|
|
738
|
+
* may not name it, and a hand-written mirror of its shape here would be a second declaration of
|
|
739
|
+
* somebody else's contract — stale on the day `agent` changes it, and silently so, because nothing
|
|
740
|
+
* would compare the two. What this package does with the value is hand it straight back to the
|
|
741
|
+
* functions below, which is all an opaque handle has to support.
|
|
742
|
+
*/
|
|
743
|
+
type AgentRegistryHandle = object;
|
|
744
|
+
/**
|
|
745
|
+
* The whole of what the run-detail and mark routes may ask of the process that started them.
|
|
746
|
+
*
|
|
747
|
+
* A single object rather than loose options, because the members are one decision: they all come
|
|
748
|
+
* from ONE composition, and a server holding an invoke from one host and a registry from another is
|
|
749
|
+
* a state nobody should be able to construct.
|
|
750
|
+
*/
|
|
751
|
+
interface AgentHost {
|
|
752
|
+
/** The composed registry every call goes through. Opaque here. */
|
|
753
|
+
readonly registry: AgentRegistryHandle;
|
|
754
|
+
/**
|
|
755
|
+
* The caller this door dispatches as, MINTED by the composition root and never by this package.
|
|
756
|
+
*
|
|
757
|
+
* Attribution and grants both. The composer derives it from the CLI chat door's own mint with
|
|
758
|
+
* only the surface name changed (D-08/Pitfall 9) — a hand-typed scope list here would be the
|
|
759
|
+
* drift vector the parity legs exist to catch, and this package could not type-check one anyway.
|
|
760
|
+
*/
|
|
761
|
+
readonly caller: FunctionCaller;
|
|
762
|
+
/**
|
|
763
|
+
* The agent package's `runs.postprocessing` id (404 D-16/D-18).
|
|
764
|
+
*
|
|
765
|
+
* SUPPLIED BY THE COMPOSITION, NEVER SPELLED HERE. The post-processing read route dispatches
|
|
766
|
+
* chip truth through this door, and the op id is the agent package's constant: a string literal
|
|
767
|
+
* in this package would be a second declaration of a capability's name, free to drift from the
|
|
768
|
+
* spec that answers to it. This package also may not import `@vostride/agent-qa-agent`
|
|
769
|
+
* (validate:arch), so there is nowhere here the real constant could come from.
|
|
770
|
+
*/
|
|
771
|
+
readonly runsPostProcessingFunctionId: string;
|
|
772
|
+
/**
|
|
773
|
+
* The agent package's `runs.events` id, on the line above's footing (404 D-22).
|
|
774
|
+
*
|
|
775
|
+
* A SECOND ID BECAUSE THE READ ROUTE JOINS TWO FACTS from two different records: the chain's
|
|
776
|
+
* chips live in the durable job store, and the run's POSTURE — who was going to drain it — lives
|
|
777
|
+
* on the `runFinished` line of the run's own event log, beside the last per-stage failure text.
|
|
778
|
+
* Reading the log any other way would mean this package opening a file the function layer owns.
|
|
779
|
+
*/
|
|
780
|
+
readonly runsEventsFunctionId: string;
|
|
781
|
+
/**
|
|
782
|
+
* The agent package's `runs.detail` id, on `runsPostProcessingFunctionId`'s exact footing (405
|
|
783
|
+
* D-03).
|
|
784
|
+
*
|
|
785
|
+
* SUPPLIED BY THE COMPOSITION, NEVER SPELLED HERE, for the reason its two neighbours are: the
|
|
786
|
+
* op id is the agent package's constant, a string literal in this package would be a second
|
|
787
|
+
* declaration of a capability's name free to drift from the spec that answers to it, and this
|
|
788
|
+
* package may not import `@vostride/agent-qa-agent` at all (validate:arch) — so there is nowhere
|
|
789
|
+
* here the real constant could come from.
|
|
790
|
+
*
|
|
791
|
+
* WHAT IT REPLACES is worth naming: run detail used to be assembled by reading SQLite from
|
|
792
|
+
* inside the router. Routing it through this id is what makes the same durable read available
|
|
793
|
+
* to the CLI, MCP and chat instead of to the browser alone.
|
|
794
|
+
*/
|
|
795
|
+
readonly runsDetailFunctionId: string;
|
|
796
|
+
/** The evidence op identity supplied by the same composition as the detail reader. */
|
|
797
|
+
readonly runsEvidenceFunctionId: string;
|
|
798
|
+
/**
|
|
799
|
+
* The agent package's `runs.artifact` id, on `runsDetailFunctionId`'s exact footing (405 D-13).
|
|
800
|
+
*
|
|
801
|
+
* SUPPLIED BY THE COMPOSITION, NEVER SPELLED HERE, for the reason its four neighbours are — and
|
|
802
|
+
* for one more that is specific to this route: the artifact arm serves BYTES, and the alternative
|
|
803
|
+
* to dispatching them through the function layer is this package constructing the evidence store
|
|
804
|
+
* and reading a file. That would be a private store read no roster row covers and no audit sink
|
|
805
|
+
* sees, on a path whose two inputs both arrive from a URL. Routing it through an id is what makes
|
|
806
|
+
* the containment rule the store's own and the capability one the CLI, MCP and 408's embed reach
|
|
807
|
+
* the same way.
|
|
808
|
+
*/
|
|
809
|
+
readonly runsArtifactFunctionId: string;
|
|
810
|
+
/**
|
|
811
|
+
* The agent package's `analysis.get` id, on `runsArtifactFunctionId`'s exact footing (406 D-04).
|
|
812
|
+
*
|
|
813
|
+
* SUPPLIED BY THE COMPOSITION, NEVER SPELLED HERE, for the reason its five neighbours are: the
|
|
814
|
+
* op id is the agent package's constant, a string literal in this package would be a second
|
|
815
|
+
* declaration of a capability's name free to drift from the spec that answers to it, and this
|
|
816
|
+
* package may not import `@vostride/agent-qa-agent` at all (validate:arch) — so there is nowhere
|
|
817
|
+
* here the real constant could come from.
|
|
818
|
+
*
|
|
819
|
+
* WHAT IT SERVES is the failure view: the resolved verdicts and rendered sentences a person
|
|
820
|
+
* reads inside each failed member's group, on BOTH doors through one adapter. `analysis.get` is
|
|
821
|
+
* the single producer of those sentences (389 D-11), so routing the page's read through this id
|
|
822
|
+
* is what keeps the browser, the CLI, chat and 408's embed reading one answer.
|
|
823
|
+
*/
|
|
824
|
+
readonly analysisGetFunctionId: string;
|
|
825
|
+
/**
|
|
826
|
+
* The agent package's `runs.knowledge` id, on `analysisGetFunctionId`'s exact footing
|
|
827
|
+
* (2026-08-29, 407-01 D-05).
|
|
828
|
+
*
|
|
829
|
+
* THE SIXTH ID THE RUN-SERVICE ROUTES DISPATCH, supplied by the composition and never spelled
|
|
830
|
+
* here, for the reason its five neighbours are: the op id is the agent package's constant, a
|
|
831
|
+
* string literal in this package would be a second declaration of a capability's name free to
|
|
832
|
+
* drift from the spec that answers to it, and this package may not import
|
|
833
|
+
* `@vostride/agent-qa-agent` at all (validate:arch) — so there is nowhere here the real constant
|
|
834
|
+
* could come from.
|
|
835
|
+
*
|
|
836
|
+
* WHAT IT SERVES is the Knowledge panel: what a run USED (the brief channels served per step,
|
|
837
|
+
* the serve-time trace behind each, procedure replay and hint use, the recorded retrieval rung)
|
|
838
|
+
* and what it LEARNED (curation counts, the record changes attributable to it, canary outcomes).
|
|
839
|
+
* `runs.knowledge` is the single producer of that whole view (407 D-05), so routing the page's
|
|
840
|
+
* read through this id is what keeps the browser, the CLI, MCP and chat reading one answer
|
|
841
|
+
* instead of a page composing it from N per-step calls that can disagree.
|
|
842
|
+
*/
|
|
843
|
+
readonly runsKnowledgeFunctionId: string;
|
|
844
|
+
/** 2026-09-08, 419 D-13: the eighth op id arrives as data, on the knowledge id's footing. */
|
|
845
|
+
readonly runsMarkFunctionId: string;
|
|
846
|
+
/** 2026-09-15, 423-21 D-18: the marks-history read id, supplied as data like its neighbours. */
|
|
847
|
+
readonly runsMarksHistoryFunctionId: string;
|
|
848
|
+
/** 2026-09-15, 423-21 D-18: the read-tier mark consequence preview id. */
|
|
849
|
+
readonly runsMarkPreviewFunctionId: string;
|
|
850
|
+
/**
|
|
851
|
+
* `invokeFunction`, bound by the composer. THE one door call.
|
|
852
|
+
*
|
|
853
|
+
* `input` IS `unknown` (404-05). Every op this seam carries has its own strict input schema.
|
|
854
|
+
* Narrowing here would mean this package describing other packages' input contracts — and the
|
|
855
|
+
* real `invokeFunction` has always taken `unknown`, because the op's own schema is what validates
|
|
856
|
+
* it, once, inside the invocation.
|
|
857
|
+
*/
|
|
858
|
+
invokeFunction(registry: AgentRegistryHandle, functionId: string, input: unknown, caller: FunctionCaller): Promise<unknown>;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/** One viewer's callback. Runs on the EMITTER'S thread — see `publish`. */
|
|
862
|
+
type PostProcessingListener = (envelope: RunEventEnvelope) => void;
|
|
863
|
+
interface PostProcessingRelay {
|
|
864
|
+
/**
|
|
865
|
+
* Hand one chip envelope to whoever is watching THAT run.
|
|
866
|
+
*
|
|
867
|
+
* NEVER THROWS. The caller is the chip emitter, mid-`append`, immediately after the line is on
|
|
868
|
+
* disk: the trail is the record and a viewer's fault is not the emitter's problem (the run bus's
|
|
869
|
+
* own subscriber-fault discipline, applied one layer out). A listener that throws is skipped for
|
|
870
|
+
* this delivery and the remaining listeners still receive it — dropping the rest would let one
|
|
871
|
+
* broken page silence every other page watching the same run.
|
|
872
|
+
*/
|
|
873
|
+
publish(envelope: RunEventEnvelope): void;
|
|
874
|
+
/** Watch one run. Returns its own disposer, which is idempotent. */
|
|
875
|
+
subscribe(runId: string, listener: PostProcessingListener): () => void;
|
|
876
|
+
/** How many listeners that run has — so a host can assert a closed stream released its own. */
|
|
877
|
+
listenerCount(runId: string): number;
|
|
878
|
+
}
|
|
879
|
+
declare function createPostProcessingRelay(): PostProcessingRelay;
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* What starting a run needs from the composition, as data rather than as a live handle.
|
|
883
|
+
*
|
|
884
|
+
* 2026-10-01 (432-21, D-12): the matched token's `trustScope` and `clientId` are deleted with the
|
|
885
|
+
* token. A start carries the caller's pattern set and nothing about who the caller is.
|
|
886
|
+
*/
|
|
887
|
+
interface RunServiceStartInput {
|
|
888
|
+
/** The pattern set the caller asked for; absent means the configured default set. */
|
|
889
|
+
readonly patterns?: readonly string[];
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Everything this door reaches the world through (D-02).
|
|
893
|
+
*
|
|
894
|
+
* ALL ARE INJECTED, and the injection is what makes the module mountable: the implementations name
|
|
895
|
+
* `invokeFunction`, `runs.start` and a composed local host, all of it vocabulary this package may
|
|
896
|
+
* not import. `InProcessRunStarter` is the shape precedent one directory over. 2026-10-01 (432-21,
|
|
897
|
+
* D-12): `verifyBearer` is deleted; the same-site guard needs no port.
|
|
898
|
+
*/
|
|
899
|
+
interface RunServicePorts {
|
|
900
|
+
/**
|
|
901
|
+
* Start a run and return the acknowledgment.
|
|
902
|
+
*
|
|
903
|
+
* REJECTION IS NOT "the run failed" — a failing run still acknowledges. A rejection is a denial,
|
|
904
|
+
* a refused request field, an input error or a crash.
|
|
905
|
+
*/
|
|
906
|
+
invokeRunStart(input: RunServiceStartInput): Promise<RunStartAcknowledgement>;
|
|
907
|
+
/** The started run's session, or nothing if this process never started that run. */
|
|
908
|
+
attach(runId: string): RunSession | undefined;
|
|
909
|
+
/** The measured readiness facts `/ok` reports (D-15). */
|
|
910
|
+
readiness(): RunServiceReadiness;
|
|
911
|
+
/**
|
|
912
|
+
* Mint a live-view URL for one handle of a run (403 D-03).
|
|
913
|
+
*
|
|
914
|
+
* REQUIRED, NOT OPTIONAL, and that is a decision rather than an oversight. An optional member is
|
|
915
|
+
* a member a composition can forget, and the failure mode of forgetting it here is a 404 that
|
|
916
|
+
* reads exactly like "no such route" — which is what a door with no live view is indistinguishable
|
|
917
|
+
* from. A composition that cannot mint supplies a function that REJECTS with a
|
|
918
|
+
* `LiveViewUnavailableError`-named error, which the arm maps to 503 `live_view_unavailable`: an
|
|
919
|
+
* honest host fact, said out loud, once, in the vocabulary the local door already uses.
|
|
920
|
+
*
|
|
921
|
+
* The minted token's own subject (run, handle) and expiry are checked by the live-view validator
|
|
922
|
+
* independently of who asked (D-13). 2026-10-01 (432-21, D-12): the bearer's trust scope this mint
|
|
923
|
+
* used to run under is deleted; the composition supplies the one caller it runs under.
|
|
924
|
+
*/
|
|
925
|
+
mintLiveUrl(input: MintLiveUrlInput): Promise<RunsLiveUrlResult>;
|
|
926
|
+
/**
|
|
927
|
+
* The injected agent host the two READ rows dispatch through (405-05, D-12/D-13).
|
|
928
|
+
*
|
|
929
|
+
* OPTIONAL, unlike `mintLiveUrl` above, and the asymmetry is deliberate rather than an
|
|
930
|
+
* oversight. `mintLiveUrl` is required because a composition that forgot it would answer a 404
|
|
931
|
+
* indistinguishable from an unserved path — but the argument runs the other way here: a host
|
|
932
|
+
* genuinely composed without a function registry (a bare mount, this file's own fixtures) has no
|
|
933
|
+
* capability to reach, and the honest answer is the SAME 503 `agent_host_unavailable` the local
|
|
934
|
+
* door already gives for exactly this state. One word, one meaning, both doors.
|
|
935
|
+
*
|
|
936
|
+
* It is a HANDLE and nothing else — this package may not name the agent packages, so what
|
|
937
|
+
* crosses is the seam `chat/host.ts` declares and the op ids the composition supplies as data.
|
|
938
|
+
*/
|
|
939
|
+
readonly agentHost?: AgentHost;
|
|
940
|
+
}
|
|
941
|
+
/** What minting one `liveUrl` needs from a request, as data: the subject, and nothing else. */
|
|
942
|
+
interface MintLiveUrlInput {
|
|
943
|
+
readonly runId: string;
|
|
944
|
+
readonly handle: number;
|
|
945
|
+
}
|
|
946
|
+
/** Which handler a table row reaches. A closed union, so a row with no arm is a type error. */
|
|
947
|
+
type RunServiceRouteHandlerName = 'start' | 'events' | 'liveUrl' | 'detail' | 'evidence' | 'artifact' | 'analysis' | 'marks' | 'markPreview' | 'ok';
|
|
948
|
+
interface RunServiceRouteRow {
|
|
949
|
+
readonly method: 'GET' | 'POST';
|
|
950
|
+
/** The literal path, or — for a templated route — its `:param` shape. */
|
|
951
|
+
readonly path: string;
|
|
952
|
+
readonly handler: RunServiceRouteHandlerName;
|
|
953
|
+
/** Present only on a templated route; the matcher uses it instead of `path`. */
|
|
954
|
+
readonly pattern?: RegExp;
|
|
955
|
+
}
|
|
956
|
+
declare const RUN_SERVICE_START_ROUTE = "/run";
|
|
957
|
+
declare const RUN_SERVICE_EVENTS_TEMPLATE = "/run/:runId/events";
|
|
958
|
+
/**
|
|
959
|
+
* The MINT (403 D-03, Q3) — the remote half of the pair whose local half is
|
|
960
|
+
* `GET /api/runs/:runId/live-url`.
|
|
961
|
+
*
|
|
962
|
+
* SAME SHAPE AS THE EVENTS ROUTE ONE LINE UP, deliberately: a caller addresses a run it started by
|
|
963
|
+
* id and asks this door for one thing about it per route. Since 432-21 the two doors differ in
|
|
964
|
+
* nothing but the path — the same op, the same caller shape, the same result and fragment carriage.
|
|
965
|
+
*/
|
|
966
|
+
declare const RUN_SERVICE_LIVE_URL_TEMPLATE = "/run/:runId/live-url";
|
|
967
|
+
/**
|
|
968
|
+
* Readiness (D-15) — behind the same-site guard, like every other route on this door.
|
|
969
|
+
*
|
|
970
|
+
* An `/ok` that answered before the guard would tell a page on another origin that this process is
|
|
971
|
+
* an agent-qa run service and that it is up. (Until 432-21 the guard here was the bearer verdict,
|
|
972
|
+
* and the preflight was liveness and early token validation in one round-trip.)
|
|
973
|
+
*/
|
|
974
|
+
/**
|
|
975
|
+
* The durable run-detail READ (405-05, D-12 / 402 D-16) — the remote twin of the local door's
|
|
976
|
+
* `GET /api/runs/:id/detail`.
|
|
977
|
+
*
|
|
978
|
+
* IT DISPATCHES THROUGH THE SAME ADAPTER THE LOCAL DOOR CALLS. `serveRunDetailRead` is one
|
|
979
|
+
* function, so the two doors cannot come to disagree about what a run's projection is; a second
|
|
980
|
+
* spelling here would agree on the day it was written and be a wire-contract fork thereafter.
|
|
981
|
+
*
|
|
982
|
+
* THIS ROW IS MASKED, and deliberately. It is a machine wire like `/run/:runId/events` one line up
|
|
983
|
+
* — its consumer (the CLI's own `emitHuman`, 408's embed) is where a person meets the text, and
|
|
984
|
+
* that consumer is the display boundary. The alternative would restore declared secrets onto a
|
|
985
|
+
* machine wire, extending the disclosure surface to every client and every intermediary, which is
|
|
986
|
+
* not what "the browser is never the boundary" (400-06) asks for. 405-10
|
|
987
|
+
* applies the unmask seam to the LOCAL door only and enrols this row on the masked side.
|
|
988
|
+
*/
|
|
989
|
+
declare const RUN_SERVICE_DETAIL_TEMPLATE = "/run/:runId/detail";
|
|
990
|
+
declare const RUN_SERVICE_EVIDENCE_TEMPLATE = "/run/:runId/evidence";
|
|
991
|
+
/**
|
|
992
|
+
* The ref-resolving artifact READ (405-05, D-12 / 402 D-16) — the remote twin of the local door's
|
|
993
|
+
* `GET /api/runs/:runId/artifacts/:sha`, and the row 402 D-16 named by anticipation.
|
|
994
|
+
*
|
|
995
|
+
* TWO CAPTURES, `[^/]+` ON BOTH. The runId is decoded and the sha is not: a content address is 64
|
|
996
|
+
* characters from `[0-9a-f]`, none of which percent-encoding can represent differently, so a sha
|
|
997
|
+
* segment that needed decoding is already not one.
|
|
998
|
+
*
|
|
999
|
+
* MASKED, LIKE ITS SIBLING AND MORE SO: these are BYTES. The unmask seam never touches an artifact
|
|
1000
|
+
* body on any door (D-09) — a screenshot is not text and there is nothing in it to restore.
|
|
1001
|
+
*/
|
|
1002
|
+
declare const RUN_SERVICE_ARTIFACT_TEMPLATE = "/run/:runId/artifacts/:sha";
|
|
1003
|
+
/**
|
|
1004
|
+
* The run-ANALYSIS read (406-01, D-04) — the remote twin of the local door's
|
|
1005
|
+
* `GET /api/runs/:id/analysis`.
|
|
1006
|
+
*
|
|
1007
|
+
* IT DISPATCHES THROUGH THE SAME ADAPTER THE LOCAL DOOR CALLS, for the reason its detail sibling
|
|
1008
|
+
* does: `serveRunAnalysisRead` is one function, so the two doors cannot come to disagree about
|
|
1009
|
+
* what a run's analysis is.
|
|
1010
|
+
*
|
|
1011
|
+
* THIS ROW IS MASKED, AND FOR THE REASON ITS SIBLING IS. The verdict sentence quotes the failing
|
|
1012
|
+
* step's recorded error, which carries declared placeholders at rest; restoring them onto a
|
|
1013
|
+
* machine wire would hand them to every client and every intermediary, and this row's
|
|
1014
|
+
* consumer — the CLI's own emit, 408's embed — is where a person meets the text. The dispatch
|
|
1015
|
+
* below passes no `boundary`, and the option's `'machine'` default is what keeps it masked.
|
|
1016
|
+
*/
|
|
1017
|
+
declare const RUN_SERVICE_ANALYSIS_TEMPLATE = "/run/:runId/analysis";
|
|
1018
|
+
/** 2026-09-15, 423-21 D-18: runs.marks.history and runs.mark.preview behind the same guard. */
|
|
1019
|
+
declare const RUN_SERVICE_MARKS_TEMPLATE = "/run/:runId/marks";
|
|
1020
|
+
declare const RUN_SERVICE_MARK_PREVIEW_TEMPLATE = "/run/:runId/mark-preview";
|
|
1021
|
+
declare const RUN_SERVICE_OK_ROUTE = "/ok";
|
|
1022
|
+
/**
|
|
1023
|
+
* EVERY PATH THIS DOOR ANSWERS, as data the dispatch reads (the chat door's own idiom).
|
|
1024
|
+
*
|
|
1025
|
+
* WHY A TABLE AND NOT THREE ARMS. The same-site guard below is applied to the REQUEST, above this
|
|
1026
|
+
* table, so a route added tomorrow inherits it by construction. The table is what makes that
|
|
1027
|
+
* inheritance AUDITABLE from outside: because dispatch reads it, a route not in it is unreachable,
|
|
1028
|
+
* so a census cell can enumerate this array and drive every row cross-site — and a newly added
|
|
1029
|
+
* route cannot escape that corpus by nobody noticing it.
|
|
1030
|
+
*/
|
|
1031
|
+
declare const RUN_SERVICE_ROUTE_TABLE: readonly RunServiceRouteRow[];
|
|
1032
|
+
/** What a matched row carries: the row itself and whatever its pattern captured. */
|
|
1033
|
+
interface RunServiceRouteMatch {
|
|
1034
|
+
readonly row: RunServiceRouteRow;
|
|
1035
|
+
/** The raw, still-ENCODED capture groups. Decoding is the handler's business, and it can throw. */
|
|
1036
|
+
readonly params: readonly string[];
|
|
1037
|
+
}
|
|
1038
|
+
/** Resolve a request to a table row, or to nothing. The dispatch's only route lookup. */
|
|
1039
|
+
declare function matchRunServiceRoute(method: string | undefined, path: string): RunServiceRouteMatch | undefined;
|
|
1040
|
+
/**
|
|
1041
|
+
* Whether a path belongs to this door — the MOUNT's whole question, and method-blind on purpose.
|
|
1042
|
+
*
|
|
1043
|
+
* A host consults this before its own routing so that every request to a service path, including
|
|
1044
|
+
* an `OPTIONS` preflight and a method this table has no row for, is answered by this door rather
|
|
1045
|
+
* than by the host's generic handling. That is what keeps the host's wildcard CORS preflight and
|
|
1046
|
+
* its SPA fallback away from this surface.
|
|
1047
|
+
*
|
|
1048
|
+
* DERIVED FROM THE TABLE, never a second list: the prefixes are read off the rows, so a route
|
|
1049
|
+
* added above is mounted the same commit.
|
|
1050
|
+
*/
|
|
1051
|
+
declare function ownsRunServicePath(path: string): boolean;
|
|
1052
|
+
interface RunServiceRouter {
|
|
1053
|
+
(req: IncomingMessage, res: ServerResponse): void;
|
|
1054
|
+
/** Live SSE connections — so a host can assert this door leaks none on teardown. */
|
|
1055
|
+
openStreamCount(): number;
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Where a stream request asked to resume from. Three arms, and the count is the contract.
|
|
1059
|
+
*
|
|
1060
|
+
* `start` is "the caller has observed nothing", `after` carries the last seq it DID observe, and
|
|
1061
|
+
* `invalid` is a fact about the caller's own input. There is deliberately no fourth arm meaning
|
|
1062
|
+
* "unparseable, serve them from the beginning": silently restarting a resume at seq 1 would
|
|
1063
|
+
* re-deliver every envelope the caller already has, and silently serving from the floor would hide
|
|
1064
|
+
* a gap — the two answers D-13 refuses by name.
|
|
1065
|
+
*/
|
|
1066
|
+
type ResumePoint = {
|
|
1067
|
+
readonly kind: 'start';
|
|
1068
|
+
} | {
|
|
1069
|
+
readonly kind: 'after';
|
|
1070
|
+
readonly seq: number;
|
|
1071
|
+
} | {
|
|
1072
|
+
readonly kind: 'invalid';
|
|
1073
|
+
};
|
|
1074
|
+
/**
|
|
1075
|
+
* The DENIAL this rejection is, if it is one — read structurally, never by class (402-06, T-402-22).
|
|
1076
|
+
*
|
|
1077
|
+
* ON `dashboardNeutralReasonOf`'S EXACT PRECEDENT (`execution/in-process-backend.ts`), and for the
|
|
1078
|
+
* same architectural reason: the rejection is a `RunDeniedError` minted in `agent-local`, and this
|
|
1079
|
+
* package may not import `agent`, `agent-local` or `safety` — `pnpm validate:arch` is the
|
|
1080
|
+
* mechanical proof.
|
|
1081
|
+
*
|
|
1082
|
+
* THE DISCRIMINATOR IS `neutralReason`, DELIBERATELY. Only a run denial carries it: an
|
|
1083
|
+
* unrecognised request field, an input error and "the host was asked to start a run before it was
|
|
1084
|
+
* composed" carry none, and those are exactly the rejections that must stay a 500 with no words on
|
|
1085
|
+
* them. Keying on the class NAME would be a string compare against another package's private
|
|
1086
|
+
* vocabulary.
|
|
1087
|
+
*
|
|
1088
|
+
* 2026-10-01 (432-19, D-15): there is no remedy list to validate or project. The production refusal
|
|
1089
|
+
* never carried one, and the remedy vocabulary left with the execution gate.
|
|
1090
|
+
*/
|
|
1091
|
+
declare function runDenialOf(error: unknown): RunServiceDenialBody | undefined;
|
|
1092
|
+
declare function createRunServiceRoutes(ports: RunServicePorts): RunServiceRouter;
|
|
1093
|
+
|
|
1094
|
+
interface StartServerOptions {
|
|
1095
|
+
db: DashboardDatabase;
|
|
1096
|
+
port?: number;
|
|
1097
|
+
/**
|
|
1098
|
+
* The interface this server binds (402-05, D-07 — INVK-03).
|
|
1099
|
+
*
|
|
1100
|
+
* DEFAULTS TO LOOPBACK, and a non-loopback value is REFUSED before anything listens — with no
|
|
1101
|
+
* exemption. This is BREAKING for anyone who relied on the previous `server.listen(port)`, which
|
|
1102
|
+
* bound every interface, and there is no migration flag: the v2 ethos ships no back-compat shim
|
|
1103
|
+
* for a hole. Remote access goes through an authenticating proxy (432 D-13's forward note).
|
|
1104
|
+
*/
|
|
1105
|
+
host?: string;
|
|
1106
|
+
uiDir?: string;
|
|
1107
|
+
/**
|
|
1108
|
+
* Where this workspace writes run artifacts (`services.dashboard.artifactsDir`).
|
|
1109
|
+
*
|
|
1110
|
+
* ACCEPTED AND NOT READ BY THIS SERVER SINCE 405-07 (D-14). It used to be resolved into a
|
|
1111
|
+
* screenshots and a videos root that two path-serving routes and the run-deletion sweep read.
|
|
1112
|
+
* Refs are the only artifact addressing now, and the bytes are reached through the
|
|
1113
|
+
* `runs.artifact` op over the evidence store's own layout. The option stays declared because it
|
|
1114
|
+
* is a user-facing config key the RECORDING path still writes under; the dashboard simply no
|
|
1115
|
+
* longer serves or deletes anything by path.
|
|
1116
|
+
*/
|
|
1117
|
+
artifactsDir?: string;
|
|
1118
|
+
workspacePaths?: ResolvedWorkspacePaths;
|
|
1119
|
+
configPath?: string;
|
|
1120
|
+
declaredSecretMask: MaskFn;
|
|
1121
|
+
llmConfig?: DashboardLLMConfig & {
|
|
1122
|
+
vision?: {
|
|
1123
|
+
screenshotSize?: number;
|
|
1124
|
+
effectiveResolution?: number;
|
|
1125
|
+
};
|
|
1126
|
+
};
|
|
1127
|
+
authFetch?: typeof globalThis.fetch;
|
|
1128
|
+
/**
|
|
1129
|
+
* The composed agent host the chat door dispatches through (400-01, D-01).
|
|
1130
|
+
*
|
|
1131
|
+
* INJECTED by whoever starts this server — in production `agent-qa dashboard`, which composes the
|
|
1132
|
+
* SAME port set `agent-qa chat` does and hands it over, so the two interactive doors have one
|
|
1133
|
+
* answer to "what can a conversation do" (D-08). Absent leaves the chat routes answering 501 by
|
|
1134
|
+
* name: the door exists, this process was started without a host. See `chat/host.ts`.
|
|
1135
|
+
*/
|
|
1136
|
+
agentHost?: AgentHost;
|
|
1137
|
+
/**
|
|
1138
|
+
* The backend every dashboard run executes through (401-07, D-01/D-09/D-10 — INVK-01).
|
|
1139
|
+
*
|
|
1140
|
+
* INJECTED, ON `agentHost`'s OWN IDIOM AND FOR ITS OWN REASON. The implementation that starts a
|
|
1141
|
+
* run names `runs.start`, `invokeFunction` and the composed local host — all of it
|
|
1142
|
+
* `@vostride/agent-qa-agent*` vocabulary this package may not import (validate:arch). So
|
|
1143
|
+
* `agent-qa dashboard` composes it in the process that runs this server and hands it over, which
|
|
1144
|
+
* is what makes the run button one more caller of the op chat already uses rather than a
|
|
1145
|
+
* dashboard carve-out around the gates.
|
|
1146
|
+
*
|
|
1147
|
+
* ABSENT DEGRADES BY NAME, exactly as it did when the CLI binary could not be resolved: the
|
|
1148
|
+
* execution routes answer 503 and the queue's execute handler does nothing. A dashboard started
|
|
1149
|
+
* without a run door is a dashboard that cannot run tests, and it says so.
|
|
1150
|
+
*/
|
|
1151
|
+
executionBackend?: ExecutionBackend;
|
|
1152
|
+
/**
|
|
1153
|
+
* The run service's ports (402-01, D-02/D-06 — INVK-03).
|
|
1154
|
+
*
|
|
1155
|
+
* INJECTED, ON `agentHost`'s AND `executionBackend`'s OWN IDIOM AND FOR THEIR OWN REASON. The
|
|
1156
|
+
* implementations name `runs.start`, `invokeFunction` and the composed local host — all of it
|
|
1157
|
+
* vocabulary this package may not import (validate:arch). So `agent-qa dashboard` composes them
|
|
1158
|
+
* in the process that runs this server and hands them over, which is what makes the run service
|
|
1159
|
+
* a mountable module rather than a fourth process.
|
|
1160
|
+
*
|
|
1161
|
+
* ABSENT MEANS THE SERVICE IS NOT MOUNTED, and `/run` and its neighbours reach whatever the
|
|
1162
|
+
* static/SPA handling below does with an unknown path. 2026-10-01 (432-21, D-12): `agent-qa
|
|
1163
|
+
* dashboard` supplies it unconditionally now. It used to mount only when a run-service credential
|
|
1164
|
+
* was declared, and those credentials are deleted; the door is guarded by the same-site check
|
|
1165
|
+
* and the loopback bind like every other door here. Absent is a bare in-process mount's posture.
|
|
1166
|
+
*/
|
|
1167
|
+
runService?: RunServicePorts;
|
|
1168
|
+
/**
|
|
1169
|
+
* The run service's readiness predicate, served WITHOUT a token on the local dashboard API
|
|
1170
|
+
* (402-05, D-15 — `GET /api/run-readiness`).
|
|
1171
|
+
*
|
|
1172
|
+
* SEPARATE FROM `runService` ABOVE AND NOT DERIVED FROM IT, because a mount without the service
|
|
1173
|
+
* still has a browser that needs an honest readiness answer rather than a 404. `agent-qa
|
|
1174
|
+
* dashboard` supplies the very same function object it put on `runService.readiness`: one
|
|
1175
|
+
* predicate, two doors. (Until 432-21 the service was absent whenever no credential was declared.)
|
|
1176
|
+
*/
|
|
1177
|
+
runServiceReadiness?: () => RunServiceReadiness;
|
|
1178
|
+
/**
|
|
1179
|
+
* The run id → live `RunSession` lookup, as a FIRST-CLASS option (403 D-14 — WINDOWS #264).
|
|
1180
|
+
*
|
|
1181
|
+
* SEPARATE FROM `runService` AND NOT DERIVED FROM IT, on `runServiceReadiness`'s exact argument
|
|
1182
|
+
* one field up. The derivation `runService?.attach` is what made the local run-stream door
|
|
1183
|
+
* answer 503 about runs this very process was executing: until 432-21 `composeRunServicePorts`
|
|
1184
|
+
* ran only when `runServiceTokens` were declared, so the DEFAULT zero-token dashboard composed no
|
|
1185
|
+
* registry at all and the local door had nothing to attach to. `agent-qa dashboard` supplies this
|
|
1186
|
+
* from a registry hoisted above the door.
|
|
1187
|
+
*
|
|
1188
|
+
* The legacy derivation still applies when this is absent, so an in-process caller that only
|
|
1189
|
+
* passes `runService` keeps working byte-for-byte.
|
|
1190
|
+
*/
|
|
1191
|
+
runServiceAttach?: (runId: string) => RunSession | undefined;
|
|
1192
|
+
/**
|
|
1193
|
+
* The live-view seam (403 D-03/D-06): the function layer's validator, by reference, and the
|
|
1194
|
+
* mint dispatched through the registry.
|
|
1195
|
+
*
|
|
1196
|
+
* ABSENT MEANS NO LIVE VIEW IS SERVED — the embed route is not registered, the frame socket
|
|
1197
|
+
* refuses the upgrade, and `GET /api/runs/:runId/live-url` answers 503 by name. This server
|
|
1198
|
+
* holds NO signing key and NO token parse of its own; both are the function layer's, handed in.
|
|
1199
|
+
*/
|
|
1200
|
+
liveView?: {
|
|
1201
|
+
validate(token: string, expect: {
|
|
1202
|
+
subject: LiveViewSubjectRef;
|
|
1203
|
+
now: number;
|
|
1204
|
+
}): LiveUrlVerdict;
|
|
1205
|
+
mintLiveUrl(runId: string, handle: number): Promise<RunsLiveUrlResult>;
|
|
1206
|
+
/**
|
|
1207
|
+
* The `editor.liveurl` mint, dispatched through the registry (428 D-07). ABSENT MEANS NO
|
|
1208
|
+
* EDITOR VIEW IS MINTED, and `GET /api/live-editor/sessions/:id/live-url` answers 503 by name.
|
|
1209
|
+
*/
|
|
1210
|
+
mintEditorLiveUrl?(sessionId: string): Promise<EditorLiveUrlResult>;
|
|
1211
|
+
};
|
|
1212
|
+
/**
|
|
1213
|
+
* The editor-subject registry (428 D-07): the session manager registers each live editor session
|
|
1214
|
+
* here and the frame socket attaches editor subjects through it.
|
|
1215
|
+
*
|
|
1216
|
+
* ABSENT MEANS NO EDITOR SUBJECT IS SERVED ON THE FRAME SOCKET. `agent-qa dashboard` creates one
|
|
1217
|
+
* registry and hands the same object to the live-view composition and to this server.
|
|
1218
|
+
*/
|
|
1219
|
+
liveEditorSubjects?: LiveEditorSubjects;
|
|
1220
|
+
/**
|
|
1221
|
+
* The in-process post-processing chip relay (404-05, D-16).
|
|
1222
|
+
*
|
|
1223
|
+
* INJECTED, ON `agentHost`'s OWN IDIOM AND FOR ITS OWN REASON. The PUBLISHER is the CLI's
|
|
1224
|
+
* post-run chip emitter, in this same process, and this package may not import it — so
|
|
1225
|
+
* `agent-qa dashboard` creates one relay, hands `relay.publish` to the run door's bridge as the
|
|
1226
|
+
* emitter's second sink and hands the relay itself here. It has to be the SAME object: a relay
|
|
1227
|
+
* built inside this function would be a registry nothing publishes to, and the stream door would
|
|
1228
|
+
* heartbeat forever at a page whose chips are demonstrably moving.
|
|
1229
|
+
*
|
|
1230
|
+
* ABSENT MEANS NO PUSH CHANNEL, and `GET /api/runs/:id/post-processing/stream` answers 503 by
|
|
1231
|
+
* name. The READ route needs none of this and keeps working: polling is the floor the whole
|
|
1232
|
+
* design rests on, and the stream is the optimisation over it.
|
|
1233
|
+
*/
|
|
1234
|
+
postProcessingRelay?: PostProcessingRelay;
|
|
1235
|
+
}
|
|
1236
|
+
declare function startServer(opts: StartServerOptions): Promise<{
|
|
1237
|
+
server: Server;
|
|
1238
|
+
port: number;
|
|
1239
|
+
close: () => void;
|
|
1240
|
+
}>;
|
|
1241
|
+
|
|
1242
|
+
interface AppiumLeaseOwner {
|
|
1243
|
+
runId: string;
|
|
1244
|
+
platform: 'android' | 'ios';
|
|
1245
|
+
}
|
|
1246
|
+
interface AppiumLease extends AppiumLeaseOwner {
|
|
1247
|
+
acquiredAt: string;
|
|
1248
|
+
}
|
|
1249
|
+
declare class AppiumManager {
|
|
1250
|
+
private process;
|
|
1251
|
+
private port;
|
|
1252
|
+
private ready;
|
|
1253
|
+
private startPromise;
|
|
1254
|
+
private managed;
|
|
1255
|
+
private startupTimeoutMs;
|
|
1256
|
+
private pollIntervalMs;
|
|
1257
|
+
private anonymousRefCount;
|
|
1258
|
+
private leases;
|
|
1259
|
+
private logLevel;
|
|
1260
|
+
private appiumResolver;
|
|
1261
|
+
constructor(opts?: {
|
|
1262
|
+
port?: number;
|
|
1263
|
+
startupTimeoutMs?: number;
|
|
1264
|
+
pollIntervalMs?: number;
|
|
1265
|
+
logLevel?: 'normal' | 'debug';
|
|
1266
|
+
appiumResolver?: () => ResolvedAppiumExecutable;
|
|
1267
|
+
});
|
|
1268
|
+
acquire(): Promise<void>;
|
|
1269
|
+
release(): void;
|
|
1270
|
+
acquireLease(owner: AppiumLeaseOwner): Promise<void>;
|
|
1271
|
+
releaseLease(runId: string, reason?: string): boolean;
|
|
1272
|
+
releaseAllLeases(reason?: string): string[];
|
|
1273
|
+
ensureRunning(): Promise<void>;
|
|
1274
|
+
checkStatus(): Promise<boolean>;
|
|
1275
|
+
private startAndWait;
|
|
1276
|
+
private performShutdown;
|
|
1277
|
+
private maybeShutdownAfterRelease;
|
|
1278
|
+
shutdown(): void;
|
|
1279
|
+
getUrl(): string;
|
|
1280
|
+
getPort(): number;
|
|
1281
|
+
isManaged(): boolean;
|
|
1282
|
+
getRefCount(): number;
|
|
1283
|
+
getLeaseCount(): number;
|
|
1284
|
+
getLeases(): AppiumLease[];
|
|
1285
|
+
hasLease(runId: string): boolean;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
type ApplyChangesOutcome = {
|
|
1289
|
+
kind: 'ok';
|
|
1290
|
+
revision: string;
|
|
1291
|
+
baseRevision: string;
|
|
1292
|
+
priors?: ConfigPriors;
|
|
1293
|
+
} | {
|
|
1294
|
+
kind: 'refused';
|
|
1295
|
+
error: Exclude<ConfigWriteRefusal, 'write_failed'>;
|
|
1296
|
+
issues?: ConfigWriteIssue[];
|
|
1297
|
+
/** The field an `expect` found changed. */
|
|
1298
|
+
path?: ConfigPathSegment[];
|
|
1299
|
+
};
|
|
1300
|
+
type FieldState = {
|
|
1301
|
+
present: false;
|
|
1302
|
+
} | {
|
|
1303
|
+
present: true;
|
|
1304
|
+
value: unknown;
|
|
1305
|
+
};
|
|
1306
|
+
/**
|
|
1307
|
+
* What one change found and left at the field it touched (429-11 D-24). The nodes are the file's
|
|
1308
|
+
* own, so a restore brings back real secrets and comments rather than anything the page holds.
|
|
1309
|
+
*/
|
|
1310
|
+
type ConfigPrior = {
|
|
1311
|
+
kind: 'entry';
|
|
1312
|
+
array: ConfigPathSegment[];
|
|
1313
|
+
name: string;
|
|
1314
|
+
index: number;
|
|
1315
|
+
node: Node;
|
|
1316
|
+
} | {
|
|
1317
|
+
kind: 'path';
|
|
1318
|
+
path: ConfigPathSegment[];
|
|
1319
|
+
before: Node | null | undefined;
|
|
1320
|
+
after: FieldState;
|
|
1321
|
+
};
|
|
1322
|
+
type ConfigPriors = readonly ConfigPrior[];
|
|
1323
|
+
/** A whole-document replacement, checked against the bytes the caller read (432-07 D-05). */
|
|
1324
|
+
type ReplaceDocumentOutcome = {
|
|
1325
|
+
kind: 'ok';
|
|
1326
|
+
revision: string;
|
|
1327
|
+
baseRevision: string;
|
|
1328
|
+
} | {
|
|
1329
|
+
kind: 'refused';
|
|
1330
|
+
error: 'file_changed';
|
|
1331
|
+
revision: string;
|
|
1332
|
+
} | {
|
|
1333
|
+
kind: 'refused';
|
|
1334
|
+
error: 'marker_written_back';
|
|
1335
|
+
message: string;
|
|
1336
|
+
};
|
|
1337
|
+
type RestorePriorsOutcome = {
|
|
1338
|
+
kind: 'ok';
|
|
1339
|
+
revision: string;
|
|
1340
|
+
} | {
|
|
1341
|
+
kind: 'refused';
|
|
1342
|
+
error: 'undo_conflict' | 'file_invalid';
|
|
1343
|
+
};
|
|
1344
|
+
type ConfigReadOutcome = {
|
|
1345
|
+
kind: 'ok';
|
|
1346
|
+
config: Record<string, unknown>;
|
|
1347
|
+
raw: string;
|
|
1348
|
+
revision: string;
|
|
1349
|
+
} | {
|
|
1350
|
+
kind: 'absent';
|
|
1351
|
+
} | {
|
|
1352
|
+
kind: 'file_invalid';
|
|
1353
|
+
message: string;
|
|
1354
|
+
revision: string;
|
|
1355
|
+
};
|
|
1356
|
+
/** One line of the config file, as the declared mask shows it. */
|
|
1357
|
+
interface ConfigFileLine {
|
|
1358
|
+
n: number;
|
|
1359
|
+
text: string;
|
|
1360
|
+
}
|
|
1361
|
+
type MaskedConfigOutcome = {
|
|
1362
|
+
kind: 'ok';
|
|
1363
|
+
config: Record<string, unknown>;
|
|
1364
|
+
revision: string;
|
|
1365
|
+
lines: ConfigFileLine[];
|
|
1366
|
+
fileIssues: ConfigWriteIssue[];
|
|
1367
|
+
} | {
|
|
1368
|
+
kind: 'file_invalid';
|
|
1369
|
+
message: string;
|
|
1370
|
+
revision: string;
|
|
1371
|
+
};
|
|
1372
|
+
/** Run `work` in this path's turn. The queue moves on however `work` settles. */
|
|
1373
|
+
declare function serializeConfigWrite<T>(configPath: string, work: () => Promise<T>): Promise<T>;
|
|
1374
|
+
declare class ConfigFileInvalidError extends Error {
|
|
1375
|
+
constructor(message: string);
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* The config's values for a reader that only needs them (429-32, Pitfall 3): a missing file reads
|
|
1379
|
+
* as empty, and a file that does not parse is an error rather than the part of it yaml recovers.
|
|
1380
|
+
*/
|
|
1381
|
+
declare function readConfigValues(manager: ConfigManager): Promise<Record<string, unknown>>;
|
|
1382
|
+
declare class ConfigManager {
|
|
1383
|
+
private configPath;
|
|
1384
|
+
readonly mask: MaskFn;
|
|
1385
|
+
constructor(configPath: string, mask: MaskFn);
|
|
1386
|
+
static addsShownMarker(current: Uint8Array | undefined, next: Uint8Array): boolean;
|
|
1387
|
+
readWithRevision(): Promise<ConfigReadOutcome>;
|
|
1388
|
+
readMasked(): Promise<MaskedConfigOutcome>;
|
|
1389
|
+
private maskSummary;
|
|
1390
|
+
private readParsed;
|
|
1391
|
+
applyChanges(changes: readonly ConfigChange[], options?: {
|
|
1392
|
+
capturePriors?: boolean;
|
|
1393
|
+
}): Promise<ApplyChangesOutcome>;
|
|
1394
|
+
restorePriors(priors: ConfigPriors): Promise<RestorePriorsOutcome>;
|
|
1395
|
+
replaceDocument(expectedRevision: string, content: Uint8Array): Promise<ReplaceDocumentOutcome>;
|
|
1396
|
+
private serialized;
|
|
1397
|
+
private restorePriorsOnce;
|
|
1398
|
+
private applyChangesOnce;
|
|
1399
|
+
private writeAtomic;
|
|
1400
|
+
private writeTarget;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
type LiveSessionLLMConfig = DashboardLLMConfig & {
|
|
1404
|
+
vision?: {
|
|
1405
|
+
screenshotSize?: number;
|
|
1406
|
+
effectiveResolution?: number;
|
|
1407
|
+
};
|
|
1408
|
+
/** The model entry's own `vision` flag, when the config sets it (439 D-19). */
|
|
1409
|
+
modelVision?: boolean;
|
|
1410
|
+
};
|
|
1411
|
+
interface LiveDraftMetadata {
|
|
1412
|
+
testName: string;
|
|
1413
|
+
testContext?: string;
|
|
1414
|
+
}
|
|
1415
|
+
interface EntityRef {
|
|
1416
|
+
type: 'suite' | 'test';
|
|
1417
|
+
id: string;
|
|
1418
|
+
}
|
|
1419
|
+
interface LiveExecutionLog {
|
|
1420
|
+
id: string;
|
|
1421
|
+
type: 'hook' | 'appium-script' | 'runjs';
|
|
1422
|
+
name: string;
|
|
1423
|
+
hookId?: string | null;
|
|
1424
|
+
phase: 'setup' | 'teardown' | 'inline';
|
|
1425
|
+
status: 'passed' | 'failed';
|
|
1426
|
+
duration: number;
|
|
1427
|
+
stdout: string | null;
|
|
1428
|
+
stderr: string | null;
|
|
1429
|
+
returnData: unknown | null;
|
|
1430
|
+
variables: Record<string, string> | null;
|
|
1431
|
+
createdAt: string;
|
|
1432
|
+
}
|
|
1433
|
+
interface LiveSubActionData {
|
|
1434
|
+
index: number;
|
|
1435
|
+
observation: string;
|
|
1436
|
+
reasoning: string;
|
|
1437
|
+
plannedAction: unknown;
|
|
1438
|
+
result: 'success' | 'failure' | 'no-input' | 'in-progress';
|
|
1439
|
+
error?: string;
|
|
1440
|
+
screenStateBefore: string;
|
|
1441
|
+
screenStateAfter?: string;
|
|
1442
|
+
confidence?: number;
|
|
1443
|
+
verifierReasoning?: string;
|
|
1444
|
+
cached: boolean;
|
|
1445
|
+
tokenUsage?: {
|
|
1446
|
+
promptTokens: number;
|
|
1447
|
+
completionTokens: number;
|
|
1448
|
+
totalTokens: number;
|
|
1449
|
+
};
|
|
1450
|
+
phaseDurations?: {
|
|
1451
|
+
observe?: number;
|
|
1452
|
+
plan?: number;
|
|
1453
|
+
execute?: number;
|
|
1454
|
+
verify?: number;
|
|
1455
|
+
};
|
|
1456
|
+
annotation?: StepAnnotation;
|
|
1457
|
+
screenContextBefore?: string;
|
|
1458
|
+
screenContextAfter?: string;
|
|
1459
|
+
data?: unknown;
|
|
1460
|
+
}
|
|
1461
|
+
interface LiveStepResultPayload {
|
|
1462
|
+
status: 'passed' | 'failed' | 'cancelled';
|
|
1463
|
+
duration?: number;
|
|
1464
|
+
error?: string;
|
|
1465
|
+
capturedVariables?: Record<string, string>;
|
|
1466
|
+
variableSnapshot?: Record<string, {
|
|
1467
|
+
value: string;
|
|
1468
|
+
source: string;
|
|
1469
|
+
}>;
|
|
1470
|
+
originalStepName?: string;
|
|
1471
|
+
consoleLogs?: ConsoleLogEntry[];
|
|
1472
|
+
networkLogs?: NetworkLogEntry[];
|
|
1473
|
+
executionLogs?: LiveExecutionLog[];
|
|
1474
|
+
subActionsData?: LiveSubActionData[];
|
|
1475
|
+
}
|
|
1476
|
+
interface LiveSuiteHookOwner {
|
|
1477
|
+
scope: 'suite';
|
|
1478
|
+
}
|
|
1479
|
+
interface LiveTestOwner {
|
|
1480
|
+
scope: 'test';
|
|
1481
|
+
testExecutionId: string;
|
|
1482
|
+
testIndex: number;
|
|
1483
|
+
testId: string;
|
|
1484
|
+
testName: string;
|
|
1485
|
+
}
|
|
1486
|
+
type LiveHookOwner = LiveSuiteHookOwner | LiveTestOwner;
|
|
1487
|
+
interface LiveHookPayload {
|
|
1488
|
+
executionId: string;
|
|
1489
|
+
hookId: string;
|
|
1490
|
+
hookName: string;
|
|
1491
|
+
phase: 'setup' | 'teardown';
|
|
1492
|
+
owner: LiveHookOwner;
|
|
1493
|
+
status: 'running' | 'passed' | 'failed';
|
|
1494
|
+
duration?: number;
|
|
1495
|
+
stdout?: string | null;
|
|
1496
|
+
stderr?: string | null;
|
|
1497
|
+
variables?: Record<string, string> | null;
|
|
1498
|
+
error?: string;
|
|
1499
|
+
createdAt: string;
|
|
1500
|
+
}
|
|
1501
|
+
interface LiveTestExecutionPayload {
|
|
1502
|
+
testExecutionId: string;
|
|
1503
|
+
testIndex: number;
|
|
1504
|
+
testId: string;
|
|
1505
|
+
testName: string;
|
|
1506
|
+
}
|
|
1507
|
+
interface LiveTestStepPayload extends LiveTestExecutionPayload {
|
|
1508
|
+
stepIndex: number;
|
|
1509
|
+
stepInstruction: string;
|
|
1510
|
+
}
|
|
1511
|
+
interface LiveTestResultPayload {
|
|
1512
|
+
status: 'passed' | 'failed' | 'cancelled';
|
|
1513
|
+
duration: number;
|
|
1514
|
+
error?: string;
|
|
1515
|
+
setupHookExecutions: LiveHookPayload[];
|
|
1516
|
+
stepResults: LiveStepResultPayload[];
|
|
1517
|
+
teardownHookExecutions: LiveHookPayload[];
|
|
1518
|
+
}
|
|
1519
|
+
type ServerMessage = {
|
|
1520
|
+
type: 'session-ready';
|
|
1521
|
+
sessionId: string;
|
|
1522
|
+
platform: string;
|
|
1523
|
+
interactive: boolean;
|
|
1524
|
+
error?: string | null;
|
|
1525
|
+
} | {
|
|
1526
|
+
type: 'hook-start';
|
|
1527
|
+
hook: LiveHookPayload;
|
|
1528
|
+
} | {
|
|
1529
|
+
type: 'hook-complete';
|
|
1530
|
+
hook: LiveHookPayload;
|
|
1531
|
+
} | {
|
|
1532
|
+
type: 'step-phase';
|
|
1533
|
+
phase: string;
|
|
1534
|
+
data?: unknown;
|
|
1535
|
+
} | {
|
|
1536
|
+
type: 'step-complete';
|
|
1537
|
+
result: LiveStepResultPayload;
|
|
1538
|
+
} | {
|
|
1539
|
+
type: 'step-cancelled';
|
|
1540
|
+
} | {
|
|
1541
|
+
type: 'step-error';
|
|
1542
|
+
error: string;
|
|
1543
|
+
} | {
|
|
1544
|
+
type: 'test-start';
|
|
1545
|
+
test: LiveTestExecutionPayload;
|
|
1546
|
+
} | {
|
|
1547
|
+
type: 'test-step-start';
|
|
1548
|
+
step: LiveTestStepPayload;
|
|
1549
|
+
} | {
|
|
1550
|
+
type: 'test-step-phase';
|
|
1551
|
+
step: LiveTestStepPayload;
|
|
1552
|
+
phase: string;
|
|
1553
|
+
data?: unknown;
|
|
1554
|
+
} | {
|
|
1555
|
+
type: 'test-step-complete';
|
|
1556
|
+
step: LiveTestStepPayload;
|
|
1557
|
+
result: LiveStepResultPayload;
|
|
1558
|
+
} | {
|
|
1559
|
+
type: 'test-step-cancelled';
|
|
1560
|
+
step: LiveTestStepPayload;
|
|
1561
|
+
} | {
|
|
1562
|
+
type: 'test-step-error';
|
|
1563
|
+
step: LiveTestStepPayload;
|
|
1564
|
+
error: string;
|
|
1565
|
+
} | {
|
|
1566
|
+
type: 'test-complete';
|
|
1567
|
+
test: LiveTestExecutionPayload;
|
|
1568
|
+
result: LiveTestResultPayload;
|
|
1569
|
+
} | {
|
|
1570
|
+
type: 'test-error';
|
|
1571
|
+
test: LiveTestExecutionPayload;
|
|
1572
|
+
error: string;
|
|
1573
|
+
} | {
|
|
1574
|
+
type: 'session-state';
|
|
1575
|
+
state: SessionState;
|
|
1576
|
+
} | {
|
|
1577
|
+
type: 'session-terminated';
|
|
1578
|
+
} | {
|
|
1579
|
+
type: 'browser-disconnected';
|
|
1580
|
+
} | {
|
|
1581
|
+
type: 'error';
|
|
1582
|
+
message: string;
|
|
1583
|
+
} | {
|
|
1584
|
+
type: 'step-busy';
|
|
1585
|
+
currentStep: string;
|
|
1586
|
+
} | {
|
|
1587
|
+
type: 'navigate-complete';
|
|
1588
|
+
url: string | null;
|
|
1589
|
+
} | {
|
|
1590
|
+
type: 'device-logs';
|
|
1591
|
+
entries: Array<{
|
|
1592
|
+
level: string;
|
|
1593
|
+
message: string;
|
|
1594
|
+
timestamp: number;
|
|
1595
|
+
}>;
|
|
1596
|
+
} | {
|
|
1597
|
+
type: 'aria-tree';
|
|
1598
|
+
tree: string;
|
|
1599
|
+
};
|
|
1600
|
+
interface SessionState {
|
|
1601
|
+
sessionId: string;
|
|
1602
|
+
platform: 'web' | 'android' | 'ios';
|
|
1603
|
+
targetName?: string | null;
|
|
1604
|
+
status: 'idle' | 'executing' | 'disconnected';
|
|
1605
|
+
currentStep: string | null;
|
|
1606
|
+
currentUrl: string | null;
|
|
1607
|
+
stepsExecuted: number;
|
|
1608
|
+
createdAt: number;
|
|
1609
|
+
interactive: boolean;
|
|
1610
|
+
terminalError: string | null;
|
|
1611
|
+
}
|
|
1612
|
+
/**
|
|
1613
|
+
* What the create door found the current config declares about the session's target (432 D-18).
|
|
1614
|
+
*
|
|
1615
|
+
* It can only add a refusal: `initialize` refuses any name here unless the config it reads itself
|
|
1616
|
+
* opts in. There is deliberately no opt-in field, so nothing a caller passes can allow production.
|
|
1617
|
+
*/
|
|
1618
|
+
interface LiveProductionFinding {
|
|
1619
|
+
readonly productionTargetNames: readonly string[];
|
|
1620
|
+
}
|
|
1621
|
+
interface LiveSessionConfig {
|
|
1622
|
+
platform: 'web' | 'android' | 'ios';
|
|
1623
|
+
targetName?: string;
|
|
1624
|
+
production?: LiveProductionFinding;
|
|
1625
|
+
/** 436 D-03: resolved at the create door from the project config and the edited entity. */
|
|
1626
|
+
plannerLimits?: PlannerLimits;
|
|
1627
|
+
/** 436 D-03: per executed test, project then suite then the test's own `use`. */
|
|
1628
|
+
plannerLimitsForTest?: (testId: string) => Promise<PlannerLimits>;
|
|
1629
|
+
llmConfig: LiveSessionLLMConfig;
|
|
1630
|
+
authFetch?: typeof globalThis.fetch;
|
|
1631
|
+
envVars?: Record<string, string>;
|
|
1632
|
+
secretStore?: SecretStore;
|
|
1633
|
+
secretRedactor?: SecretRedactor;
|
|
1634
|
+
setupHooks?: string[];
|
|
1635
|
+
teardownHooks?: string[];
|
|
1636
|
+
resolvedHooks?: Map<string, HookDefinition>;
|
|
1637
|
+
hookRegistryError?: string;
|
|
1638
|
+
url?: string;
|
|
1639
|
+
headless?: boolean;
|
|
1640
|
+
useDeviceName?: string;
|
|
1641
|
+
appState?: 'preserve' | 'reset';
|
|
1642
|
+
bundleId?: string;
|
|
1643
|
+
appPackage?: string;
|
|
1644
|
+
appActivity?: string;
|
|
1645
|
+
device?: {
|
|
1646
|
+
name: string;
|
|
1647
|
+
platformVersion?: string;
|
|
1648
|
+
appPath?: string;
|
|
1649
|
+
browserName?: string;
|
|
1650
|
+
appPackage?: string;
|
|
1651
|
+
appActivity?: string;
|
|
1652
|
+
bundleId?: string;
|
|
1653
|
+
udid?: string;
|
|
1654
|
+
avd?: string;
|
|
1655
|
+
appiumUrl?: string;
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
/**
|
|
1659
|
+
* The close code the control socket uses for a session this process does not hold (428 P5), so the
|
|
1660
|
+
* page can say the session ended instead of "Disconnected". The dashboard UI spells the same number;
|
|
1661
|
+
* Plan 23 pins the two equal.
|
|
1662
|
+
*/
|
|
1663
|
+
declare const LIVE_EDITOR_UNKNOWN_SESSION_CLOSE_CODE = 4404;
|
|
1664
|
+
/**
|
|
1665
|
+
* The close code for a control socket a newer one has replaced (428 WR-01). The session is still
|
|
1666
|
+
* held; this socket no longer drives it, so the page must not reconnect and take it back. The
|
|
1667
|
+
* dashboard UI spells the same number.
|
|
1668
|
+
*/
|
|
1669
|
+
declare const LIVE_EDITOR_SUPERSEDED_CLOSE_CODE = 4409;
|
|
1670
|
+
|
|
1671
|
+
/** Why the session let go of a control socket: a newer one replaced it, or the session ended. */
|
|
1672
|
+
type LiveAttachmentRelease = 'superseded' | 'ended';
|
|
1673
|
+
interface LiveSessionDeps {
|
|
1674
|
+
appiumManager?: AppiumManager;
|
|
1675
|
+
configManager?: ConfigManager;
|
|
1676
|
+
configPath?: string;
|
|
1677
|
+
}
|
|
1678
|
+
interface LiveStepExecutionResult extends StepResult {
|
|
1679
|
+
executionLogs?: LiveExecutionLog[];
|
|
1680
|
+
subActionsData?: LiveSubActionData[];
|
|
1681
|
+
}
|
|
1682
|
+
declare class LiveSession {
|
|
1683
|
+
private deps;
|
|
1684
|
+
readonly sessionId: string;
|
|
1685
|
+
status: 'idle' | 'executing' | 'disconnected' | 'terminated';
|
|
1686
|
+
private abortController;
|
|
1687
|
+
stepsExecuted: number;
|
|
1688
|
+
createdAt: number;
|
|
1689
|
+
private adapter;
|
|
1690
|
+
private planner;
|
|
1691
|
+
private verifier;
|
|
1692
|
+
private modelProfiles;
|
|
1693
|
+
private storageStateStore;
|
|
1694
|
+
private model;
|
|
1695
|
+
private variableStore;
|
|
1696
|
+
private previousSteps;
|
|
1697
|
+
private currentStep;
|
|
1698
|
+
private platform;
|
|
1699
|
+
private targetName;
|
|
1700
|
+
private currentUrl;
|
|
1701
|
+
private executing;
|
|
1702
|
+
private idleTimer;
|
|
1703
|
+
private _modelName;
|
|
1704
|
+
private vision;
|
|
1705
|
+
private visionGate;
|
|
1706
|
+
private plannerLimits;
|
|
1707
|
+
private plannerLimitsForTest;
|
|
1708
|
+
private readyForInteraction;
|
|
1709
|
+
private terminalError;
|
|
1710
|
+
private setupHooks;
|
|
1711
|
+
private teardownHooks;
|
|
1712
|
+
private resolvedHooks;
|
|
1713
|
+
private hookRegistryError;
|
|
1714
|
+
private pendingMessages;
|
|
1715
|
+
private messageSink;
|
|
1716
|
+
private hookCaptures;
|
|
1717
|
+
private mobileAppiumLease;
|
|
1718
|
+
private secretStore;
|
|
1719
|
+
private secretRedactor;
|
|
1720
|
+
private liveHub;
|
|
1721
|
+
private attachment;
|
|
1722
|
+
private resolveEnded;
|
|
1723
|
+
private readonly ended;
|
|
1724
|
+
private cleanupRun;
|
|
1725
|
+
private attachmentGeneration;
|
|
1726
|
+
private releaseAttachment;
|
|
1727
|
+
constructor(sessionId: string, deps?: LiveSessionDeps);
|
|
1728
|
+
get modelName(): string;
|
|
1729
|
+
/** What the editor-subject registry holds for this session: its hub, read at call time. */
|
|
1730
|
+
liveAttachment(): LiveEditorAttachment;
|
|
1731
|
+
private openLiveHub;
|
|
1732
|
+
/**
|
|
1733
|
+
* Make a control socket the current one. `release` is how the session lets go of it later: the
|
|
1734
|
+
* previous attachment's is called with `superseded` here (428 WR-01), so one socket drives the
|
|
1735
|
+
* session at a time.
|
|
1736
|
+
*/
|
|
1737
|
+
beginAttachment(release?: (reason: LiveAttachmentRelease) => void): number;
|
|
1738
|
+
isCurrentAttachment(generation: number): boolean;
|
|
1739
|
+
attachMessageSink(sink: ((message: ServerMessage) => void) | null): void;
|
|
1740
|
+
/**
|
|
1741
|
+
* A command's reply, for whichever control socket is attached now (428 CR-01). A command can
|
|
1742
|
+
* outlive the socket that issued it, and its outcome belongs to the page, not to that socket:
|
|
1743
|
+
* with none attached it is held and flushed to the next one.
|
|
1744
|
+
*/
|
|
1745
|
+
deliver(message: ServerMessage): void;
|
|
1746
|
+
private dispatch;
|
|
1747
|
+
private redact;
|
|
1748
|
+
private resolveSecrets;
|
|
1749
|
+
private emitHookStart;
|
|
1750
|
+
private emitHookComplete;
|
|
1751
|
+
private emitTestStepStart;
|
|
1752
|
+
private emitTestStepPhase;
|
|
1753
|
+
private emitTestStepComplete;
|
|
1754
|
+
private emitTestStepError;
|
|
1755
|
+
private getHookEnvVars;
|
|
1756
|
+
private runLifecycleHooks;
|
|
1757
|
+
executeHookCommand(phase: 'setup' | 'teardown', hookId: string): Promise<LiveExecutionLog>;
|
|
1758
|
+
private releaseMobileAppiumLease;
|
|
1759
|
+
private legacyMobilePlatformConfig;
|
|
1760
|
+
private resolveMobilePlatformConfig;
|
|
1761
|
+
private setupMobileAdapter;
|
|
1762
|
+
initialize(config: LiveSessionConfig): Promise<void>;
|
|
1763
|
+
private resolveStepInstruction;
|
|
1764
|
+
private buildPreparationFailureResult;
|
|
1765
|
+
private getStorageStateStore;
|
|
1766
|
+
private executeStepInternal;
|
|
1767
|
+
executeStepCommand(stepInstruction: string, stepIndex?: number, onPhase?: (event: StepPhaseEvent) => void, draft?: LiveDraftMetadata): Promise<LiveStepExecutionResult>;
|
|
1768
|
+
executeTestCommand(testExecutionId: string, testDraft: {
|
|
1769
|
+
testIndex: number;
|
|
1770
|
+
testId: string;
|
|
1771
|
+
testName: string;
|
|
1772
|
+
testContext?: string;
|
|
1773
|
+
steps: string[];
|
|
1774
|
+
setup: string[];
|
|
1775
|
+
teardown: string[];
|
|
1776
|
+
}, onPhase?: (event: StepPhaseEvent) => void): Promise<LiveTestResultPayload>;
|
|
1777
|
+
private runLifecycleHooksCapturing;
|
|
1778
|
+
captureWebAuthState(name: string, options?: {
|
|
1779
|
+
replace?: boolean;
|
|
1780
|
+
}): Promise<AuthStateMetadata>;
|
|
1781
|
+
cancelStep(): void;
|
|
1782
|
+
navigate(url: string): Promise<string | null>;
|
|
1783
|
+
refreshPage(): Promise<string | null>;
|
|
1784
|
+
goBack(): Promise<string | null>;
|
|
1785
|
+
goForward(): Promise<string | null>;
|
|
1786
|
+
drainDeviceLogs(): Promise<Array<{
|
|
1787
|
+
level: string;
|
|
1788
|
+
message: string;
|
|
1789
|
+
timestamp: number;
|
|
1790
|
+
}>>;
|
|
1791
|
+
getState(): SessionState;
|
|
1792
|
+
getAriaTree(): Promise<string | null>;
|
|
1793
|
+
startIdleTimer(onExpire: () => void): void;
|
|
1794
|
+
clearIdleTimer(): void;
|
|
1795
|
+
/** Idempotent: every later or concurrent call shares the first run, so teardown happens once. */
|
|
1796
|
+
cleanup(): Promise<void>;
|
|
1797
|
+
private runCleanup;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
interface SessionManagerDeps {
|
|
1801
|
+
appiumManager?: AppiumManager;
|
|
1802
|
+
configManager?: ConfigManager;
|
|
1803
|
+
configPath?: string;
|
|
1804
|
+
liveEditorSubjects?: LiveEditorSubjects;
|
|
1805
|
+
}
|
|
1806
|
+
declare class SessionManager {
|
|
1807
|
+
private deps;
|
|
1808
|
+
private sessions;
|
|
1809
|
+
private counters;
|
|
1810
|
+
constructor(deps?: SessionManagerDeps);
|
|
1811
|
+
createSession(config: LiveSessionConfig, entity?: EntityRef): Promise<{
|
|
1812
|
+
sessionId: string;
|
|
1813
|
+
sessionNumber: number | null;
|
|
1814
|
+
}>;
|
|
1815
|
+
getSession(sessionId: string): LiveSession | undefined;
|
|
1816
|
+
listSessions(): SessionState[];
|
|
1817
|
+
terminateSession(sessionId: string): Promise<boolean>;
|
|
1818
|
+
cleanupAll(): Promise<void>;
|
|
1819
|
+
getCounter(entity: EntityRef): number;
|
|
1820
|
+
get size(): number;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
interface EnqueueOptions {
|
|
1824
|
+
name: string;
|
|
1825
|
+
filePath?: string;
|
|
1826
|
+
kind?: RunArtifactKind;
|
|
1827
|
+
attributes?: RunAttributes;
|
|
1828
|
+
priority?: number;
|
|
1829
|
+
platform?: string;
|
|
1830
|
+
testFileContent?: string;
|
|
1831
|
+
modelName?: string;
|
|
1832
|
+
llmProvider?: string;
|
|
1833
|
+
metadata?: Record<string, unknown>;
|
|
1834
|
+
parallel?: boolean;
|
|
1835
|
+
}
|
|
1836
|
+
type RunCancelOutcome = {
|
|
1837
|
+
kind: 'cancelled';
|
|
1838
|
+
} | {
|
|
1839
|
+
kind: 'stop-requested';
|
|
1840
|
+
} | {
|
|
1841
|
+
kind: 'not-stoppable-here';
|
|
1842
|
+
} | {
|
|
1843
|
+
kind: 'finished';
|
|
1844
|
+
status: string;
|
|
1845
|
+
} | {
|
|
1846
|
+
kind: 'orphan-finalized';
|
|
1847
|
+
message: string;
|
|
1848
|
+
} | {
|
|
1849
|
+
kind: 'owned-elsewhere';
|
|
1850
|
+
message: string;
|
|
1851
|
+
ownerPid: number;
|
|
1852
|
+
} | {
|
|
1853
|
+
kind: 'not-cancellable';
|
|
1854
|
+
};
|
|
1855
|
+
declare class JobQueue extends EventEmitter {
|
|
1856
|
+
private db;
|
|
1857
|
+
private pollInterval;
|
|
1858
|
+
private isProcessing;
|
|
1859
|
+
private concurrency;
|
|
1860
|
+
private activeCount;
|
|
1861
|
+
private activePlatforms;
|
|
1862
|
+
private activeRuns;
|
|
1863
|
+
private requestStop;
|
|
1864
|
+
private stopped;
|
|
1865
|
+
constructor(opts: {
|
|
1866
|
+
db: DashboardDatabase;
|
|
1867
|
+
concurrency?: number;
|
|
1868
|
+
requestStop?: (runId: string) => boolean;
|
|
1869
|
+
});
|
|
1870
|
+
start(intervalMs?: number): void;
|
|
1871
|
+
stop(): void;
|
|
1872
|
+
enqueue(opts: EnqueueOptions): string;
|
|
1873
|
+
processNext(): void;
|
|
1874
|
+
onSlotFreed(runId: string): void;
|
|
1875
|
+
private dispatchReported;
|
|
1876
|
+
getActiveCount(): number;
|
|
1877
|
+
getConcurrency(): number;
|
|
1878
|
+
cancel(runId: string): RunCancelOutcome;
|
|
1879
|
+
private finishedOrMissing;
|
|
1880
|
+
private findNextEligible;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
interface TestFileInfo {
|
|
1884
|
+
path: string;
|
|
1885
|
+
name: string;
|
|
1886
|
+
testId: string | null;
|
|
1887
|
+
targetName: string | null;
|
|
1888
|
+
platform: string | null;
|
|
1889
|
+
modified: string;
|
|
1890
|
+
}
|
|
1891
|
+
interface TestValidationError {
|
|
1892
|
+
message: string;
|
|
1893
|
+
line?: number;
|
|
1894
|
+
column?: number;
|
|
1895
|
+
suggestion?: string;
|
|
1896
|
+
}
|
|
1897
|
+
interface TestValidationResult {
|
|
1898
|
+
valid: boolean;
|
|
1899
|
+
errors: TestValidationError[];
|
|
1900
|
+
}
|
|
1901
|
+
declare class TestFileManager {
|
|
1902
|
+
private readonly workspace;
|
|
1903
|
+
constructor(workspace: ResolvedWorkspacePaths);
|
|
1904
|
+
private readonly underLock;
|
|
1905
|
+
list(): Promise<TestFileInfo[]>;
|
|
1906
|
+
read(relativePath: string): Promise<string>;
|
|
1907
|
+
findByTestId(testId: string): Promise<{
|
|
1908
|
+
path: string;
|
|
1909
|
+
content: string;
|
|
1910
|
+
} | null>;
|
|
1911
|
+
write(relativePath: string, content: string): Promise<void>;
|
|
1912
|
+
delete(relativePath: string): Promise<void>;
|
|
1913
|
+
validate(content: string): Promise<TestValidationResult>;
|
|
1914
|
+
private resolveTarget;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
interface SuiteFileInfo {
|
|
1918
|
+
path: string;
|
|
1919
|
+
suiteId: string | null;
|
|
1920
|
+
name: string;
|
|
1921
|
+
testCount: number;
|
|
1922
|
+
modified: string;
|
|
1923
|
+
platform: string | null;
|
|
1924
|
+
}
|
|
1925
|
+
interface SuiteValidationResult {
|
|
1926
|
+
valid: boolean;
|
|
1927
|
+
errors: {
|
|
1928
|
+
message: string;
|
|
1929
|
+
line?: number;
|
|
1930
|
+
column?: number;
|
|
1931
|
+
suggestion?: string;
|
|
1932
|
+
}[];
|
|
1933
|
+
missingTests?: Array<{
|
|
1934
|
+
index: number;
|
|
1935
|
+
test: string;
|
|
1936
|
+
id: string;
|
|
1937
|
+
}>;
|
|
1938
|
+
}
|
|
1939
|
+
declare class SuiteFileManager {
|
|
1940
|
+
private readonly workspace;
|
|
1941
|
+
private testFileManager;
|
|
1942
|
+
constructor(workspace: ResolvedWorkspacePaths, testFileManager?: TestFileManager);
|
|
1943
|
+
private readonly underLock;
|
|
1944
|
+
list(): Promise<SuiteFileInfo[]>;
|
|
1945
|
+
findBySuiteId(suiteId: string): Promise<{
|
|
1946
|
+
path: string;
|
|
1947
|
+
content: string;
|
|
1948
|
+
} | null>;
|
|
1949
|
+
resolvePath(filePath: string, requireExisting?: boolean): Promise<{
|
|
1950
|
+
storagePath: string;
|
|
1951
|
+
executionPath: string;
|
|
1952
|
+
}>;
|
|
1953
|
+
read(relativePath: string): Promise<string>;
|
|
1954
|
+
write(relativePath: string, content: string): Promise<void>;
|
|
1955
|
+
delete(relativePath: string): Promise<void>;
|
|
1956
|
+
validate(content: string): Promise<SuiteValidationResult>;
|
|
1957
|
+
private resolveTarget;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
type DashboardRuntimeLLMConfig = DashboardLLMConfig & {
|
|
1961
|
+
vision?: DashboardVisionConfig;
|
|
1962
|
+
modelVision?: boolean;
|
|
1963
|
+
};
|
|
1964
|
+
type DashboardVisionConfig = {
|
|
1965
|
+
screenshotSize?: number;
|
|
1966
|
+
effectiveResolution?: number;
|
|
1967
|
+
};
|
|
1968
|
+
interface RouterDeps {
|
|
1969
|
+
db: DashboardDatabase;
|
|
1970
|
+
workspacePaths?: ResolvedWorkspacePaths;
|
|
1971
|
+
/**
|
|
1972
|
+
* The injected run backend (401-07, D-09).
|
|
1973
|
+
*
|
|
1974
|
+
* TYPED AGAINST THE INTERFACE, AND NOW ACTUALLY SEPARABLE. It always meant to be, and was not:
|
|
1975
|
+
* the retired live-event route reached past the interface for a per-run replay buffer only the
|
|
1976
|
+
* concrete class declared, so the seam had exactly one possible implementation. That route and
|
|
1977
|
+
* that buffer are gone (405-07, D-19); what this router asks of a backend is the interface.
|
|
1978
|
+
*/
|
|
1979
|
+
executionBackend?: ExecutionBackend;
|
|
1980
|
+
jobQueue?: JobQueue;
|
|
1981
|
+
testFileManager?: TestFileManager;
|
|
1982
|
+
suiteFileManager?: SuiteFileManager;
|
|
1983
|
+
configManager?: ConfigManager;
|
|
1984
|
+
configPath?: string;
|
|
1985
|
+
declaredSecretMask?: MaskFn;
|
|
1986
|
+
llmConfig?: DashboardRuntimeLLMConfig;
|
|
1987
|
+
authFetch?: typeof globalThis.fetch;
|
|
1988
|
+
sessionManager?: SessionManager;
|
|
1989
|
+
/**
|
|
1990
|
+
* The composed agent host (400-01, D-01). It serves run detail, marks, analysis, knowledge and
|
|
1991
|
+
* post-processing. (The run-confirm pair it also served is deleted, 432-14.)
|
|
1992
|
+
*
|
|
1993
|
+
* INJECTED, and optional because a router built the legacy two-argument way
|
|
1994
|
+
* (`createRouter(db)`) has none. Each arm that needs it then answers by name rather than 404 —
|
|
1995
|
+
* the route exists, this server was started without a host. See `chat/host.ts` for why the
|
|
1996
|
+
* composition happens in the CLI command that starts this server rather than here.
|
|
1997
|
+
*/
|
|
1998
|
+
agentHost?: AgentHost;
|
|
1999
|
+
/**
|
|
2000
|
+
* The run service's readiness predicate, served on the LOCAL door (402-05, D-15).
|
|
2001
|
+
*
|
|
2002
|
+
* THE SAME FUNCTION THE RUN SERVICE'S `/ok` SERVES — one readiness spelling behind two doors,
|
|
2003
|
+
* which is the whole of D-15. It is passed in rather than computed here for the reason `agentHost`
|
|
2004
|
+
* is: every fact in it (is a host composed) is known only to the process that composed it, and a
|
|
2005
|
+
* copy made here is how the run service and the dashboard start disagreeing about whether this
|
|
2006
|
+
* process can run anything.
|
|
2007
|
+
*
|
|
2008
|
+
* ABSENT IS THE LEGACY MOUNT, NOT AN UNCONFIGURED SERVICE. `agent-qa dashboard` ALWAYS supplies
|
|
2009
|
+
* this, because the browser preflight needs an honest answer and a 404 is not one. A router built the legacy
|
|
2010
|
+
* two-argument way has none, and that arm answers 503 by name on `agentHost`'s idiom.
|
|
2011
|
+
*/
|
|
2012
|
+
runServiceReadiness?: () => RunServiceReadiness;
|
|
2013
|
+
/**
|
|
2014
|
+
* The started run's session, or nothing — the LOCAL door's attach port (402-08, D-01).
|
|
2015
|
+
*
|
|
2016
|
+
* THE SAME PORT THE AUTHED DOOR USES, and `server.ts` supplies it by reading
|
|
2017
|
+
* `runService.attach` rather than by composing a second lookup: one session registry serves both
|
|
2018
|
+
* doors, so a run the remote door can attach to is a run this door can attach to, and there is
|
|
2019
|
+
* no second retention window to disagree about.
|
|
2020
|
+
*
|
|
2021
|
+
* ABSENT MEANS NO RUN SERVICE WAS MOUNTED, which is the same fact as "this process composed no
|
|
2022
|
+
* session registry" — unlike `runServiceReadiness` above, whose absence is a legacy mount rather
|
|
2023
|
+
* than an unconfigured service. The route then answers 503 BY NAME on the execution routes' own
|
|
2024
|
+
* degradation idiom, never a 404 (the route exists) and never an empty stream (which would look
|
|
2025
|
+
* like a run that emitted nothing).
|
|
2026
|
+
*/
|
|
2027
|
+
runServiceAttach?: (runId: string) => RunSession | undefined;
|
|
2028
|
+
/**
|
|
2029
|
+
* The `runs.liveurl` mint, DISPATCHED THROUGH THE REGISTRY by the composition root (403 D-03).
|
|
2030
|
+
*
|
|
2031
|
+
* A FUNCTION, not a signer: this router holds no key and mints nothing. What it does is turn one
|
|
2032
|
+
* HTTP shape into one op call and the op's refusals into status codes — which is the whole of a
|
|
2033
|
+
* thin client over the function layer.
|
|
2034
|
+
*
|
|
2035
|
+
* ABSENT MEANS THIS HOST SERVES NO LIVE VIEW, and the route answers 503 BY NAME on
|
|
2036
|
+
* `runServiceReadiness`'s idiom — never a 404 (the route exists) and never a URL to nowhere.
|
|
2037
|
+
*/
|
|
2038
|
+
mintLiveUrl?: (runId: string, handle: number) => Promise<RunsLiveUrlResult>;
|
|
2039
|
+
/**
|
|
2040
|
+
* The `editor.liveurl` mint, dispatched through the registry by the composition root (428 D-07).
|
|
2041
|
+
* The editor counterpart of `mintLiveUrl`: a function, never a signer. ABSENT MEANS THIS HOST
|
|
2042
|
+
* MINTS NO EDITOR VIEW, and the route answers 503 by name.
|
|
2043
|
+
*/
|
|
2044
|
+
mintEditorLiveUrl?: (sessionId: string) => Promise<EditorLiveUrlResult>;
|
|
2045
|
+
/**
|
|
2046
|
+
* The in-process chip sink the post-processing stream relays from (404-05, D-16).
|
|
2047
|
+
*
|
|
2048
|
+
* INJECTED ON `agentHost`'s OWN IDIOM. The publisher is the CLI's post-run chip emitter — a
|
|
2049
|
+
* package this one may not import — so `agent-qa dashboard` creates the relay, hands
|
|
2050
|
+
* `relay.publish` to the run door's bridge and hands the relay itself here. One object, two
|
|
2051
|
+
* halves of the same process; a relay composed here instead would be a registry the emitter
|
|
2052
|
+
* never reaches, and the stream would heartbeat forever at a page whose chips do move.
|
|
2053
|
+
*
|
|
2054
|
+
* ABSENT MEANS THIS SERVER HAS NO PUSH CHANNEL and the stream arm answers 503 BY NAME — never a
|
|
2055
|
+
* 404 (the route exists) and never an empty stream, which would look like a chain that never
|
|
2056
|
+
* ticked. The READ route is unaffected: polling works with no relay at all.
|
|
2057
|
+
*/
|
|
2058
|
+
postProcessingRelay?: PostProcessingRelay;
|
|
2059
|
+
}
|
|
2060
|
+
declare function createRouter(deps: RouterDeps): (req: IncomingMessage, res: ServerResponse) => void;
|
|
2061
|
+
declare function createRouter(db: DashboardDatabase): (req: IncomingMessage, res: ServerResponse) => void;
|
|
2062
|
+
|
|
2063
|
+
declare function readJsonBody<T>(req: IncomingMessage, maxBytes?: number): Promise<T>;
|
|
2064
|
+
|
|
2065
|
+
/**
|
|
2066
|
+
* The request URL, or `undefined` when the request line is not one.
|
|
2067
|
+
*
|
|
2068
|
+
* THE ONE PRODUCER. Its whole contract is that it never throws, which is what lets each caller
|
|
2069
|
+
* answer in its own vocabulary — a 400 body, a JSON envelope, a destroyed socket — instead of
|
|
2070
|
+
* every frame growing its own `try`.
|
|
2071
|
+
*/
|
|
2072
|
+
declare function tryParseRequestUrl(req: Pick<IncomingMessage, 'url'>): URL | undefined;
|
|
2073
|
+
/**
|
|
2074
|
+
* Refuse a request whose target could not be parsed, in FIXED BYTES.
|
|
2075
|
+
*
|
|
2076
|
+
* NOTHING FROM THE REQUEST REACHES THIS RESPONSE, and that is deliberate rather than terse. The
|
|
2077
|
+
* refusal exists precisely because the request target is malformed and attacker-chosen; echoing it
|
|
2078
|
+
* would put attacker bytes into a body, and formatting it would give the refusal its own way to
|
|
2079
|
+
* throw. The previous round recorded the same lesson one layer down: a backstop that answers
|
|
2080
|
+
* through the door's own responder can re-enter the fault it was catching.
|
|
2081
|
+
*/
|
|
2082
|
+
declare function refuseUnparseableRequest(res: ServerResponse): void;
|
|
2083
|
+
/**
|
|
2084
|
+
* The process-level backstop's answer: a 500 in fixed bytes, or a destroyed socket if it is late.
|
|
2085
|
+
*
|
|
2086
|
+
* THE ERROR IS NOT PRINTED AND NOT SERIALIZED. Phase 395 measured a planted credential leaving this
|
|
2087
|
+
* product through an error's serialization rather than its `message`, so what goes to stderr here
|
|
2088
|
+
* is the error's CONSTRUCTOR NAME and the request METHOD — two values with no attacker- or
|
|
2089
|
+
* secret-derived content — and never the URL, which is attacker-chosen, or the error itself, whose
|
|
2090
|
+
* `cause` chain is exactly the surface that leaked.
|
|
2091
|
+
*/
|
|
2092
|
+
declare function backstopServerError(res: ServerResponse, req: Pick<IncomingMessage, 'method'>, error: unknown): void;
|
|
2093
|
+
|
|
2094
|
+
export { createRouter as $, type AgentHost as A, type RunRow as B, ConfigFileInvalidError as C, DashboardDatabase as D, type ExecutionBackend as E, type RunServicePorts as F, type RunServiceRouteHandlerName as G, type RunServiceRouteMatch as H, type RunServiceRouteRow as I, type RunServiceRouter as J, type RunServiceStartInput as K, type LiveEditorAttachment as L, type MintLiveUrlInput as M, type RunSettledDetails as N, type RunSettledStatus as O, type PreflightOptions as P, RunsDatabaseVersionRefusedError as Q, RUN_SERVICE_ANALYSIS_TEMPLATE as R, TestFileManager as S, type TestFileInfo as T, type TestValidationError as U, type TestValidationResult as V, backstopServerError as W, classifyRunOwner as X, completeRunOwnership as Y, createLiveEditorSubjects as Z, createPostProcessingRelay as _, type ExecuteOptions as a, createRunServiceRoutes as a0, defaultRunOwnership as a1, matchRunServiceRoute as a2, orphanSummary as a3, ownedElsewhere as a4, ownsRunServicePath as a5, readConfigValues as a6, readJsonBody as a7, refuseUnparseableRequest as a8, runDenialOf as a9, serializeConfigWrite as aa, startServer as ab, tryParseRequestUrl as ac, type PostProcessingRelay as b, type AgentRegistryHandle as c, AppiumManager as d, ConfigManager as e, LIVE_EDITOR_SUPERSEDED_CLOSE_CODE as f, LIVE_EDITOR_UNKNOWN_SESSION_CLOSE_CODE as g, type LiveEditorSubjects as h, type PostProcessingListener as i, RUN_SERVICE_ARTIFACT_TEMPLATE as j, RUN_SERVICE_DETAIL_TEMPLATE as k, RUN_SERVICE_EVENTS_TEMPLATE as l, RUN_SERVICE_EVIDENCE_TEMPLATE as m, RUN_SERVICE_LIVE_URL_TEMPLATE as n, RUN_SERVICE_MARKS_TEMPLATE as o, RUN_SERVICE_MARK_PREVIEW_TEMPLATE as p, RUN_SERVICE_OK_ROUTE as q, RUN_SERVICE_ROUTE_TABLE as r, RUN_SERVICE_START_ROUTE as s, type ReplaceDocumentOutcome as t, type ResumePoint as u, type RunOwner as v, type RunOwnerProbe as w, type RunOwnerState as x, type RunOwnership as y, type RunOwnershipInput as z };
|