@agentswf/engine 0.1.0-beta.5 → 0.1.0
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/LICENSE +21 -0
- package/README.md +16 -0
- package/package.json +13 -9
- package/src/accounting/format.ts +4 -3
- package/src/accounting/spent.ts +128 -0
- package/src/accounting/summary.ts +40 -32
- package/src/agent-cli/cli.ts +28 -55
- package/src/agent-cli/client.ts +1 -27
- package/src/allowance-command.ts +8 -10
- package/src/attempt-ending.ts +4 -4
- package/src/attempt-output.ts +13 -31
- package/src/attempt-view.ts +33 -32
- package/src/author-typebox.ts +13 -0
- package/src/client/checks.ts +94 -0
- package/src/client/exported.ts +17 -0
- package/src/client/index.ts +122 -0
- package/src/client/locate.ts +34 -0
- package/src/client/published.ts +3 -0
- package/src/client/runs.ts +337 -0
- package/src/command-line.ts +64 -0
- package/src/commands.ts +7 -1
- package/src/control-plane.ts +4 -6
- package/src/coordinator-prompt.ts +4 -3
- package/src/coordinator-socket.ts +27 -28
- package/src/deadlines.ts +6 -40
- package/src/decisions/directory.ts +50 -37
- package/src/decisions/fake.ts +1 -1
- package/src/decisions/openrouter.ts +4 -2
- package/src/decisions/seam.ts +4 -4
- package/src/display-path.ts +3 -3
- package/src/duration.ts +19 -33
- package/src/emit-command.ts +38 -22
- package/src/emit.ts +148 -59
- package/src/event-ids.ts +31 -0
- package/src/files.ts +44 -4
- package/src/hand-over.ts +150 -0
- package/src/help/allowance.txt +4 -3
- package/src/help/emit.txt +11 -7
- package/src/help/inbox.txt +5 -3
- package/src/help/logs.txt +7 -5
- package/src/help/open.txt +29 -0
- package/src/help/reply.txt +6 -8
- package/src/help/run.txt +13 -8
- package/src/help/runs.txt +4 -2
- package/src/help/tick.txt +9 -4
- package/src/herdr-placement.ts +3 -33
- package/src/herdr-run-session.ts +5 -13
- package/src/herdr-workspace-marks.ts +7 -17
- package/src/holder-command.ts +54 -136
- package/src/in-lines.ts +28 -0
- package/src/inbox-command.ts +42 -56
- package/src/index.ts +1 -1
- package/src/init-command.ts +8 -17
- package/src/init-template/hello/workflow.ts.template +3 -2
- package/src/jsonl.ts +10 -6
- package/src/keep-alive.ts +7 -19
- package/src/logs-command.ts +134 -53
- package/src/message-operation.ts +6 -3
- package/src/messaging/messaging.ts +8 -10
- package/src/messaging/text.ts +2 -2
- package/src/open-command.ts +770 -0
- package/src/operation-events.ts +4 -6
- package/src/operation-liveness.ts +19 -4
- package/src/operator-cli.ts +206 -90
- package/src/operator-runtime.ts +24 -2
- package/src/ops.ts +270 -0
- package/src/pane-layout.ts +49 -96
- package/src/progress-view.ts +2 -2
- package/src/project-engine.ts +79 -0
- package/src/projects.ts +13 -2
- package/src/refusals.ts +119 -0
- package/src/result-slots.ts +11 -31
- package/src/result-validation.ts +2 -2
- package/src/run-command.ts +27 -31
- package/src/run-commands.ts +42 -114
- package/src/run-event-log.ts +1 -1
- package/src/run-lines.ts +159 -0
- package/src/run-lock.ts +57 -1
- package/src/run-policy/stage-plan.ts +154 -0
- package/src/run-policy/stages.ts +113 -0
- package/src/run-policy/waits.ts +452 -0
- package/src/run-prepare.ts +165 -41
- package/src/run-read.ts +109 -41
- package/src/run-state.ts +170 -50
- package/src/run-text.ts +54 -0
- package/src/run-unreachable.ts +18 -0
- package/src/run-usage.ts +17 -11
- package/src/run-view.ts +73 -14
- package/src/runs-command.ts +81 -63
- package/src/runs.ts +284 -222
- package/src/sandbox-homes.ts +65 -13
- package/src/sandboxes.ts +182 -28
- package/src/session-lines.ts +93 -0
- package/src/skills/run-skills.ts +8 -1
- package/src/sources.ts +2 -39
- package/src/spend.ts +333 -0
- package/src/stage-ledger.ts +111 -153
- package/src/stop-crashed.ts +6 -5
- package/src/stopped.ts +4 -4
- package/src/test-command.ts +37 -19
- package/src/testing/operation-events.ts +3 -23
- package/src/testing.ts +66 -18
- package/src/tick-command.ts +145 -15
- package/src/tools.ts +11 -30
- package/src/turn-ends.ts +37 -0
- package/src/waits.ts +651 -0
- package/src/wake.ts +84 -54
- package/src/workflow-loader.ts +49 -2
- package/src/workflow-runner.ts +525 -618
- package/src/workflow-test-preload.ts +4 -1
- package/src/workflow-testing/coordinators.ts +1 -1
- package/src/workflow-testing/fake-client.ts +387 -0
- package/src/workflow-testing/host.ts +28 -29
- package/src/workflow-testing/index.ts +9 -27
- package/src/workflow-testing/run-test.ts +201 -286
- package/src/workflow-testing/script.ts +7 -39
- package/src/deprecations.ts +0 -177
- package/src/hold-run-lock.fixture.ts +0 -8
- package/src/mailbox.ts +0 -972
- package/src/stage-plan.ts +0 -186
- package/src/workflow-testing/coordinator.typecheck.ts +0 -54
- package/src/workflow-testing/workflow-testing.typecheck.ts +0 -67
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
-
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { appendFile, mkdir } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
type
|
|
6
|
+
type EventLine,
|
|
7
|
+
type RunLine,
|
|
8
|
+
type RunPanes,
|
|
9
|
+
readRun,
|
|
8
10
|
type SandboxRecord,
|
|
9
|
-
|
|
10
|
-
type StageNeed,
|
|
11
|
-
type StageRecord,
|
|
11
|
+
type StageLine,
|
|
12
12
|
} from "@agentswf/contract/records";
|
|
13
13
|
import {
|
|
14
14
|
AWF_ID_PREFIX,
|
|
@@ -20,31 +20,30 @@ import {
|
|
|
20
20
|
isEventWorkflow,
|
|
21
21
|
type JsonObject,
|
|
22
22
|
type JsonValue,
|
|
23
|
+
type KeepPane,
|
|
23
24
|
type OutputOf,
|
|
24
|
-
type ParticipantReply,
|
|
25
25
|
positiveDurationMilliseconds,
|
|
26
26
|
type RuntimeAliases,
|
|
27
27
|
WORKFLOW_KIND,
|
|
28
28
|
type Workflow,
|
|
29
29
|
} from "@agentswf/contract/workflow";
|
|
30
|
-
import {
|
|
30
|
+
import type { Harness } from "@agentswf/harness";
|
|
31
|
+
import { SCRIPTED_HOMES } from "@agentswf/harness/testing";
|
|
31
32
|
import { createFakeSandboxProvider } from "@agentswf/sandbox/testing/fake";
|
|
32
33
|
import { decideEnding, type Finished } from "../attempt-ending";
|
|
33
34
|
import { continueCommand } from "../attempt-view";
|
|
34
|
-
import { type Deprecated, RunDeprecations } from "../deprecations";
|
|
35
35
|
import { checkEvent, eventIdProblem, replyRecord } from "../emit";
|
|
36
36
|
import { messageOf } from "../errors";
|
|
37
|
-
import {
|
|
37
|
+
import { isInside } from "../files";
|
|
38
|
+
import { appendIn, readIn } from "../in-lines";
|
|
38
39
|
import { OPERATOR_ALIASES } from "../operator-aliases";
|
|
39
|
-
import {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
} from "../runs";
|
|
47
|
-
import { primaryFailure, WorkflowStopped } from "../stopped";
|
|
40
|
+
import { type EarlierOps, listOps } from "../ops";
|
|
41
|
+
import { KEEP_PANES } from "../pane-layout";
|
|
42
|
+
import { claimRunLines, type RunLines, readRunLines, runLinesFile } from "../run-lines";
|
|
43
|
+
import type { TimerState } from "../run-policy/waits";
|
|
44
|
+
import { createRun, idProblem, readEvents, readTurns } from "../runs";
|
|
45
|
+
import { primaryFailure, type WorkflowStopped } from "../stopped";
|
|
46
|
+
import { RunParked, readWaits } from "../waits";
|
|
48
47
|
import { assertJsonValue, type EventWorkflow } from "../workflow-loader";
|
|
49
48
|
import type { RunnableWorkflow } from "../workflow-runner";
|
|
50
49
|
import { runWorkflow, type SettledRun, WorkflowRunError } from "../workflow-runner";
|
|
@@ -112,18 +111,27 @@ type EventOptions<Events extends EventSchemas> = [Events] extends [never]
|
|
|
112
111
|
events?: readonly ScriptedEvent<Events>[];
|
|
113
112
|
};
|
|
114
113
|
|
|
115
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* What an earlier attempt left: given, the test's attempt is that run's continue, the attempt after
|
|
116
|
+
* the latest its lines name, attempt 2 when they name none later than 1.
|
|
117
|
+
*/
|
|
116
118
|
export type EarlierAttempt = {
|
|
117
119
|
/**
|
|
118
120
|
* The stages it recorded, by name: each value, or `undefined` for a stage that returns nothing,
|
|
119
|
-
* reused without calling their work, as `awf run --continue` would.
|
|
121
|
+
* reused without calling their work, as `awf run --continue` would. Written as attempt 1's
|
|
122
|
+
* `stage` lines.
|
|
120
123
|
*/
|
|
121
124
|
stages?: Readonly<Record<string, JsonValue | undefined>>;
|
|
122
125
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
126
|
+
* The events senders gave the run, its `in.jsonl`, in order. A starting `event` is appended
|
|
127
|
+
* after them, as the event that woke it.
|
|
128
|
+
*/
|
|
129
|
+
events?: readonly EventLine[];
|
|
130
|
+
/**
|
|
131
|
+
* What the run settled, its `run.jsonl` lines, in order, after `stages`: its timers, what its
|
|
132
|
+
* waits and races took, and the turns it began, as `run.run` gives them.
|
|
125
133
|
*/
|
|
126
|
-
|
|
134
|
+
run?: readonly RunLine[];
|
|
127
135
|
};
|
|
128
136
|
|
|
129
137
|
type CommonTestOptions = {
|
|
@@ -161,12 +169,8 @@ type CommonTestOptions = {
|
|
|
161
169
|
runtimes?: RuntimeAliases;
|
|
162
170
|
/** The run's deadline; none unless given, as `awf run` has none unless `--timeout` gives one. */
|
|
163
171
|
timeout?: Duration;
|
|
164
|
-
/** @deprecated Give `timeout`, a `Duration` such as `"5m"`. */
|
|
165
|
-
timeoutMs?: number;
|
|
166
172
|
/** Real time with no turn or decision starting or ending before the test fails; `"2s"`. */
|
|
167
173
|
stall?: Duration;
|
|
168
|
-
/** @deprecated Give `stall`, a `Duration` such as `"2s"`. */
|
|
169
|
-
stallMs?: number;
|
|
170
174
|
/**
|
|
171
175
|
* The run's clock is virtual: where a run would park with a timer to come, the clock moves to it
|
|
172
176
|
* and the timer fires, so a two-day reminder takes no time. After this many, the run parks
|
|
@@ -190,10 +194,6 @@ type CommonTestOptions = {
|
|
|
190
194
|
maxTurns?: number;
|
|
191
195
|
/** What an earlier attempt left; given, the test's attempt is its continue. */
|
|
192
196
|
earlier?: EarlierAttempt;
|
|
193
|
-
/** @deprecated Give `earlier: { stages }`. */
|
|
194
|
-
recorded?: Readonly<Record<string, JsonValue | undefined>>;
|
|
195
|
-
/** @deprecated Give `earlier: { mailbox }`. */
|
|
196
|
-
mailbox?: readonly MailboxRecord[];
|
|
197
197
|
/**
|
|
198
198
|
* The stage the attempt starts at, as `--from-stage`: a continue of `earlier`, or a new run
|
|
199
199
|
* started there. A workflow with events takes it only with `earlier`, as `awf run` takes it only
|
|
@@ -205,6 +205,12 @@ type CommonTestOptions = {
|
|
|
205
205
|
* a workflow with events takes none.
|
|
206
206
|
*/
|
|
207
207
|
values?: Readonly<Record<string, JsonValue>>;
|
|
208
|
+
/**
|
|
209
|
+
* Where the run's TUI agents' panes go and whether they stay, as `awf run --workspace` and
|
|
210
|
+
* `--keep-panes` give them: `run.agents` shows what each agent's host was asked. Absent, the
|
|
211
|
+
* run's workspace, and no pane kept.
|
|
212
|
+
*/
|
|
213
|
+
panes?: RunPanes;
|
|
208
214
|
};
|
|
209
215
|
|
|
210
216
|
/** Every option a test may give, whatever its workflow's args and events: what the run reads. */
|
|
@@ -234,54 +240,34 @@ const OPTION_KEYS: readonly (keyof AnyTestOptions)[] = [
|
|
|
234
240
|
"earlier",
|
|
235
241
|
"fromStage",
|
|
236
242
|
"values",
|
|
243
|
+
"panes",
|
|
237
244
|
"event",
|
|
238
245
|
"events",
|
|
239
246
|
];
|
|
240
|
-
|
|
241
|
-
const DEPRECATED_KEYS: readonly (keyof AnyTestOptions)[] = [
|
|
242
|
-
"timeoutMs",
|
|
243
|
-
"stallMs",
|
|
244
|
-
"recorded",
|
|
245
|
-
"mailbox",
|
|
246
|
-
];
|
|
247
|
-
const EARLIER_KEYS: readonly (keyof EarlierAttempt)[] = ["stages", "mailbox"];
|
|
247
|
+
const EARLIER_KEYS: readonly (keyof EarlierAttempt)[] = ["stages", "events", "run"];
|
|
248
248
|
|
|
249
249
|
/**
|
|
250
|
-
* A test's options as `testWorkflow` was called
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
* an array), so a workflow's object args are never taken for args there: they are read as options,
|
|
254
|
-
* and refused by their first key.
|
|
250
|
+
* A test's options as `testWorkflow` was called, `(workflow, options?)`: a workflow's args given
|
|
251
|
+
* second, or options third, are refused, naming `{ args }`; object args given second are named by
|
|
252
|
+
* their first key.
|
|
255
253
|
*/
|
|
256
|
-
export function testCall(
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
):
|
|
260
|
-
const deprecatedForm =
|
|
261
|
-
given.length >= 2 || (given.length === 1 && given[0] !== undefined && !isObject(given[0]));
|
|
262
|
-
if (!deprecatedForm) {
|
|
263
|
-
const options = plainOptions(given[0] ?? {});
|
|
264
|
-
// Object args given second are named by their key, whatever the workflow is.
|
|
265
|
-
const problem = optionsProblem(
|
|
266
|
-
options,
|
|
267
|
-
isEventWorkflow(workflow),
|
|
268
|
-
(workflow as { meta?: { name?: string } } | null)?.meta?.name ?? "the workflow",
|
|
254
|
+
export function testCall(workflow: unknown, given: readonly unknown[]): AnyTestOptions {
|
|
255
|
+
if (given.length > 1)
|
|
256
|
+
throw new Error(
|
|
257
|
+
"testWorkflow takes (workflow, options?): give args among the options, testWorkflow(workflow, { args, … })",
|
|
269
258
|
);
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if ("args" in options)
|
|
283
|
-
throw new Error("args are given once: testWorkflow(workflow, { args, … }) is the one form");
|
|
284
|
-
return { options: { ...options, args: args as JsonValue }, used: ["test-args"] };
|
|
259
|
+
const options = plainOptions(given[0] === undefined ? {} : given[0]);
|
|
260
|
+
const problem = optionsProblem(
|
|
261
|
+
options,
|
|
262
|
+
isEventWorkflow(workflow),
|
|
263
|
+
(workflow as { meta?: { name?: string } } | null)?.meta?.name ?? "the workflow",
|
|
264
|
+
);
|
|
265
|
+
if (problem) throw new Error(problem);
|
|
266
|
+
if (!isWorkflow(workflow))
|
|
267
|
+
throw new Error(
|
|
268
|
+
"testWorkflow(workflow, options?) tests a workflow defineWorkflow made: define it with defineWorkflow",
|
|
269
|
+
);
|
|
270
|
+
return options;
|
|
285
271
|
}
|
|
286
272
|
|
|
287
273
|
function isObject(value: unknown): value is AnyTestOptions {
|
|
@@ -320,56 +306,58 @@ function optionsProblem(
|
|
|
320
306
|
name: string,
|
|
321
307
|
): string | undefined {
|
|
322
308
|
for (const key of Object.keys(options) as (keyof AnyTestOptions)[]) {
|
|
323
|
-
if (!OPTION_KEYS.includes(key)
|
|
309
|
+
if (!OPTION_KEYS.includes(key))
|
|
324
310
|
return `testWorkflow has no option ${JSON.stringify(key)}: its options are ${OPTION_KEYS.join(", ")}${
|
|
325
311
|
"args" in options ? "" : "; a workflow's args are given as { args }"
|
|
326
312
|
}`;
|
|
327
313
|
if (!withEvents && (key === "event" || key === "events"))
|
|
328
314
|
return `${key} is an option for a workflow with events, and ${name} declares none`;
|
|
329
315
|
}
|
|
316
|
+
const panes = panesProblem(options.panes);
|
|
317
|
+
if (panes) return panes;
|
|
330
318
|
const { earlier } = options;
|
|
331
319
|
if (earlier === undefined) return undefined;
|
|
332
|
-
if (!isObject(earlier)) return "earlier is an object: { stages?,
|
|
320
|
+
if (!isObject(earlier)) return "earlier is an object: { stages?, events?, run? }";
|
|
333
321
|
const unknown = Object.keys(earlier).find(
|
|
334
322
|
(key) => !EARLIER_KEYS.includes(key as keyof EarlierAttempt),
|
|
335
323
|
);
|
|
336
324
|
return unknown === undefined
|
|
337
325
|
? undefined
|
|
338
|
-
: `earlier has no ${JSON.stringify(unknown)}: it gives ${EARLIER_KEYS.join("
|
|
326
|
+
: `earlier has no ${JSON.stringify(unknown)}: it gives ${EARLIER_KEYS.join(", ")}`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Why `panes` isn't what `awf run --workspace` and `--keep-panes` could give; undefined when it is. */
|
|
330
|
+
function panesProblem(panes: unknown): string | undefined {
|
|
331
|
+
if (panes === undefined) return undefined;
|
|
332
|
+
if (!isObject(panes)) return "panes is an object: { workspace?, keep? }";
|
|
333
|
+
const { workspace, keep, ...rest } = panes as Record<string, unknown>;
|
|
334
|
+
const other = Object.keys(rest)[0];
|
|
335
|
+
if (other !== undefined) return `panes has no ${JSON.stringify(other)}: it gives workspace, keep`;
|
|
336
|
+
if (keep !== undefined && !KEEP_PANES.includes(keep as KeepPane))
|
|
337
|
+
return `panes.keep is ${KEEP_PANES.join(", ")}, not ${JSON.stringify(keep)}`;
|
|
338
|
+
const named =
|
|
339
|
+
isObject(workspace) &&
|
|
340
|
+
Object.keys(workspace).join() === "name" &&
|
|
341
|
+
typeof (workspace as { name?: unknown }).name === "string" &&
|
|
342
|
+
(workspace as { name: string }).name.trim() !== "";
|
|
343
|
+
if (workspace !== undefined && workspace !== "run" && workspace !== "origin" && !named)
|
|
344
|
+
return `panes.workspace is "run", "origin" or { name }, not ${JSON.stringify(workspace)}`;
|
|
345
|
+
return undefined;
|
|
339
346
|
}
|
|
340
347
|
|
|
341
348
|
/** What a test of the engine, not of a workflow, may give `runTest`. */
|
|
342
|
-
|
|
349
|
+
type EngineTestOptions = {
|
|
343
350
|
/**
|
|
344
351
|
* Where an agent may `--await-reply`: `listed`, where the engine's list allows it for the
|
|
345
352
|
* agent's harness and placement, as a run would; `anywhere`, on any harness, the fake's included.
|
|
346
353
|
*/
|
|
347
354
|
awaitReply?: "listed" | "anywhere";
|
|
348
|
-
/** Deprecated forms the call itself used, logged with the run's own. */
|
|
349
|
-
used?: readonly Deprecated[];
|
|
350
355
|
};
|
|
351
356
|
|
|
352
|
-
/**
|
|
353
|
-
|
|
354
|
-
* milliseconds, which is reported; both at once are refused.
|
|
355
|
-
*/
|
|
356
|
-
function milliseconds(
|
|
357
|
-
options: AnyTestOptions,
|
|
358
|
-
field: "timeout" | "stall",
|
|
359
|
-
old: "timeoutMs" | "stallMs",
|
|
360
|
-
deprecated: () => void,
|
|
361
|
-
): number | undefined {
|
|
357
|
+
/** A test's length of time in milliseconds, a `Duration` checked; undefined when not given. */
|
|
358
|
+
function milliseconds(options: AnyTestOptions, field: "timeout" | "stall"): number | undefined {
|
|
362
359
|
const given = options[field];
|
|
363
|
-
|
|
364
|
-
if (given != null) {
|
|
365
|
-
if (ms != null) throw new Error(`give ${field} alone: ${old} is its deprecated form`);
|
|
366
|
-
return positiveDurationMilliseconds(given, `a test's ${field}`);
|
|
367
|
-
}
|
|
368
|
-
if (ms == null) return undefined;
|
|
369
|
-
if (!Number.isSafeInteger(ms) || ms <= 0)
|
|
370
|
-
throw new Error(`a test's ${old} is a positive whole number of milliseconds: not ${ms}`);
|
|
371
|
-
deprecated();
|
|
372
|
-
return ms;
|
|
360
|
+
return given == null ? undefined : positiveDurationMilliseconds(given, `a test's ${field}`);
|
|
373
361
|
}
|
|
374
362
|
|
|
375
363
|
/** A file's contents, or a directory of them by name. */
|
|
@@ -382,41 +370,24 @@ const WAITED_OUT = "the test ended the attempt: everything left waited on a part
|
|
|
382
370
|
|
|
383
371
|
/**
|
|
384
372
|
* An event a test sends while the run goes on, at an agent's `n`th turn, `"{agent}#{n}"`: `during`
|
|
385
|
-
* it, or `after` it has ended, once the run waits live in an agent's `keepAlive` window.
|
|
373
|
+
* it, or `after` it has ended, once the run waits live in an agent's `keepAlive` window. `n` counts
|
|
374
|
+
* the agent's turns in the test, a nudge's included, and not its compactions: not the `n` of its
|
|
375
|
+
* operation's id, `{agent}.{n}`, which counts each operation once and its compactions too.
|
|
386
376
|
*/
|
|
387
377
|
export type ScriptedEvent<Events extends EventSchemas> = (
|
|
388
378
|
| { during: string; after?: undefined }
|
|
389
379
|
| { after: string; during?: undefined }
|
|
390
380
|
) &
|
|
391
381
|
EventIds &
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}[Exclude<keyof Events & string, `${string}:ended`>]
|
|
399
|
-
/**
|
|
400
|
-
* A reply on a participant routed to the code with `to: "code"`; awf fills its author and id
|
|
401
|
-
* when absent.
|
|
402
|
-
*
|
|
403
|
-
* @deprecated Declare `{key}:reply: ParticipantReply` and script `{ name: "{key}:reply", data }`.
|
|
404
|
-
*/
|
|
405
|
-
| { name: "participant:reply"; from: string; data: ParticipantReply }
|
|
406
|
-
);
|
|
407
|
-
|
|
408
|
-
/** An event's id in the run's mailbox, as `awf emit --event-id` gives it. */
|
|
409
|
-
type EventIds = {
|
|
410
|
-
eventId?: string;
|
|
411
|
-
/** @deprecated Give it as `eventId`. */
|
|
412
|
-
id?: string;
|
|
413
|
-
};
|
|
382
|
+
{
|
|
383
|
+
[Name in Exclude<keyof Events & string, `${string}:ended`>]: {
|
|
384
|
+
name: Name;
|
|
385
|
+
data: OutputOf<Events[Name]>;
|
|
386
|
+
};
|
|
387
|
+
}[Exclude<keyof Events & string, `${string}:ended`>];
|
|
414
388
|
|
|
415
|
-
/**
|
|
416
|
-
|
|
417
|
-
Events extends EventSchemas,
|
|
418
|
-
Args extends JsonValue = JsonValue,
|
|
419
|
-
> = TestOptions<Args, Events>;
|
|
389
|
+
/** An event's id in the run's `in.jsonl`, as `awf emit --event-id` gives it. */
|
|
390
|
+
type EventIds = { eventId?: string };
|
|
420
391
|
|
|
421
392
|
/** An event the run sent, to the run whose ref is `ref`, which a test doesn't start. */
|
|
422
393
|
export type EmittedEvent = { ref: string; name: string; data: JsonValue; eventId?: string };
|
|
@@ -450,35 +421,29 @@ export type TestRun<Result> = {
|
|
|
450
421
|
decisions: DecisionRequest[];
|
|
451
422
|
logs: { message: string; fields?: JsonObject }[];
|
|
452
423
|
/**
|
|
453
|
-
* Each stage
|
|
454
|
-
* reason included, which the run's own summaries leave out
|
|
424
|
+
* Each stage the attempt entered, in order, by its line in `run.jsonl`: one it ran or was given
|
|
425
|
+
* its own, times and reason included, which the run's own summaries leave out; one it reused the
|
|
426
|
+
* line it reused, an earlier attempt's. A stage the plan stopped as it was entered wrote none, and
|
|
427
|
+
* is absent.
|
|
455
428
|
*/
|
|
456
|
-
stages:
|
|
429
|
+
stages: StageLine[];
|
|
457
430
|
/**
|
|
458
431
|
* How the attempt ended, as the workflow's `present` and `report` are given it: `completed` with
|
|
459
432
|
* its value, `parked` at its last wait, or `stopped`, `failed`, `timed-out` or `cancelled` with
|
|
460
433
|
* the stage it ended in and why.
|
|
461
434
|
*/
|
|
462
435
|
ending: Ending<Result>;
|
|
463
|
-
/**
|
|
464
|
-
* @deprecated Read `run.ending`, whose kind is `"stopped"`. How the attempt stopped, and the
|
|
465
|
-
* values it stopped for; absent when it didn't.
|
|
466
|
-
*/
|
|
467
|
-
readonly stopped?: { reason: string; stage?: string; needs?: readonly StageNeed[] };
|
|
468
436
|
/** Every event the run sent to another workflow, in order. */
|
|
469
437
|
emits: EmittedEvent[];
|
|
470
|
-
/**
|
|
471
|
-
* @deprecated Read `run.ending`, whose kind is `"parked"`. A run that parked, and its last wait;
|
|
472
|
-
* absent when it returned.
|
|
473
|
-
*/
|
|
474
|
-
readonly parked?: { label?: string; until?: string };
|
|
475
438
|
/** The run's timers in the order set: when each was due, and whether it fired or was cancelled. */
|
|
476
439
|
timers: { name: string; due: string; state: "fired" | "cancelled" | "pending" }[];
|
|
440
|
+
/** The events senders gave the run, its `in.jsonl` as the run left it, in order. */
|
|
441
|
+
events: EventLine[];
|
|
477
442
|
/**
|
|
478
|
-
*
|
|
479
|
-
*
|
|
443
|
+
* What the run settled, its `run.jsonl` as the run left it, in order: each attempt's `claimed`,
|
|
444
|
+
* and its stages, timers, sessions, what its waits and races took, and the turns it began.
|
|
480
445
|
*/
|
|
481
|
-
|
|
446
|
+
run: RunLine[];
|
|
482
447
|
};
|
|
483
448
|
|
|
484
449
|
/**
|
|
@@ -495,11 +460,7 @@ export async function runTest<Result extends JsonValue>(
|
|
|
495
460
|
const problem = optionsProblem(options, withEvents !== undefined, workflow.meta.name);
|
|
496
461
|
if (problem) throw new Error(problem);
|
|
497
462
|
const logs: TestRun<Result>["logs"] = [];
|
|
498
|
-
|
|
499
|
-
const deprecations = new RunDeprecations((message) => logs.push({ message }));
|
|
500
|
-
const deprecated = (name: Deprecated) => deprecations.used(name);
|
|
501
|
-
for (const name of engine.used ?? []) deprecated(name);
|
|
502
|
-
const earlier = earlierOf(options, deprecated);
|
|
463
|
+
const earlier = earlierOf(options);
|
|
503
464
|
if (withEvents && options.values !== undefined)
|
|
504
465
|
throw new Error(
|
|
505
466
|
"a run of a workflow with events takes no values: before fromStage, a stage with no record runs",
|
|
@@ -508,7 +469,7 @@ export async function runTest<Result extends JsonValue>(
|
|
|
508
469
|
throw new Error(
|
|
509
470
|
"a run of a workflow with events takes fromStage only with earlier, as awf run takes --from-stage only with --continue",
|
|
510
471
|
);
|
|
511
|
-
const started = startOf(withEvents, options
|
|
472
|
+
const started = startOf(withEvents, options);
|
|
512
473
|
const definition = workflow as RunnableWorkflow<JsonValue, Result>;
|
|
513
474
|
const scripts = new Scripts(options.agents);
|
|
514
475
|
const compactionScripts = new Scripts(options.compactions, {
|
|
@@ -521,11 +482,8 @@ export async function runTest<Result extends JsonValue>(
|
|
|
521
482
|
}
|
|
522
483
|
const stopping = new AbortController();
|
|
523
484
|
const problems: string[] = [];
|
|
524
|
-
const stallMs =
|
|
525
|
-
|
|
526
|
-
const timeoutMs = milliseconds(options, "timeout", "timeoutMs", () =>
|
|
527
|
-
deprecations.used("timeout-ms", "in testWorkflow's options"),
|
|
528
|
-
);
|
|
485
|
+
const stallMs = milliseconds(options, "stall") ?? 2_000;
|
|
486
|
+
const timeoutMs = milliseconds(options, "timeout");
|
|
529
487
|
let stall: ReturnType<typeof setTimeout> | undefined;
|
|
530
488
|
/** The test ended the attempt: everything left waited on a coordinator's script. */
|
|
531
489
|
let waitedOut = false;
|
|
@@ -576,8 +534,7 @@ export async function runTest<Result extends JsonValue>(
|
|
|
576
534
|
events.onActivity();
|
|
577
535
|
return true;
|
|
578
536
|
};
|
|
579
|
-
const scripted =
|
|
580
|
-
withEvents && new ScriptedEvents(withEvents, options, earlier.mailbox ?? [], now, deprecated);
|
|
537
|
+
const scripted = withEvents && new ScriptedEvents(withEvents, options, earlier.events ?? [], now);
|
|
581
538
|
const maxTurns = options.maxTurns ?? 20;
|
|
582
539
|
if (!Number.isInteger(maxTurns) || maxTurns < 1)
|
|
583
540
|
throw new Error(`maxTurns is a whole number of turns, at least 1, not ${options.maxTurns}`);
|
|
@@ -588,7 +545,6 @@ export async function runTest<Result extends JsonValue>(
|
|
|
588
545
|
const events = {
|
|
589
546
|
onActivity: () => arm(stallMs),
|
|
590
547
|
onScriptError: (message: string) => stopWith(message),
|
|
591
|
-
onDeprecated: (name: Deprecated, subject?: string) => deprecations.used(name, subject),
|
|
592
548
|
onTurnStarted: async (turn: TestTurn) => {
|
|
593
549
|
turnsStarted += 1;
|
|
594
550
|
if (turnsStarted > maxTurns)
|
|
@@ -597,11 +553,13 @@ export async function runTest<Result extends JsonValue>(
|
|
|
597
553
|
);
|
|
598
554
|
const due = scripted ? scripted.due(turn) : [];
|
|
599
555
|
if (due.length === 0) return;
|
|
600
|
-
for (const
|
|
556
|
+
for (const line of due) await appendIn(run.dir, line);
|
|
601
557
|
nudge?.();
|
|
602
558
|
},
|
|
603
559
|
};
|
|
604
|
-
const attempt = earlier.continues
|
|
560
|
+
const attempt = earlier.continues
|
|
561
|
+
? Math.max(1, ...(earlier.run ?? []).map((line) => line.attempt)) + 1
|
|
562
|
+
: 1;
|
|
605
563
|
const coordinators = new ParticipantScripts(options.participants, attempt, events);
|
|
606
564
|
const stopWith = (message: string) => {
|
|
607
565
|
problems.push(message);
|
|
@@ -650,8 +608,10 @@ export async function runTest<Result extends JsonValue>(
|
|
|
650
608
|
};
|
|
651
609
|
let sandboxOf = new Map<string, AgentSandbox>();
|
|
652
610
|
const emits: EmittedEvent[] = [];
|
|
653
|
-
let
|
|
654
|
-
let
|
|
611
|
+
let inLines: EventLine[] = [];
|
|
612
|
+
let runLines: RunLine[] = [];
|
|
613
|
+
let timers: TestRun<Result>["timers"] = [];
|
|
614
|
+
let stages: StageLine[] = [];
|
|
655
615
|
let messages: (MessageRecord & { to: string })[] = [];
|
|
656
616
|
const runRoot = directory();
|
|
657
617
|
const deadline =
|
|
@@ -666,15 +626,28 @@ export async function runTest<Result extends JsonValue>(
|
|
|
666
626
|
for (const path of temporary) rmSync(path, { recursive: true, force: true });
|
|
667
627
|
throw error;
|
|
668
628
|
});
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
629
|
+
let lines: RunLines;
|
|
630
|
+
let ops: EarlierOps;
|
|
631
|
+
try {
|
|
632
|
+
if (earlier.stages) await recordEarlierAttempt(run.dir, earlier.stages);
|
|
633
|
+
if (earlier.run) await writeEarlierLines(run.dir, earlier.run);
|
|
634
|
+
for (const line of earlier.events ?? []) await appendIn(run.dir, line);
|
|
635
|
+
if (started.record) await appendIn(run.dir, started.record);
|
|
636
|
+
ops = await listOps(run.dir);
|
|
637
|
+
lines = await claimRunLines(run.dir, attempt, new Date(now()).toISOString());
|
|
638
|
+
} catch (error) {
|
|
639
|
+
for (const path of temporary) rmSync(path, { recursive: true, force: true });
|
|
640
|
+
throw error;
|
|
641
|
+
}
|
|
672
642
|
const recordedStages = async ({ stages: entered = [] }: SettledRun) => {
|
|
673
|
-
const
|
|
674
|
-
//
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
643
|
+
const { view } = await readRunLines(run.dir);
|
|
644
|
+
// The run's summaries hold one entry per stage, as it last stood: a stage by content that
|
|
645
|
+
// failed and ran again in this attempt is one entry, its latest line. So each entry's line is
|
|
646
|
+
// the latest its attempt wrote: a stage this attempt ran or was given has its own, a reused one
|
|
647
|
+
// the line it reused. A stage the plan stopped wrote none.
|
|
648
|
+
return entered.flatMap(({ stage, attempt: wrote }) => {
|
|
649
|
+
const line = view.stages[stage]?.history.findLast((each) => each.attempt === wrote);
|
|
650
|
+
return line ? [line] : [];
|
|
678
651
|
});
|
|
679
652
|
};
|
|
680
653
|
events.onActivity();
|
|
@@ -694,8 +667,10 @@ export async function runTest<Result extends JsonValue>(
|
|
|
694
667
|
run: {
|
|
695
668
|
dir: run.dir,
|
|
696
669
|
id: run.record.id,
|
|
697
|
-
|
|
670
|
+
lines,
|
|
671
|
+
ops,
|
|
698
672
|
...(options.fromStage === undefined ? {} : { fromStage: options.fromStage }),
|
|
673
|
+
...(options.panes === undefined ? {} : { panes: options.panes }),
|
|
699
674
|
...(options.values === undefined
|
|
700
675
|
? {}
|
|
701
676
|
: { values: new Map(Object.entries(options.values)) }),
|
|
@@ -707,7 +682,7 @@ export async function runTest<Result extends JsonValue>(
|
|
|
707
682
|
live: async (until: number) => {
|
|
708
683
|
liveUntil = until;
|
|
709
684
|
const due = scripted.ended(host.inFlight());
|
|
710
|
-
for (const
|
|
685
|
+
for (const line of due) await appendIn(run.dir, line);
|
|
711
686
|
if (due.length > 0) nudge?.();
|
|
712
687
|
},
|
|
713
688
|
listenNudges: (given: () => void) => {
|
|
@@ -737,11 +712,8 @@ export async function runTest<Result extends JsonValue>(
|
|
|
737
712
|
},
|
|
738
713
|
sandboxesDir: directory(),
|
|
739
714
|
machineRoot: directory(),
|
|
740
|
-
//
|
|
741
|
-
|
|
742
|
-
...process.env,
|
|
743
|
-
...Object.fromEntries(sandboxTokens().map((name) => [name, "workflow-test"])),
|
|
744
|
-
},
|
|
715
|
+
// An agent here launches nothing, so it needs no harness's executable or login.
|
|
716
|
+
homes: SCRIPTED_HOMES,
|
|
745
717
|
},
|
|
746
718
|
decisions: {
|
|
747
719
|
providers: { scripted: decisions.provider },
|
|
@@ -777,7 +749,9 @@ export async function runTest<Result extends JsonValue>(
|
|
|
777
749
|
}
|
|
778
750
|
host.settle(await readTurns(run.dir));
|
|
779
751
|
messages = await messagesIn(run.dir);
|
|
780
|
-
|
|
752
|
+
inLines = await readIn(run.dir);
|
|
753
|
+
runLines = (await readRunLines(run.dir)).lines;
|
|
754
|
+
timers = timersOf((await readWaits(run.dir, now())).timers);
|
|
781
755
|
for (const path of temporary) rmSync(path, { recursive: true, force: true });
|
|
782
756
|
}
|
|
783
757
|
if (problems.length === 0 && scripted) problems.push(...scripted.unsent(host.inFlight()));
|
|
@@ -827,9 +801,7 @@ export async function runTest<Result extends JsonValue>(
|
|
|
827
801
|
continue: goOn(undefined, decided.stages),
|
|
828
802
|
}
|
|
829
803
|
: decided;
|
|
830
|
-
|
|
831
|
-
const parked = parkedOf(settled);
|
|
832
|
-
const result: TestRun<Result> = {
|
|
804
|
+
return {
|
|
833
805
|
get value() {
|
|
834
806
|
if ("value" in settled) return settled.value;
|
|
835
807
|
if (primaryFailure(settled.error) instanceof RunParked)
|
|
@@ -842,7 +814,8 @@ export async function runTest<Result extends JsonValue>(
|
|
|
842
814
|
},
|
|
843
815
|
turns: host.turns,
|
|
844
816
|
emits,
|
|
845
|
-
|
|
817
|
+
events: inLines,
|
|
818
|
+
run: runLines,
|
|
846
819
|
turnsOf: (agent) => host.turns.filter((turn) => turn.agent === agent),
|
|
847
820
|
compactions: host.compactions,
|
|
848
821
|
compactionsOf: (agent) => host.compactions.filter((compaction) => compaction.agent === agent),
|
|
@@ -860,32 +833,17 @@ export async function runTest<Result extends JsonValue>(
|
|
|
860
833
|
decisions: decisions.asked,
|
|
861
834
|
logs,
|
|
862
835
|
stages,
|
|
863
|
-
timers
|
|
836
|
+
timers,
|
|
864
837
|
ending,
|
|
865
838
|
};
|
|
866
|
-
// Not enumerable, so spreading the run reads neither.
|
|
867
|
-
Object.defineProperties(result, {
|
|
868
|
-
stopped: {
|
|
869
|
-
get: () => {
|
|
870
|
-
deprecated("run-stopped");
|
|
871
|
-
return stopped;
|
|
872
|
-
},
|
|
873
|
-
},
|
|
874
|
-
parked: {
|
|
875
|
-
get: () => {
|
|
876
|
-
deprecated("run-parked");
|
|
877
|
-
return parked;
|
|
878
|
-
},
|
|
879
|
-
},
|
|
880
|
-
});
|
|
881
|
-
return result;
|
|
882
839
|
}
|
|
883
840
|
|
|
884
|
-
|
|
885
|
-
|
|
841
|
+
/** Each timer: taken, or due by the run's clock and not cancelled, is `fired`. */
|
|
842
|
+
function timersOf(timers: ReadonlyMap<string, TimerState>): TestRun<unknown>["timers"] {
|
|
843
|
+
return [...timers.values()].map(({ name, due, taken, fired, cancelled }) => ({
|
|
886
844
|
name,
|
|
887
845
|
due,
|
|
888
|
-
state:
|
|
846
|
+
state: taken ? "fired" : cancelled ? "cancelled" : fired ? "fired" : "pending",
|
|
889
847
|
}));
|
|
890
848
|
}
|
|
891
849
|
|
|
@@ -940,36 +898,52 @@ async function messagesIn(dir: string): Promise<(MessageRecord & { to: string })
|
|
|
940
898
|
}
|
|
941
899
|
|
|
942
900
|
/**
|
|
943
|
-
* Writes `recorded` as attempt 1's succeeded
|
|
944
|
-
* given: a continue orders what is recorded by when each stage
|
|
901
|
+
* Writes `recorded` as attempt 1's succeeded stage lines into a run's `run.jsonl`, after attempt 1's
|
|
902
|
+
* claim, a second apart in the order given: a continue orders what is recorded by when each stage
|
|
903
|
+
* started.
|
|
945
904
|
*/
|
|
946
905
|
async function recordEarlierAttempt(
|
|
947
906
|
dir: string,
|
|
948
907
|
recorded: Readonly<Record<string, JsonValue | undefined>>,
|
|
949
908
|
): Promise<void> {
|
|
950
909
|
let at = Date.now() - Object.keys(recorded).length * 1_000;
|
|
951
|
-
|
|
910
|
+
const lines = await claimRunLines(dir, 1, new Date(at - 1_000).toISOString());
|
|
911
|
+
for (const [name, value] of Object.entries(recorded)) {
|
|
952
912
|
const time = new Date(at).toISOString();
|
|
953
913
|
at += 1_000;
|
|
954
|
-
await
|
|
955
|
-
|
|
956
|
-
stage,
|
|
914
|
+
await lines.append({
|
|
915
|
+
kind: "stage",
|
|
957
916
|
attempt: 1,
|
|
917
|
+
at: time,
|
|
918
|
+
name,
|
|
958
919
|
outcome: "succeeded",
|
|
959
920
|
started: time,
|
|
960
|
-
ended: time,
|
|
961
|
-
sessions: [],
|
|
962
921
|
...(value === undefined ? {} : { value }),
|
|
963
922
|
});
|
|
964
923
|
}
|
|
965
924
|
}
|
|
966
925
|
|
|
926
|
+
/**
|
|
927
|
+
* Writes `lines`, what an earlier attempt settled, into the run's `run.jsonl` as given, once
|
|
928
|
+
* `readRun` takes them: the test's claim comes after them.
|
|
929
|
+
*/
|
|
930
|
+
async function writeEarlierLines(dir: string, lines: readonly RunLine[]): Promise<void> {
|
|
931
|
+
try {
|
|
932
|
+
readRun(lines);
|
|
933
|
+
} catch (error) {
|
|
934
|
+
throw new Error(`earlier.run: ${messageOf(error)}`);
|
|
935
|
+
}
|
|
936
|
+
await appendFile(runLinesFile(dir), lines.map((line) => `${JSON.stringify(line)}\n`).join(""), {
|
|
937
|
+
mode: 0o600,
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
|
|
967
941
|
/** An event a test sends while its run goes on, once its turn begins. */
|
|
968
942
|
type PendingEvent = {
|
|
969
943
|
at: string;
|
|
970
944
|
after: boolean;
|
|
971
945
|
name: string;
|
|
972
|
-
record:
|
|
946
|
+
record: EventLine;
|
|
973
947
|
/** Its turn, once begun: an `after` event waits for it to end. */
|
|
974
948
|
turn?: TestTurn;
|
|
975
949
|
};
|
|
@@ -981,11 +955,10 @@ class ScriptedEvents {
|
|
|
981
955
|
constructor(
|
|
982
956
|
workflow: EventWorkflow,
|
|
983
957
|
options: AnyTestOptions,
|
|
984
|
-
/**
|
|
985
|
-
earlier: readonly
|
|
958
|
+
/** The events the earlier attempt left, whose ids are taken. */
|
|
959
|
+
earlier: readonly EventLine[],
|
|
986
960
|
/** The run's clock, which stamps what is sent. */
|
|
987
961
|
private readonly now: () => number,
|
|
988
|
-
deprecated: (name: Deprecated) => void,
|
|
989
962
|
) {
|
|
990
963
|
const ids = new Set<string>();
|
|
991
964
|
const idOf = (given: string | undefined, made: string) => {
|
|
@@ -998,8 +971,8 @@ class ScriptedEvents {
|
|
|
998
971
|
ids.add(id);
|
|
999
972
|
return id;
|
|
1000
973
|
};
|
|
1001
|
-
for (const
|
|
1002
|
-
if (options.event) idOf(
|
|
974
|
+
for (const line of earlier) ids.add(line.id);
|
|
975
|
+
if (options.event) idOf(options.event.eventId, `${AWF_ID_PREFIX}start`);
|
|
1003
976
|
this.#pending = (options.events ?? []).map((event, index) => {
|
|
1004
977
|
if ((event.during === undefined) === (event.after === undefined))
|
|
1005
978
|
throw new Error("a scripted event gives during or after, one of them");
|
|
@@ -1009,7 +982,7 @@ class ScriptedEvents {
|
|
|
1009
982
|
throw new Error(
|
|
1010
983
|
`a scripted event's ${after ? "after" : "during"} is "{agent}#{n}", an agent's nth turn, not ${at}`,
|
|
1011
984
|
);
|
|
1012
|
-
const id = idOf(
|
|
985
|
+
const id = idOf(event.eventId, `${AWF_ID_PREFIX}test.${index + 1}`);
|
|
1013
986
|
// A participant's reply is recorded as `awf emit` records one, whatever name it is sent by.
|
|
1014
987
|
const stamped = new Date().toISOString();
|
|
1015
988
|
let sent = {
|
|
@@ -1020,11 +993,7 @@ class ScriptedEvents {
|
|
|
1020
993
|
at: stamped,
|
|
1021
994
|
};
|
|
1022
995
|
const reply = awfEventOf(event.name);
|
|
1023
|
-
if (
|
|
1024
|
-
deprecated("test-participant-reply");
|
|
1025
|
-
const old = event as { from: string; data: ParticipantReply };
|
|
1026
|
-
sent = replyRecord(old.from, id, { author: "engineer", ...old.data }, stamped);
|
|
1027
|
-
} else if (reply?.kind === "reply") {
|
|
996
|
+
if (reply?.kind === "reply") {
|
|
1028
997
|
checkEvent(workflow, event, true);
|
|
1029
998
|
sent = replyRecord(reply.key, id, event.data, stamped);
|
|
1030
999
|
} else checkEvent(workflow, event);
|
|
@@ -1032,24 +1001,24 @@ class ScriptedEvents {
|
|
|
1032
1001
|
at,
|
|
1033
1002
|
after,
|
|
1034
1003
|
name: event.name,
|
|
1035
|
-
record:
|
|
1004
|
+
record: sent,
|
|
1036
1005
|
};
|
|
1037
1006
|
});
|
|
1038
1007
|
}
|
|
1039
1008
|
|
|
1040
1009
|
/** The events due during `turn`, taken off the list, stamped now; those after it wait for it. */
|
|
1041
|
-
due(turn: TestTurn):
|
|
1010
|
+
due(turn: TestTurn): EventLine[] {
|
|
1042
1011
|
const at = `${turn.agent}#${turn.n}`;
|
|
1043
1012
|
for (const each of this.#pending) if (each.after && each.at === at) each.turn = turn;
|
|
1044
1013
|
return this.#take((each) => !each.after && each.at === at);
|
|
1045
1014
|
}
|
|
1046
1015
|
|
|
1047
1016
|
/** The run waits live: the events after a turn that has ended, taken off the list. */
|
|
1048
|
-
ended(inFlight: readonly TestTurn[]):
|
|
1017
|
+
ended(inFlight: readonly TestTurn[]): EventLine[] {
|
|
1049
1018
|
return this.#take((each) => each.turn !== undefined && !inFlight.includes(each.turn));
|
|
1050
1019
|
}
|
|
1051
1020
|
|
|
1052
|
-
#take(which: (each: PendingEvent) => boolean):
|
|
1021
|
+
#take(which: (each: PendingEvent) => boolean): EventLine[] {
|
|
1053
1022
|
const due = this.#pending.filter(which);
|
|
1054
1023
|
for (const each of due) this.#pending.splice(this.#pending.indexOf(each), 1);
|
|
1055
1024
|
return due.map((each) => ({ ...each.record, at: new Date(this.now()).toISOString() }));
|
|
@@ -1085,7 +1054,7 @@ async function layOut(
|
|
|
1085
1054
|
for (const [path, tree] of Object.entries(files ?? {})) {
|
|
1086
1055
|
// Under the test's own directory, which it removes: a path outside would be left behind.
|
|
1087
1056
|
const target = resolve(cwd, path);
|
|
1088
|
-
if (isAbsolute(path) || relative(cwd, target)
|
|
1057
|
+
if (isAbsolute(path) || (relative(cwd, target) !== "" && !isInside(cwd, target)))
|
|
1089
1058
|
throw new Error(`files lays out under the run's directory; ${path} is outside it`);
|
|
1090
1059
|
await write(target, tree);
|
|
1091
1060
|
}
|
|
@@ -1098,8 +1067,7 @@ async function layOut(
|
|
|
1098
1067
|
function startOf(
|
|
1099
1068
|
workflow: EventWorkflow | undefined,
|
|
1100
1069
|
options: AnyTestOptions,
|
|
1101
|
-
|
|
1102
|
-
): { id: string; record?: MailboxRecord } {
|
|
1070
|
+
): { id: string; record?: EventLine } {
|
|
1103
1071
|
const { event, id = "test" } = options;
|
|
1104
1072
|
if (typeof id !== "string") throw new Error(`a run's id is a string, not ${JSON.stringify(id)}`);
|
|
1105
1073
|
const problem = idProblem(id);
|
|
@@ -1111,11 +1079,9 @@ function startOf(
|
|
|
1111
1079
|
throw new Error(
|
|
1112
1080
|
`${event.name} doesn't start a run of ${workflow.meta.name}: its starts are ${starts.join(", ")}`,
|
|
1113
1081
|
);
|
|
1114
|
-
const record = {
|
|
1115
|
-
version: MAILBOX_RECORD_VERSION,
|
|
1082
|
+
const record: EventLine = {
|
|
1116
1083
|
at: new Date().toISOString(),
|
|
1117
|
-
|
|
1118
|
-
id: eventIdOf(event, deprecated) ?? `${AWF_ID_PREFIX}start`,
|
|
1084
|
+
id: event.eventId ?? `${AWF_ID_PREFIX}start`,
|
|
1119
1085
|
name: event.name,
|
|
1120
1086
|
data: event.data,
|
|
1121
1087
|
from: "operator",
|
|
@@ -1124,32 +1090,9 @@ function startOf(
|
|
|
1124
1090
|
return { id, record };
|
|
1125
1091
|
}
|
|
1126
1092
|
|
|
1127
|
-
/**
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
*/
|
|
1131
|
-
function earlierOf(
|
|
1132
|
-
options: AnyTestOptions,
|
|
1133
|
-
deprecated: (name: Deprecated) => void,
|
|
1134
|
-
): EarlierAttempt & { continues: boolean } {
|
|
1135
|
-
const { earlier = {}, recorded, mailbox } = options;
|
|
1136
|
-
if (recorded !== undefined) {
|
|
1137
|
-
if (earlier.stages !== undefined)
|
|
1138
|
-
throw new Error("give earlier.stages alone: recorded is its deprecated form");
|
|
1139
|
-
deprecated("test-recorded");
|
|
1140
|
-
}
|
|
1141
|
-
if (mailbox !== undefined) {
|
|
1142
|
-
if (earlier.mailbox !== undefined)
|
|
1143
|
-
throw new Error("give earlier.mailbox alone: mailbox is its deprecated form");
|
|
1144
|
-
deprecated("test-mailbox");
|
|
1145
|
-
}
|
|
1146
|
-
const stages = earlier.stages ?? recorded;
|
|
1147
|
-
const left = earlier.mailbox ?? mailbox;
|
|
1148
|
-
return {
|
|
1149
|
-
...(stages === undefined ? {} : { stages }),
|
|
1150
|
-
...(left === undefined ? {} : { mailbox: left }),
|
|
1151
|
-
continues: options.earlier !== undefined || stages !== undefined || left !== undefined,
|
|
1152
|
-
};
|
|
1093
|
+
/** What the earlier attempt left; `continues` when `earlier` is given: the test's is a continue. */
|
|
1094
|
+
function earlierOf(options: AnyTestOptions): EarlierAttempt & { continues: boolean } {
|
|
1095
|
+
return { ...options.earlier, continues: options.earlier !== undefined };
|
|
1153
1096
|
}
|
|
1154
1097
|
|
|
1155
1098
|
/**
|
|
@@ -1176,15 +1119,6 @@ function argsOf(
|
|
|
1176
1119
|
}
|
|
1177
1120
|
}
|
|
1178
1121
|
|
|
1179
|
-
/** An event's id, given as `eventId` or, deprecated, `id`. */
|
|
1180
|
-
function eventIdOf(event: EventIds, deprecated: (name: Deprecated) => void): string | undefined {
|
|
1181
|
-
if (event.id === undefined) return event.eventId;
|
|
1182
|
-
deprecated("test-event-id");
|
|
1183
|
-
if (event.eventId !== undefined && event.eventId !== event.id)
|
|
1184
|
-
throw new Error(`an event gives eventId ${event.eventId} and id ${event.id}: give eventId`);
|
|
1185
|
-
return event.id;
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
1122
|
function sandboxesOf(sandboxes: readonly SandboxRecord[] | undefined): Map<string, AgentSandbox> {
|
|
1189
1123
|
return new Map(
|
|
1190
1124
|
(sandboxes ?? []).flatMap(({ key, provider, spec, domains, agents }) =>
|
|
@@ -1192,22 +1126,3 @@ function sandboxesOf(sandboxes: readonly SandboxRecord[] | undefined): Map<strin
|
|
|
1192
1126
|
),
|
|
1193
1127
|
);
|
|
1194
1128
|
}
|
|
1195
|
-
|
|
1196
|
-
/** A run that parked, and its last wait: what the attempt ended with. */
|
|
1197
|
-
function parkedOf(settled: { value: unknown } | { error: unknown }): TestRun<unknown>["parked"] {
|
|
1198
|
-
if (!("error" in settled)) return undefined;
|
|
1199
|
-
const parked = primaryFailure(settled.error);
|
|
1200
|
-
return parked instanceof RunParked ? parked.wait : undefined;
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
/** Decided as `awf run` decides it: a stop is the run's first failure, whatever failed beside it. */
|
|
1204
|
-
function stoppedOf(settled: { value: unknown } | { error: unknown }): TestRun<unknown>["stopped"] {
|
|
1205
|
-
if (!("error" in settled)) return undefined;
|
|
1206
|
-
const stop = primaryFailure(settled.error);
|
|
1207
|
-
if (!(stop instanceof WorkflowStopped)) return undefined;
|
|
1208
|
-
return {
|
|
1209
|
-
reason: stop.reason,
|
|
1210
|
-
...(stop.stage === undefined ? {} : { stage: stop.stage }),
|
|
1211
|
-
...(stop.needs === undefined ? {} : { needs: stop.needs }),
|
|
1212
|
-
};
|
|
1213
|
-
}
|