@akterjs/conformance 0.0.0-stage → 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.
Files changed (57) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +52 -2
  3. package/dist/client-contract.suite.d.mts +23 -0
  4. package/dist/client-contract.suite.mjs +279 -0
  5. package/dist/client-target.errors.d.mts +8 -0
  6. package/dist/client-target.errors.mjs +4 -0
  7. package/dist/engine-conformance.suite.d.mts +19 -0
  8. package/dist/engine-conformance.suite.mjs +357 -0
  9. package/dist/fork/fork-conformance.suite.d.mts +20 -0
  10. package/dist/fork/fork-conformance.suite.mjs +167 -0
  11. package/dist/guarantee-titles.pure.d.mts +31 -0
  12. package/dist/guarantee-titles.pure.mjs +32 -0
  13. package/dist/ledger/ledger-calls.pure.d.mts +79 -0
  14. package/dist/ledger/ledger-calls.pure.mjs +111 -0
  15. package/dist/ledger/ledger-checks.pure.d.mts +21 -0
  16. package/dist/ledger/ledger-checks.pure.mjs +37 -0
  17. package/dist/ledger/ledger.actor.d.mts +13 -0
  18. package/dist/ledger/ledger.actor.mjs +115 -0
  19. package/dist/ledger/ledger.contract.d.mts +310 -0
  20. package/dist/ledger/ledger.contract.mjs +112 -0
  21. package/dist/ledger/plain-ledger.actor.d.mts +4 -0
  22. package/dist/ledger/plain-ledger.actor.mjs +214 -0
  23. package/dist/rejected.errors.d.mts +8 -0
  24. package/dist/rejected.errors.mjs +4 -0
  25. package/dist/tally-clients.layer.d.mts +39 -0
  26. package/dist/tally-clients.layer.mjs +97 -0
  27. package/dist/tally.actor.d.mts +49 -0
  28. package/dist/tally.actor.mjs +26 -0
  29. package/dist/tally.contract.d.mts +51 -0
  30. package/dist/tally.contract.mjs +28 -0
  31. package/dist/workbench/computer-conformance.suite.d.mts +15 -0
  32. package/dist/workbench/computer-conformance.suite.mjs +330 -0
  33. package/dist/workbench/workbench-calls.pure.d.mts +45 -0
  34. package/dist/workbench/workbench-calls.pure.mjs +75 -0
  35. package/dist/workbench/workbench.actor.d.mts +4 -0
  36. package/dist/workbench/workbench.actor.mjs +107 -0
  37. package/dist/workbench/workbench.contract.d.mts +220 -0
  38. package/dist/workbench/workbench.contract.mjs +54 -0
  39. package/package.json +86 -3
  40. package/src/client-contract.suite.ts +413 -0
  41. package/src/client-target.errors.ts +7 -0
  42. package/src/engine-conformance.suite.ts +484 -0
  43. package/src/fork/fork-conformance.suite.ts +204 -0
  44. package/src/guarantee-titles.pure.ts +50 -0
  45. package/src/ledger/ledger-calls.pure.ts +179 -0
  46. package/src/ledger/ledger-checks.pure.ts +84 -0
  47. package/src/ledger/ledger.actor.ts +196 -0
  48. package/src/ledger/ledger.contract.ts +76 -0
  49. package/src/ledger/plain-ledger.actor.ts +246 -0
  50. package/src/rejected.errors.ts +6 -0
  51. package/src/tally-clients.layer.ts +221 -0
  52. package/src/tally.actor.ts +28 -0
  53. package/src/tally.contract.ts +22 -0
  54. package/src/workbench/computer-conformance.suite.ts +380 -0
  55. package/src/workbench/workbench-calls.pure.ts +92 -0
  56. package/src/workbench/workbench.actor.ts +175 -0
  57. package/src/workbench/workbench.contract.ts +47 -0
@@ -0,0 +1,39 @@
1
+ import { RejectedError } from "./rejected.errors.mjs";
2
+ import { Effect, MutableRef, Scope, Stream } from "effect";
3
+ type Headers = Readonly<Record<string, string>>;
4
+ interface Update<Value> {
5
+ readonly value: Value;
6
+ readonly version: number;
7
+ }
8
+ interface Owned {
9
+ readonly owner: string;
10
+ readonly total: number;
11
+ }
12
+ interface TallyHandle {
13
+ readonly add: (amount: number) => Effect.Effect<number, RejectedError>;
14
+ readonly create: (owner: string) => Effect.Effect<void, RejectedError>;
15
+ readonly ensure: (owner: string) => Effect.Effect<void, RejectedError>;
16
+ readonly fork: (into: string) => Effect.Effect<TallyHandle, RejectedError>;
17
+ readonly owner: Effect.Effect<string, RejectedError>;
18
+ readonly state: Stream.Stream<Update<Owned>, RejectedError>;
19
+ readonly take: (amount: number) => Effect.Effect<number, RejectedError>;
20
+ readonly total: Effect.Effect<number, RejectedError>;
21
+ readonly totals: Stream.Stream<Update<number>, RejectedError>;
22
+ readonly warm: Effect.Effect<void, RejectedError>;
23
+ }
24
+ interface Wire {
25
+ readonly fetch: typeof globalThis.fetch;
26
+ readonly loseNextReply: Effect.Effect<void>;
27
+ readonly opened: MutableRef.MutableRef<number>;
28
+ readonly sockets: Set<globalThis.WebSocket>;
29
+ readonly webSocket: typeof globalThis.WebSocket;
30
+ }
31
+ interface Connection {
32
+ readonly headers: Headers;
33
+ readonly url: string;
34
+ readonly wire: Wire;
35
+ }
36
+ type Connect = (connection: Connection) => Effect.Effect<(key: string) => TallyHandle, never, Scope.Scope>;
37
+ declare const clients: readonly (readonly [string, Connect])[];
38
+ declare const wired: Effect.Effect<Wire>;
39
+ export { type Connect, type Headers, type TallyHandle, type Update, type Wire, clients, wired };
@@ -0,0 +1,97 @@
1
+ import { RejectedError } from "./rejected.errors.mjs";
2
+ import { Tally } from "./tally.contract.mjs";
3
+ import { Cause, Effect, Layer, MutableRef, Option, Queue, Schema, Stream } from "effect";
4
+ import { createClient } from "@akterjs/client";
5
+ import { akterClient } from "@akterjs/client/effect";
6
+ import { FetchHttpClient } from "effect/http";
7
+ // src/tally-clients.layer.ts
8
+ const noSockets = 0;
9
+ const withoutPayload = (send, payload) => send(payload);
10
+ const settle = (promise) => Effect.tryPromise({
11
+ catch: (reason) => RejectedError.make({ reason }),
12
+ try: promise
13
+ });
14
+ const effectClient = ({ headers, url, wire }) => Effect.map(Layer.build(akterClient({
15
+ fetch: wire.fetch,
16
+ headers: () => headers,
17
+ url,
18
+ webSocket: wire.webSocket
19
+ })), (services) => function handleOf(key) {
20
+ const client = Tally.client(key);
21
+ const run = (use) => Effect.scoped(Effect.flatMap(client, use)).pipe(Effect.mapError((reason) => RejectedError.make({ reason })), Effect.provide(services));
22
+ const watch = (use) => Stream.unwrap(Effect.map(client, use)).pipe(Stream.mapError((reason) => RejectedError.make({ reason })), Stream.provide(services));
23
+ return {
24
+ add: (amount) => run((tally) => tally.add({ amount })),
25
+ create: (owner) => run((tally) => tally.create({ owner })),
26
+ ensure: (owner) => run((tally) => tally.ensure({ owner })),
27
+ fork: (into) => Effect.as(run((tally) => Effect.flatMap(tally.fork(into), (copy) => withoutPayload(copy.owner))), handleOf(into)),
28
+ owner: run((tally) => withoutPayload(tally.owner)),
29
+ state: watch((tally) => tally.state),
30
+ take: (amount) => run((tally) => tally.take({ amount })),
31
+ total: run((tally) => withoutPayload(tally.total)),
32
+ totals: watch((tally) => withoutPayload(tally.total.subscribe)),
33
+ warm: run((tally) => tally.warm)
34
+ };
35
+ });
36
+ const listened = (subscribe) => Stream.callback((queue) => Effect.acquireRelease(Effect.sync(() => subscribe((update) => {
37
+ Queue.offerUnsafe(queue, update);
38
+ }, (failure) => {
39
+ Queue.failCauseUnsafe(queue, Cause.fail(RejectedError.make({ reason: failure })));
40
+ })), (stop) => Effect.sync(stop)));
41
+ const promiseClient = ({ headers, url, wire }) => Effect.map(Effect.acquireRelease(Effect.sync(() => createClient({
42
+ actors: [Tally],
43
+ fetch: wire.fetch,
44
+ headers: () => headers,
45
+ url,
46
+ webSocket: wire.webSocket
47
+ })), (client) => Effect.promise(() => client.close())), (client) => function handleOf(key) {
48
+ const tally = client.Tally(key);
49
+ return {
50
+ add: (amount) => settle(() => tally.add({ amount })),
51
+ create: (owner) => settle(() => tally.create({ owner })),
52
+ ensure: (owner) => settle(() => tally.ensure({ owner })),
53
+ fork: (into) => Effect.as(Effect.flatMap(settle(() => tally.fork(into)), (copy) => settle(() => withoutPayload(copy.owner))), handleOf(into)),
54
+ owner: settle(() => withoutPayload(tally.owner)),
55
+ state: listened((onUpdate, onFailure) => tally.state.subscribe(onUpdate, onFailure)),
56
+ take: (amount) => settle(() => tally.take({ amount })),
57
+ total: settle(() => withoutPayload(tally.total)),
58
+ totals: listened((onUpdate, onFailure) => tally.total.subscribe(onUpdate, onFailure)),
59
+ warm: settle(() => tally.warm())
60
+ };
61
+ });
62
+ const clients = [["the Effect client", effectClient], ["the Promise client", promiseClient]];
63
+ const isSocket = Schema.is(Schema.instanceOf(globalThis.WebSocket));
64
+ const trackedSockets = (sockets, opened) => new Proxy(globalThis.WebSocket, { construct: (native, socket) => {
65
+ const constructed = Reflect.construct(native, socket);
66
+ const tracked = Option.getOrThrow(Option.liftPredicate(constructed, isSocket));
67
+ sockets.add(tracked);
68
+ MutableRef.increment(opened);
69
+ tracked.addEventListener("close", () => {
70
+ sockets.delete(tracked);
71
+ });
72
+ return tracked;
73
+ } });
74
+ const wired = Effect.gen(function* wired() {
75
+ const losing = MutableRef.make(false);
76
+ const sockets = /* @__PURE__ */ new Set();
77
+ const opened = MutableRef.make(noSockets);
78
+ const services = yield* Effect.context();
79
+ const send = yield* FetchHttpClient.Fetch;
80
+ const losingFetch = (input, init) => Effect.runPromiseWith(services)(Effect.gen(function* fetchOrLoseReply() {
81
+ const response = yield* Effect.promise(() => send(input, init));
82
+ if (MutableRef.get(losing)) {
83
+ MutableRef.set(losing, false);
84
+ yield* Effect.promise(() => response.arrayBuffer());
85
+ return yield* Effect.die("the fixture dropped the connection after the server replied");
86
+ }
87
+ return response;
88
+ }));
89
+ return {
90
+ fetch: Object.assign(losingFetch, send),
91
+ loseNextReply: Effect.sync(() => MutableRef.set(losing, true)),
92
+ opened,
93
+ sockets,
94
+ webSocket: trackedSockets(sockets, opened)
95
+ };
96
+ });
97
+ export { clients, wired };
@@ -0,0 +1,49 @@
1
+ import { ShortError } from "./tally.contract.mjs";
2
+ declare const TallyLive: import("effect/Layer").Layer<import("@akterjs/effect/contract").ActorHandlers<"Tally">, never, never>;
3
+ declare const app: {
4
+ readonly actors: readonly [import("@akterjs/effect/contract").Actor<"Tally", import("@akterjs/effect/contract").DefinitionOf<{
5
+ readonly commands: {
6
+ add: import("@akterjs/effect/contract").Command<import("effect/Schema").Struct<{
7
+ readonly amount: import("effect/Schema").Int;
8
+ }>, import("effect/Schema").Int, import("effect/Schema").Never, never>;
9
+ take: import("@akterjs/effect/contract").Command<import("effect/Schema").Struct<{
10
+ readonly amount: import("effect/Schema").Int;
11
+ }>, import("effect/Schema").Int, typeof ShortError, never>;
12
+ };
13
+ readonly create: {
14
+ owner: import("effect/Schema").String;
15
+ };
16
+ readonly key: import("effect/Schema").String;
17
+ readonly queries: {
18
+ owner: import("@akterjs/effect/contract").Query<import("effect/Schema").Void, import("effect/Schema").String, import("effect/Schema").Never, never>;
19
+ total: import("@akterjs/effect/contract").Query<import("effect/Schema").Void, import("effect/Schema").Int, import("effect/Schema").Never, never>;
20
+ };
21
+ readonly state: import("effect/Schema").Struct<{
22
+ readonly owner: import("effect/Schema").String;
23
+ readonly total: import("effect/Schema").Int;
24
+ }>;
25
+ }>> & import("@akterjs/effect/contract").ActorHistory<import("@akterjs/effect/contract").DefinitionOf<{
26
+ readonly commands: {
27
+ add: import("@akterjs/effect/contract").Command<import("effect/Schema").Struct<{
28
+ readonly amount: import("effect/Schema").Int;
29
+ }>, import("effect/Schema").Int, import("effect/Schema").Never, never>;
30
+ take: import("@akterjs/effect/contract").Command<import("effect/Schema").Struct<{
31
+ readonly amount: import("effect/Schema").Int;
32
+ }>, import("effect/Schema").Int, typeof ShortError, never>;
33
+ };
34
+ readonly create: {
35
+ owner: import("effect/Schema").String;
36
+ };
37
+ readonly key: import("effect/Schema").String;
38
+ readonly queries: {
39
+ owner: import("@akterjs/effect/contract").Query<import("effect/Schema").Void, import("effect/Schema").String, import("effect/Schema").Never, never>;
40
+ total: import("@akterjs/effect/contract").Query<import("effect/Schema").Void, import("effect/Schema").Int, import("effect/Schema").Never, never>;
41
+ };
42
+ readonly state: import("effect/Schema").Struct<{
43
+ readonly owner: import("effect/Schema").String;
44
+ readonly total: import("effect/Schema").Int;
45
+ }>;
46
+ }>>];
47
+ readonly layer: import("effect/Layer").Layer<import("@akterjs/effect/contract").ActorHandlers<"Tally">, never, never>;
48
+ };
49
+ export { TallyLive, app };
@@ -0,0 +1,26 @@
1
+ import { ShortError, Tally } from "./tally.contract.mjs";
2
+ import { Effect } from "effect";
3
+ // src/tally.actor.ts
4
+ const empty = 0;
5
+ const TallyLive = Tally.toLayer({
6
+ commands: {
7
+ add: (tally, { amount }) => Effect.as(Tally.update({ total: tally.total + amount }), tally.total + amount),
8
+ take: (tally, { amount }) => {
9
+ if (amount > tally.total) return ShortError.make({ total: tally.total });
10
+ return Effect.as(Tally.update({ total: tally.total - amount }), tally.total - amount);
11
+ }
12
+ },
13
+ create: ({ owner }) => Effect.succeed({
14
+ owner,
15
+ total: empty
16
+ }),
17
+ queries: {
18
+ owner: (tally) => Effect.succeed(tally.owner),
19
+ total: (tally) => Effect.succeed(tally.total)
20
+ }
21
+ });
22
+ const app = {
23
+ actors: [Tally],
24
+ layer: TallyLive
25
+ };
26
+ export { TallyLive, app };
@@ -0,0 +1,51 @@
1
+ import { Schema } from "effect";
2
+ declare const ShortError_base: Schema.Class<ShortError, Schema.TaggedStruct<"ShortError", {
3
+ readonly total: Schema.Int;
4
+ }>, import("effect/Cause").YieldableError & {
5
+ readonly brand: unique symbol;
6
+ }>;
7
+ declare class ShortError extends ShortError_base {}
8
+ declare const Tally: import("@akterjs/effect/contract").Actor<"Tally", import("@akterjs/effect/contract").DefinitionOf<{
9
+ readonly commands: {
10
+ add: import("@akterjs/effect/contract").Command<Schema.Struct<{
11
+ readonly amount: Schema.Int;
12
+ }>, Schema.Int, Schema.Never, never>;
13
+ take: import("@akterjs/effect/contract").Command<Schema.Struct<{
14
+ readonly amount: Schema.Int;
15
+ }>, Schema.Int, typeof ShortError, never>;
16
+ };
17
+ readonly create: {
18
+ owner: Schema.String;
19
+ };
20
+ readonly key: Schema.String;
21
+ readonly queries: {
22
+ owner: import("@akterjs/effect/contract").Query<Schema.Void, Schema.String, Schema.Never, never>;
23
+ total: import("@akterjs/effect/contract").Query<Schema.Void, Schema.Int, Schema.Never, never>;
24
+ };
25
+ readonly state: Schema.Struct<{
26
+ readonly owner: Schema.String;
27
+ readonly total: Schema.Int;
28
+ }>;
29
+ }>> & import("@akterjs/effect/contract").ActorHistory<import("@akterjs/effect/contract").DefinitionOf<{
30
+ readonly commands: {
31
+ add: import("@akterjs/effect/contract").Command<Schema.Struct<{
32
+ readonly amount: Schema.Int;
33
+ }>, Schema.Int, Schema.Never, never>;
34
+ take: import("@akterjs/effect/contract").Command<Schema.Struct<{
35
+ readonly amount: Schema.Int;
36
+ }>, Schema.Int, typeof ShortError, never>;
37
+ };
38
+ readonly create: {
39
+ owner: Schema.String;
40
+ };
41
+ readonly key: Schema.String;
42
+ readonly queries: {
43
+ owner: import("@akterjs/effect/contract").Query<Schema.Void, Schema.String, Schema.Never, never>;
44
+ total: import("@akterjs/effect/contract").Query<Schema.Void, Schema.Int, Schema.Never, never>;
45
+ };
46
+ readonly state: Schema.Struct<{
47
+ readonly owner: Schema.String;
48
+ readonly total: Schema.Int;
49
+ }>;
50
+ }>>;
51
+ export { ShortError, Tally };
@@ -0,0 +1,28 @@
1
+ import { Schema } from "effect";
2
+ import { Akter } from "@akterjs/effect/contract";
3
+ // src/tally.contract.ts
4
+ var ShortError = class extends Schema.TaggedError()("ShortError", { total: Schema.Int }) {};
5
+ const Tally = Akter.make("Tally", {
6
+ commands: {
7
+ add: Akter.command({
8
+ payload: { amount: Schema.Int },
9
+ success: Schema.Int
10
+ }),
11
+ take: Akter.command({
12
+ error: ShortError,
13
+ payload: { amount: Schema.Int },
14
+ success: Schema.Int
15
+ })
16
+ },
17
+ create: { owner: Schema.String },
18
+ key: Schema.String,
19
+ queries: {
20
+ owner: Akter.query({ success: Schema.String }),
21
+ total: Akter.query({ success: Schema.Int })
22
+ },
23
+ state: Schema.Struct({
24
+ owner: Schema.String,
25
+ total: Schema.Int
26
+ })
27
+ });
28
+ export { ShortError, Tally };
@@ -0,0 +1,15 @@
1
+ import { Engine } from "../engine-conformance.suite.mjs";
2
+ import { computerGuaranteeTitles } from "../guarantee-titles.pure.mjs";
3
+ import { Vitest } from "@effect/vitest";
4
+ type ComputerGuaranteeName = keyof typeof computerGuaranteeTitles;
5
+ interface ComputerSkipping<Failure, Services> {
6
+ /** An engine that serves `Workbench/bench-1`, with its computers bound to the job runtime's `Actor.computer(key)`. */
7
+ readonly engine: Engine<Failure, Services>;
8
+ readonly skip: Readonly<Partial<Record<ComputerGuaranteeName, string>>>;
9
+ }
10
+ /**
11
+ * Registers every computer guarantee against `engine`, each on fresh storage, skipping only those `skip` names with a
12
+ * reason. A guarantee added later runs on every engine that registers the suite.
13
+ */
14
+ declare const computerConformanceOf: <Failure, Services>({ engine, skip }: ComputerSkipping<Failure, Services>) => ((test: Vitest.MethodsNonLive<Services>) => void);
15
+ export { type ComputerGuaranteeName, computerConformanceOf };
@@ -0,0 +1,330 @@
1
+ import { computerGuaranteeTitles } from "../guarantee-titles.pure.mjs";
2
+ import { WorkbenchLive } from "./workbench.actor.mjs";
3
+ import { ComputerOp } from "./workbench.contract.mjs";
4
+ import { bench, ensure, get, list, remove, stop, unfinished, workbenchHost } from "./workbench-calls.pure.mjs";
5
+ import { expect } from "@effect/vitest";
6
+ import { Array, Clock, Context, Effect, Layer, Option, Predicate, Record, Schedule, String as String$1 } from "effect";
7
+ import { ActorHost } from "@akterjs/effect";
8
+ // src/workbench/computer-conformance.suite.ts
9
+ const { Pause, Resume, Step } = ComputerOp.cases;
10
+ const { exec, outcomesOf, ran, startedRun } = workbenchHost;
11
+ const holding = (hold) => Record.getSomes({ hold: Option.fromUndefinedOr(hold) });
12
+ const pause = (computer, hold) => Pause.make({
13
+ computer,
14
+ ...holding(hold)
15
+ });
16
+ const resume = (computer, hold) => Resume.make({
17
+ computer,
18
+ ...holding(hold)
19
+ });
20
+ /** A durable step on `box`, named `name`, with an optional limit in seconds. */
21
+ const step = (name, command, timeoutSeconds) => {
22
+ const limit = Record.getSomes({ timeoutSeconds: Option.fromUndefinedOr(timeoutSeconds) });
23
+ return Step.make({
24
+ command,
25
+ computer: "box",
26
+ name,
27
+ ...limit
28
+ });
29
+ };
30
+ /** Runs `use` on the Workbench on the real clock: a computer's calls wait on real processes and machines. */
31
+ const onBench = (storage, use) => {
32
+ const engine = storage.pipe(Layer.provide(WorkbenchLive));
33
+ const run = Effect.flatMap(Layer.build(engine), (services) => use(Context.get(services, ActorHost)));
34
+ return Effect.provideService(Effect.scoped(run), Clock.Clock, Clock.Clock.defaultValue());
35
+ };
36
+ /** A step that counts how many times it ran, in a file on the computer. */
37
+ const counting = "echo ran >> runs; grep -c ran runs";
38
+ const stepRunsOnce = (storage) => Effect.gen(function* resendAcrossRestart() {
39
+ const live = yield* onBench(storage, (host) => ran(host, "r-1", [
40
+ ensure("box"),
41
+ exec("box", "count", counting),
42
+ exec("box", "count", counting)
43
+ ]));
44
+ const restarted = yield* onBench(storage, (host) => ran(host, "r-2", [exec("box", "count", counting), exec("box", "read", "grep -c ran runs")]));
45
+ expect([live, restarted]).toStrictEqual([[
46
+ "up",
47
+ "exit 0: 1\n",
48
+ "exit 0: 1\n"
49
+ ], ["exit 0: 1\n", "exit 0: 1\n"]]);
50
+ });
51
+ const keepsFiles = (storage) => Effect.gen(function* writePauseStopRestart() {
52
+ const live = yield* onBench(storage, (host) => ran(host, "k-1", [
53
+ ensure("box"),
54
+ exec("box", "write", "echo kept > notes"),
55
+ pause("box"),
56
+ resume("box"),
57
+ stop("box")
58
+ ]));
59
+ const restarted = yield* onBench(storage, (host) => ran(host, "k-2", [
60
+ get("box"),
61
+ exec("box", "read", "cat notes"),
62
+ get("box")
63
+ ]));
64
+ expect([live, restarted]).toStrictEqual([[
65
+ "up",
66
+ "exit 0: ",
67
+ "paused",
68
+ "up",
69
+ "stopped"
70
+ ], [
71
+ "stopped",
72
+ "exit 0: kept\n",
73
+ "up"
74
+ ]]);
75
+ });
76
+ /** Neither a plain pause nor a held pause starts a stopped computer; a plain pause cannot interfere with its hold. */
77
+ const stoppedStaysStopped = (host) => Effect.gen(function* pauseStopped() {
78
+ const asked = yield* ran(host, "h-2", [
79
+ ensure("idle"),
80
+ stop("idle"),
81
+ pause("idle"),
82
+ pause("idle", "fork-3"),
83
+ pause("idle"),
84
+ exec("idle", "step", "true"),
85
+ get("idle"),
86
+ resume("idle", "fork-3"),
87
+ exec("idle", "step", "echo again")
88
+ ]);
89
+ expect(asked).toStrictEqual([
90
+ "up",
91
+ "stopped",
92
+ "stopped",
93
+ "stopped",
94
+ "ComputerHeldError",
95
+ "ComputerHeldError",
96
+ "stopped",
97
+ "stopped",
98
+ "exit 0: again\n"
99
+ ]);
100
+ });
101
+ const heldStaysPaused = (storage) => onBench(storage, (host) => Effect.gen(function* holdThenRelease() {
102
+ const outcomes = yield* ran(host, "h-1", [
103
+ ensure("box"),
104
+ pause("box", "fork-1"),
105
+ exec("box", "step", "true"),
106
+ ensure("box"),
107
+ resume("box"),
108
+ resume("box", "fork-2"),
109
+ pause("box"),
110
+ pause("box", "fork-2"),
111
+ pause("box", "fork-1"),
112
+ get("box"),
113
+ resume("box", "fork-1"),
114
+ resume("box", "fork-1"),
115
+ exec("box", "step", "echo once")
116
+ ]);
117
+ const held = "ComputerHeldError";
118
+ expect(outcomes).toStrictEqual([
119
+ "up",
120
+ "paused",
121
+ held,
122
+ held,
123
+ held,
124
+ held,
125
+ held,
126
+ held,
127
+ "paused",
128
+ "paused",
129
+ "up",
130
+ "up",
131
+ "exit 0: once\n"
132
+ ]);
133
+ yield* stoppedStaysStopped(host);
134
+ }));
135
+ const tried = 8;
136
+ const none = 0;
137
+ const first = 1;
138
+ /** `value` `count` times, for a count from none to `tried`. */
139
+ const times = (value, count) => Array.take(Array.replicate(value, tried), count);
140
+ const listed = (names) => `[${Array.join(Array.sort(names, String$1.Order), ",")}]`;
141
+ const limitHolds = (storage) => onBench(storage, (host) => Effect.gen(function* fillThenFree() {
142
+ const names = Array.makeBy(tried, (index) => `c${String(index)}`);
143
+ const filled = yield* ran(host, "l-1", [...Array.map(names, ensure), list]);
144
+ const made = Array.takeWhile(filled, (outcome) => outcome === "up").length;
145
+ const refused = `ComputerLimitError store ${String(made)}`;
146
+ const kept = Array.take(names, made);
147
+ const freed = yield* ran(host, "l-2", [
148
+ remove("c0"),
149
+ ensure("again"),
150
+ ensure("more"),
151
+ list
152
+ ]);
153
+ expect({
154
+ filled,
155
+ freed,
156
+ made: made > none && made < tried
157
+ }).toStrictEqual({
158
+ filled: [
159
+ ...times("up", made),
160
+ ...times(refused, tried - made),
161
+ listed(kept)
162
+ ],
163
+ freed: [
164
+ "removed",
165
+ "up",
166
+ refused,
167
+ listed([...Array.drop(kept, first), "again"])
168
+ ],
169
+ made: true
170
+ });
171
+ }));
172
+ const removedIsGone = (storage) => onBench(storage, (host) => Effect.gen(function* removeThenEnsure() {
173
+ const outcomes = yield* ran(host, "g-1", [
174
+ ensure("box"),
175
+ exec("box", "write", "echo x > left"),
176
+ remove("box"),
177
+ get("box"),
178
+ exec("box", "after", "true"),
179
+ remove("box"),
180
+ list,
181
+ ensure("box"),
182
+ exec("box", "look", "ls")
183
+ ]);
184
+ const missing = "ComputerNotFoundError";
185
+ expect(outcomes).toStrictEqual([
186
+ "up",
187
+ "exit 0: ",
188
+ "removed",
189
+ missing,
190
+ missing,
191
+ "removed",
192
+ "[]",
193
+ "up",
194
+ "exit 0: "
195
+ ]);
196
+ }));
197
+ /** Each journaled step as `akter steps list` reads it, newest first: its name, its state and its exit code once it ran. */
198
+ const journaled = (host) => Effect.map(host.steps({
199
+ actor: "Workbench",
200
+ key: bench
201
+ }), (steps) => Array.map(steps, ({ exitCode, name, state }) => {
202
+ const exit = Option.map(Option.fromUndefinedOr(exitCode), String);
203
+ return Array.join([
204
+ name,
205
+ state,
206
+ ...Option.toArray(exit)
207
+ ], " ");
208
+ }));
209
+ const stepJournaled = (storage) => onBench(storage, (host) => Effect.gen(function* stepThenRead() {
210
+ const outcomes = yield* ran(host, "j-1", [
211
+ ensure("box"),
212
+ step("hello", "echo hi"),
213
+ step("no", "exit 3")
214
+ ]);
215
+ const steps = yield* host.steps({
216
+ actor: "Workbench",
217
+ key: bench
218
+ });
219
+ const hello = Array.filter(steps, ({ name }) => name === "hello");
220
+ const read = yield* Effect.forEach(hello, ({ step: id }) => host.step({
221
+ actor: "Workbench",
222
+ key: bench,
223
+ step: id
224
+ }));
225
+ const missing = yield* host.step({
226
+ actor: "Workbench",
227
+ key: bench,
228
+ step: "no-such-step"
229
+ });
230
+ const journal = yield* journaled(host);
231
+ expect({
232
+ journal,
233
+ missing: Option.isNone(missing),
234
+ outcomes,
235
+ output: Array.map(Array.getSomes(read), ({ computer, stdout }) => [computer, stdout]),
236
+ quiet: Array.every(steps, ({ stdout }) => Predicate.isUndefined(stdout))
237
+ }).toStrictEqual({
238
+ journal: ["no ran 3", "hello ran 0"],
239
+ missing: true,
240
+ outcomes: [
241
+ "up",
242
+ "exit 0: hi\n",
243
+ "exit 3: "
244
+ ],
245
+ output: [["box", "hi\n"]],
246
+ quiet: true
247
+ });
248
+ }));
249
+ /** How long a cut-off guarantee waits for the step to be journaled before the engine under it stops. */
250
+ const journalPolls = 200;
251
+ const pollEvery = "100 millis";
252
+ /** How long it waits for the replayed job to settle the step: past the step's limit and grace, with room under load. */
253
+ const settlePolls = 400;
254
+ /** A step that counts its runs, then outlives the moment the engine stops under it, within its 3 s limit. */
255
+ const slow = "echo ran >> runs; sleep 2";
256
+ const slowLimit = 3;
257
+ /**
258
+ * Starts a job whose durable step is cut off by the engine stopping once the step is journaled, then restarts it.
259
+ * Wherever the stop fell, the step ran at most once and its job settles it: with the outcome the computer recorded
260
+ * when the command finished before the stop reached it, or `unknown` once its limit has passed without one.
261
+ */
262
+ const stepSurvivesRestart = (storage) => Effect.gen(function* cutThenReplay() {
263
+ const job = yield* onBench(storage, (host) => Effect.gen(function* startThenStop() {
264
+ const started = yield* startedRun(host, {
265
+ commandId: "c-1",
266
+ key: bench,
267
+ ops: [ensure("box"), step("slow", slow, slowLimit)]
268
+ });
269
+ yield* Effect.repeat(journaled(host), {
270
+ schedule: Schedule.spaced(pollEvery),
271
+ times: journalPolls,
272
+ until: Array.isReadonlyArrayNonEmpty
273
+ });
274
+ return started;
275
+ }));
276
+ const settled = yield* onBench(storage, (host) => Effect.gen(function* replayThenCount() {
277
+ const outcomes = yield* Effect.repeat(outcomesOf(host, bench, job), {
278
+ schedule: Schedule.spaced(pollEvery),
279
+ times: settlePolls,
280
+ until: (found) => !Array.contains(found, unfinished)
281
+ });
282
+ return {
283
+ counted: yield* ran(host, "c-2", [exec("box", "count", "grep -c ran runs")]),
284
+ journal: yield* journaled(host),
285
+ outcomes
286
+ };
287
+ }));
288
+ const recorded = {
289
+ journal: ["slow ran 0"],
290
+ outcomes: ["up", "exit 0: "]
291
+ };
292
+ const unknown = {
293
+ journal: ["slow unknown"],
294
+ outcomes: ["up", "unknown"]
295
+ };
296
+ const { counted, ...settledAs } = settled;
297
+ expect(counted).toStrictEqual(["exit 0: 1\n"]);
298
+ expect([recorded, unknown]).toContainEqual(settledAs);
299
+ });
300
+ /** The computer guarantees, by name; each runs the Workbench on a fresh store. */
301
+ const computerGuarantees = {
302
+ computerHeldStaysPaused: heldStaysPaused,
303
+ computerKeepsFiles: keepsFiles,
304
+ computerLimitHolds: limitHolds,
305
+ computerRemovedIsGone: removedIsGone,
306
+ computerStepJournaled: stepJournaled,
307
+ computerStepRunsOnce: stepRunsOnce,
308
+ computerStepSurvivesRestart: stepSurvivesRestart
309
+ };
310
+ const everyComputerGuarantee = Record.keys(computerGuarantees);
311
+ /**
312
+ * Registers every computer guarantee against `engine`, each on fresh storage, skipping only those `skip` names with a
313
+ * reason. A guarantee added later runs on every engine that registers the suite.
314
+ */
315
+ const computerConformanceOf = ({ engine, skip }) => (test) => {
316
+ for (const name of everyComputerGuarantee) {
317
+ const title = computerGuaranteeTitles[name];
318
+ const guarantee = computerGuarantees[name];
319
+ const run = () => Effect.scoped(Effect.flatMap(engine, (storage) => guarantee(storage)));
320
+ Option.match(Option.fromNullishOr(skip[name]), {
321
+ onNone: () => {
322
+ test.effect(title, run);
323
+ },
324
+ onSome: (reason) => {
325
+ test.effect.skip(`${title} (skipped: ${reason})`, run);
326
+ }
327
+ });
328
+ }
329
+ };
330
+ export { computerConformanceOf };