@akterjs/conformance 0.0.0-stage → 0.1.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/LICENSE +201 -0
- package/README.md +52 -2
- package/dist/client-contract.suite.d.mts +23 -0
- package/dist/client-contract.suite.mjs +279 -0
- package/dist/client-target.errors.d.mts +8 -0
- package/dist/client-target.errors.mjs +4 -0
- package/dist/engine-conformance.suite.d.mts +19 -0
- package/dist/engine-conformance.suite.mjs +357 -0
- package/dist/fork/fork-conformance.suite.d.mts +20 -0
- package/dist/fork/fork-conformance.suite.mjs +167 -0
- package/dist/guarantee-titles.pure.d.mts +31 -0
- package/dist/guarantee-titles.pure.mjs +32 -0
- package/dist/ledger/ledger-calls.pure.d.mts +79 -0
- package/dist/ledger/ledger-calls.pure.mjs +111 -0
- package/dist/ledger/ledger-checks.pure.d.mts +21 -0
- package/dist/ledger/ledger-checks.pure.mjs +37 -0
- package/dist/ledger/ledger.actor.d.mts +13 -0
- package/dist/ledger/ledger.actor.mjs +115 -0
- package/dist/ledger/ledger.contract.d.mts +310 -0
- package/dist/ledger/ledger.contract.mjs +112 -0
- package/dist/ledger/plain-ledger.actor.d.mts +4 -0
- package/dist/ledger/plain-ledger.actor.mjs +214 -0
- package/dist/rejected.errors.d.mts +8 -0
- package/dist/rejected.errors.mjs +4 -0
- package/dist/tally-clients.layer.d.mts +39 -0
- package/dist/tally-clients.layer.mjs +97 -0
- package/dist/tally.actor.d.mts +49 -0
- package/dist/tally.actor.mjs +26 -0
- package/dist/tally.contract.d.mts +51 -0
- package/dist/tally.contract.mjs +28 -0
- package/dist/workbench/computer-conformance.suite.d.mts +15 -0
- package/dist/workbench/computer-conformance.suite.mjs +330 -0
- package/dist/workbench/workbench-calls.pure.d.mts +45 -0
- package/dist/workbench/workbench-calls.pure.mjs +75 -0
- package/dist/workbench/workbench.actor.d.mts +4 -0
- package/dist/workbench/workbench.actor.mjs +107 -0
- package/dist/workbench/workbench.contract.d.mts +220 -0
- package/dist/workbench/workbench.contract.mjs +54 -0
- package/package.json +86 -3
- package/src/client-contract.suite.ts +413 -0
- package/src/client-target.errors.ts +7 -0
- package/src/engine-conformance.suite.ts +484 -0
- package/src/fork/fork-conformance.suite.ts +204 -0
- package/src/guarantee-titles.pure.ts +50 -0
- package/src/ledger/ledger-calls.pure.ts +179 -0
- package/src/ledger/ledger-checks.pure.ts +84 -0
- package/src/ledger/ledger.actor.ts +196 -0
- package/src/ledger/ledger.contract.ts +76 -0
- package/src/ledger/plain-ledger.actor.ts +246 -0
- package/src/rejected.errors.ts +6 -0
- package/src/tally-clients.layer.ts +221 -0
- package/src/tally.actor.ts +28 -0
- package/src/tally.contract.ts +22 -0
- package/src/workbench/computer-conformance.suite.ts +380 -0
- package/src/workbench/workbench-calls.pure.ts +92 -0
- package/src/workbench/workbench.actor.ts +175 -0
- package/src/workbench/workbench.contract.ts +47 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { guaranteeTitles } from "./guarantee-titles.pure.mjs";
|
|
2
|
+
import { effectLedgers } from "./ledger/ledger.actor.mjs";
|
|
3
|
+
import { ada, amount, amountLater, amountTo, approving, beforeDue, billing, bob, depositOpening, fileDecisions, forgedWriting, key, ledgerHost, longAfterDue, nothing, overdrawnBy, payee, reading, robotDeposit, untilDue, withdrawOverdraft, writing } from "./ledger/ledger-calls.pure.mjs";
|
|
4
|
+
import { akterTables, decodeReply, fileDecision, isHostTable, isRefused, refusedPrefix } from "./ledger/ledger-checks.pure.mjs";
|
|
5
|
+
import { Ledger } from "./ledger/ledger.contract.mjs";
|
|
6
|
+
import { expect } from "@effect/vitest";
|
|
7
|
+
import { Array, Cause, Context, Effect, Exit, Layer, Option, Predicate, Queue, Record, Stream, String as String$1 } from "effect";
|
|
8
|
+
import { ActorHost } from "@akterjs/effect";
|
|
9
|
+
import { TestClock } from "effect/testing";
|
|
10
|
+
// src/engine-conformance.suite.ts
|
|
11
|
+
const { account, amounts, approve, ask, drained, held, send } = ledgerHost;
|
|
12
|
+
const tables = (host, declared) => {
|
|
13
|
+
const appTable = Predicate.not(isHostTable(declared));
|
|
14
|
+
return Effect.map(Effect.flatMap(ask(host, "tables"), (reply) => decodeReply(Ledger.queries.tables, reply)), Exit.map(Array.filter(appTable)));
|
|
15
|
+
};
|
|
16
|
+
const onEngine = (storage, handlers, use) => {
|
|
17
|
+
const engine = storage.pipe(Layer.provide(handlers));
|
|
18
|
+
const run = Effect.flatMap(Layer.build(engine), (services) => use(Context.get(services, ActorHost)));
|
|
19
|
+
return Effect.scoped(run);
|
|
20
|
+
};
|
|
21
|
+
const depositThenOverdraw = (host) => Effect.all([send(host, "deposit", depositOpening), send(host, "withdraw", withdrawOverdraft)]);
|
|
22
|
+
const replyMeansCommitted = (storage, declared) => Effect.gen(function* reopenAfterReplies() {
|
|
23
|
+
yield* onEngine(storage, declared.ledgers.live, (host) => Effect.all([send(host, "deposit", depositOpening), send(host, "deposit", {
|
|
24
|
+
commandId: "b",
|
|
25
|
+
payload: amount(7)
|
|
26
|
+
})]));
|
|
27
|
+
const seen = yield* onEngine(storage, declared.ledgers.live, (host) => Effect.all([account(host), amounts(host)]));
|
|
28
|
+
expect(seen).toStrictEqual([Exit.succeed({
|
|
29
|
+
balance: 12,
|
|
30
|
+
closed: false
|
|
31
|
+
}), Exit.succeed([5, 7])]);
|
|
32
|
+
});
|
|
33
|
+
const retriedCommandId = (storage, declared) => Effect.gen(function* retryAcrossReopens() {
|
|
34
|
+
const [first, retriedLive] = yield* onEngine(storage, declared.ledgers.live, (host) => Effect.all([depositThenOverdraw(host), depositThenOverdraw(host)]));
|
|
35
|
+
const retriedReopened = yield* onEngine(storage, declared.ledgers.live, depositThenOverdraw);
|
|
36
|
+
const after = yield* onEngine(storage, declared.ledgers.live, (host) => Effect.all([account(host), amounts(host)]));
|
|
37
|
+
const [, retriedWithdraw] = retriedReopened;
|
|
38
|
+
const overdrawn = yield* decodeReply(Ledger.commands.withdraw, retriedWithdraw);
|
|
39
|
+
expect([retriedLive, retriedReopened]).toStrictEqual([first, first]);
|
|
40
|
+
expect(overdrawn).toStrictEqual(Exit.fail(overdrawnBy));
|
|
41
|
+
expect(after).toStrictEqual([Exit.succeed({
|
|
42
|
+
balance: 5,
|
|
43
|
+
closed: false
|
|
44
|
+
}), Exit.succeed([5])]);
|
|
45
|
+
});
|
|
46
|
+
const rollsBack = (storage, declared) => onEngine(storage, declared.ledgers.live, (host) => Effect.gen(function* failThenDeposit() {
|
|
47
|
+
yield* send(host, "deposit", depositOpening);
|
|
48
|
+
const withdrawn = yield* send(host, "withdraw", withdrawOverdraft);
|
|
49
|
+
const crashed = yield* send(host, "crash", {
|
|
50
|
+
commandId: "c",
|
|
51
|
+
payload: amount(3)
|
|
52
|
+
});
|
|
53
|
+
const failures = yield* Effect.all([decodeReply(Ledger.commands.withdraw, withdrawn), decodeReply(Ledger.commands.crash, crashed)]);
|
|
54
|
+
const afterFailures = yield* Effect.all([account(host), amounts(host)]);
|
|
55
|
+
yield* send(host, "deposit", {
|
|
56
|
+
commandId: "b",
|
|
57
|
+
payload: amount(1)
|
|
58
|
+
});
|
|
59
|
+
const afterDeposit = yield* account(host);
|
|
60
|
+
expect(failures).toStrictEqual([Exit.fail(overdrawnBy), Exit.die(/* @__PURE__ */ new Error("the handler crashed after writing"))]);
|
|
61
|
+
expect(afterFailures).toStrictEqual([Exit.succeed({
|
|
62
|
+
balance: 5,
|
|
63
|
+
closed: false
|
|
64
|
+
}), Exit.succeed([5])]);
|
|
65
|
+
expect(afterDeposit).toStrictEqual(Exit.succeed({
|
|
66
|
+
balance: 6,
|
|
67
|
+
closed: false
|
|
68
|
+
}));
|
|
69
|
+
}));
|
|
70
|
+
const callersBeforeHandler = (storage, declared) => onEngine(storage, declared.ledgers.live, (host) => Effect.gen(function* refuseThenAllow() {
|
|
71
|
+
const refused = yield* Effect.all([isRefused(send(host, "close", {
|
|
72
|
+
commandId: "c-user",
|
|
73
|
+
payload: nothing
|
|
74
|
+
})), isRefused(send(host, "deposit", robotDeposit))]);
|
|
75
|
+
const untouched = yield* Effect.all([account(host), amounts(host)]);
|
|
76
|
+
yield* send(host, "close", {
|
|
77
|
+
commandId: "c-service",
|
|
78
|
+
payload: nothing,
|
|
79
|
+
principal: billing
|
|
80
|
+
});
|
|
81
|
+
const closed = yield* account(host);
|
|
82
|
+
expect(refused).toStrictEqual([true, true]);
|
|
83
|
+
expect(untouched).toStrictEqual([Exit.succeed({
|
|
84
|
+
balance: 0,
|
|
85
|
+
closed: false
|
|
86
|
+
}), Exit.succeed([])]);
|
|
87
|
+
expect(closed).toStrictEqual(Exit.succeed({
|
|
88
|
+
balance: 0,
|
|
89
|
+
closed: true
|
|
90
|
+
}));
|
|
91
|
+
}));
|
|
92
|
+
const fileRuleDecides = (storage, declared) => onEngine(storage, declared.ledgers.live, (host) => Effect.gen(function* decideFiles() {
|
|
93
|
+
const asked = (payload, principal) => fileDecision(host.query({
|
|
94
|
+
actor: "Ledger",
|
|
95
|
+
key,
|
|
96
|
+
member: "akter.files",
|
|
97
|
+
payload,
|
|
98
|
+
principal
|
|
99
|
+
}));
|
|
100
|
+
const bobs = [
|
|
101
|
+
yield* asked(reading, bob),
|
|
102
|
+
yield* asked(writing, bob),
|
|
103
|
+
yield* asked(forgedWriting, bob)
|
|
104
|
+
];
|
|
105
|
+
const others = [yield* asked(writing, ada), yield* asked(reading, billing)];
|
|
106
|
+
yield* send(host, "close", {
|
|
107
|
+
commandId: "c-files",
|
|
108
|
+
payload: nothing,
|
|
109
|
+
principal: billing
|
|
110
|
+
});
|
|
111
|
+
expect([
|
|
112
|
+
...bobs,
|
|
113
|
+
...others,
|
|
114
|
+
yield* asked(writing, ada)
|
|
115
|
+
]).toStrictEqual(fileDecisions);
|
|
116
|
+
}));
|
|
117
|
+
const migrationsOnOpen = (storage, declared) => Effect.gen(function* reopenWithNewMigration() {
|
|
118
|
+
const reserved = Option.getOrElse(declared.hostTables, () => akterTables);
|
|
119
|
+
const opened = yield* onEngine(storage, declared.ledgers.live, (host) => Effect.tap(tables(host, declared), () => send(host, "deposit", depositOpening)));
|
|
120
|
+
const reopened = yield* onEngine(storage, declared.ledgers.withNotes, (host) => Effect.all([tables(host, declared), amounts(host)]));
|
|
121
|
+
const intruded = yield* onEngine(storage, declared.ledgers.intruding(reserved), (host) => Effect.exit(ask(host, "account")));
|
|
122
|
+
const after = yield* onEngine(storage, declared.ledgers.withNotes, (host) => tables(host, declared));
|
|
123
|
+
expect(opened).toStrictEqual(Exit.succeed(["entries"]));
|
|
124
|
+
expect(reopened).toStrictEqual([Exit.succeed(["entries", "notes"]), Exit.succeed([5])]);
|
|
125
|
+
expect(Exit.match(intruded, {
|
|
126
|
+
onFailure: () => "refused",
|
|
127
|
+
onSuccess: () => "opened"
|
|
128
|
+
})).toBe("refused");
|
|
129
|
+
expect(after).toStrictEqual(Exit.succeed(["entries", "notes"]));
|
|
130
|
+
});
|
|
131
|
+
const queriesNeverWrite = (storage, declared) => onEngine(storage, declared.ledgers.live, (host) => Effect.gen(function* writeInsideQuery() {
|
|
132
|
+
yield* send(host, "deposit", depositOpening);
|
|
133
|
+
const scribbled = yield* Effect.exit(Effect.flatMap(ask(host, "scribble"), (reply) => decodeReply(Ledger.queries.scribble, reply)));
|
|
134
|
+
const after = yield* amounts(host);
|
|
135
|
+
expect([Exit.isSuccess(scribbled) && Exit.isSuccess(scribbled.value), after]).toStrictEqual([false, Exit.succeed([5])]);
|
|
136
|
+
}));
|
|
137
|
+
const engineTablesOwned = (storage, declared) => onEngine(storage, declared.ledgers.live, (host) => Effect.gen(function* writeEngineTable() {
|
|
138
|
+
yield* send(host, "deposit", depositOpening);
|
|
139
|
+
const tampered = yield* Effect.exit(send(host, "tamper", {
|
|
140
|
+
commandId: "t",
|
|
141
|
+
payload: amount(7)
|
|
142
|
+
}));
|
|
143
|
+
const refusal = Exit.match(tampered, {
|
|
144
|
+
onFailure: Cause.pretty,
|
|
145
|
+
onSuccess: String$1.trim
|
|
146
|
+
});
|
|
147
|
+
const after = yield* Effect.all([account(host), amounts(host)]);
|
|
148
|
+
expect([String$1.includes("akter_state is engine-owned")(refusal), after]).toStrictEqual([true, [Exit.succeed({
|
|
149
|
+
balance: 5,
|
|
150
|
+
closed: false
|
|
151
|
+
}), Exit.succeed([5])]]);
|
|
152
|
+
}));
|
|
153
|
+
const liveAfterCommit = (storage, declared) => onEngine(storage, declared.ledgers.live, (host) => Effect.gen(function* watchCommits() {
|
|
154
|
+
const updates = yield* Queue.unbounded();
|
|
155
|
+
const watched = host.subscribe({
|
|
156
|
+
actor: "Ledger",
|
|
157
|
+
key,
|
|
158
|
+
member: "account",
|
|
159
|
+
payload: nothing,
|
|
160
|
+
principal: ada
|
|
161
|
+
});
|
|
162
|
+
yield* Effect.forkChild(Stream.runForEach(watched, (update) => Queue.offer(updates, update.value)));
|
|
163
|
+
const next = Effect.flatMap(Queue.take(updates), (value) => decodeReply(Ledger.queries.account, value));
|
|
164
|
+
const before = yield* next;
|
|
165
|
+
yield* Effect.all([
|
|
166
|
+
send(host, "deposit", depositOpening),
|
|
167
|
+
send(host, "withdraw", withdrawOverdraft),
|
|
168
|
+
send(host, "crash", {
|
|
169
|
+
commandId: "c",
|
|
170
|
+
payload: amount(3)
|
|
171
|
+
}),
|
|
172
|
+
send(host, "deposit", {
|
|
173
|
+
commandId: "b",
|
|
174
|
+
payload: amount(1)
|
|
175
|
+
})
|
|
176
|
+
]);
|
|
177
|
+
const seen = yield* Effect.all([next, next]);
|
|
178
|
+
expect([before, ...seen]).toStrictEqual([
|
|
179
|
+
Exit.succeed({
|
|
180
|
+
balance: 0,
|
|
181
|
+
closed: false
|
|
182
|
+
}),
|
|
183
|
+
Exit.succeed({
|
|
184
|
+
balance: 5,
|
|
185
|
+
closed: false
|
|
186
|
+
}),
|
|
187
|
+
Exit.succeed({
|
|
188
|
+
balance: 6,
|
|
189
|
+
closed: false
|
|
190
|
+
})
|
|
191
|
+
]);
|
|
192
|
+
}));
|
|
193
|
+
const sentAfterCommit = (storage, declared) => Effect.gen(function* deliverAcrossReopen() {
|
|
194
|
+
const overdrawn = yield* onEngine(storage, declared.ledgers.live, (host) => Effect.gen(function* transferTwice() {
|
|
195
|
+
yield* send(host, "deposit", depositOpening);
|
|
196
|
+
const refused = yield* send(host, "transfer", {
|
|
197
|
+
commandId: "t-over",
|
|
198
|
+
payload: amountTo(9)
|
|
199
|
+
});
|
|
200
|
+
yield* send(host, "transfer", {
|
|
201
|
+
commandId: "t",
|
|
202
|
+
payload: amountTo(2)
|
|
203
|
+
});
|
|
204
|
+
return yield* decodeReply(Ledger.commands.transfer, refused);
|
|
205
|
+
}));
|
|
206
|
+
const delivered = yield* onEngine(storage, declared.ledgers.live, (host) => Effect.gen(function* drainTwice() {
|
|
207
|
+
yield* account(host);
|
|
208
|
+
yield* host.drain;
|
|
209
|
+
const firstDrain = yield* Effect.all([account(host), amounts(host, payee)]);
|
|
210
|
+
yield* host.drain;
|
|
211
|
+
const drainedAgain = yield* amounts(host, payee);
|
|
212
|
+
return [...firstDrain, drainedAgain];
|
|
213
|
+
}));
|
|
214
|
+
expect(overdrawn).toStrictEqual(Exit.fail(overdrawnBy));
|
|
215
|
+
expect(delivered).toStrictEqual([
|
|
216
|
+
Exit.succeed({
|
|
217
|
+
balance: 3,
|
|
218
|
+
closed: false
|
|
219
|
+
}),
|
|
220
|
+
Exit.succeed([2]),
|
|
221
|
+
Exit.succeed([2])
|
|
222
|
+
]);
|
|
223
|
+
});
|
|
224
|
+
const signalAfterCommit = (storage, declared) => Effect.gen(function* approveAcrossReopen() {
|
|
225
|
+
const [job, live] = yield* onEngine(storage, declared.ledgers.live, (host) => Effect.gen(function* holdThenApprove() {
|
|
226
|
+
const id = yield* held(host, "h");
|
|
227
|
+
yield* approve(host, "refused", approving({
|
|
228
|
+
amount: 3,
|
|
229
|
+
job: id,
|
|
230
|
+
refuse: true
|
|
231
|
+
}));
|
|
232
|
+
const afterRefusal = yield* drained(host);
|
|
233
|
+
yield* approve(host, "approved", approving({
|
|
234
|
+
amount: 5,
|
|
235
|
+
job: id,
|
|
236
|
+
refuse: false
|
|
237
|
+
}));
|
|
238
|
+
return [id, [afterRefusal, yield* drained(host)]];
|
|
239
|
+
}));
|
|
240
|
+
const reopened = yield* onEngine(storage, declared.ledgers.live, (host) => Effect.gen(function* retryThenApproveLate() {
|
|
241
|
+
yield* approve(host, "approved", approving({
|
|
242
|
+
amount: 5,
|
|
243
|
+
job,
|
|
244
|
+
refuse: false
|
|
245
|
+
}));
|
|
246
|
+
const afterRetry = yield* drained(host);
|
|
247
|
+
yield* approve(host, "late", approving({
|
|
248
|
+
amount: 7,
|
|
249
|
+
job,
|
|
250
|
+
refuse: false
|
|
251
|
+
}));
|
|
252
|
+
return [afterRetry, yield* drained(host)];
|
|
253
|
+
}));
|
|
254
|
+
expect([...live, ...reopened]).toStrictEqual([
|
|
255
|
+
Exit.succeed([]),
|
|
256
|
+
Exit.succeed([5]),
|
|
257
|
+
Exit.succeed([5]),
|
|
258
|
+
Exit.succeed([5])
|
|
259
|
+
]);
|
|
260
|
+
});
|
|
261
|
+
const patchedJobs = (storage, declared) => Effect.gen(function* deployOverHeldJob() {
|
|
262
|
+
const before = yield* onEngine(storage, declared.ledgers.live, (host) => held(host, "h-before"));
|
|
263
|
+
const released = yield* onEngine(storage, declared.ledgers.patched, (host) => Effect.gen(function* approveBothHolds() {
|
|
264
|
+
yield* approve(host, "a-before", approving({
|
|
265
|
+
amount: 5,
|
|
266
|
+
job: before,
|
|
267
|
+
refuse: false
|
|
268
|
+
}));
|
|
269
|
+
const afterOld = yield* drained(host);
|
|
270
|
+
const after = yield* held(host, "h-after");
|
|
271
|
+
yield* approve(host, "a-after", approving({
|
|
272
|
+
amount: 7,
|
|
273
|
+
job: after,
|
|
274
|
+
refuse: false
|
|
275
|
+
}));
|
|
276
|
+
return [afterOld, yield* drained(host)];
|
|
277
|
+
}));
|
|
278
|
+
expect(released).toStrictEqual([Exit.succeed([5]), Exit.succeed([5, 14])]);
|
|
279
|
+
});
|
|
280
|
+
const advance = (host, elapsed) => Effect.gen(function* advanceAndDrain() {
|
|
281
|
+
yield* TestClock.adjust(elapsed);
|
|
282
|
+
yield* account(host);
|
|
283
|
+
yield* host.drain;
|
|
284
|
+
return yield* account(host);
|
|
285
|
+
});
|
|
286
|
+
const timerFiresOnce = (storage, declared) => Effect.gen(function* fireAcrossReopen() {
|
|
287
|
+
const early = yield* onEngine(storage, declared.ledgers.live, (host) => Effect.andThen(send(host, "depositLater", {
|
|
288
|
+
commandId: "l",
|
|
289
|
+
payload: amountLater(5)
|
|
290
|
+
}), advance(host, beforeDue)));
|
|
291
|
+
const later = yield* onEngine(storage, declared.ledgers.live, (host) => Effect.all([
|
|
292
|
+
advance(host, untilDue),
|
|
293
|
+
advance(host, longAfterDue),
|
|
294
|
+
amounts(host)
|
|
295
|
+
]));
|
|
296
|
+
expect([early, ...later]).toStrictEqual([
|
|
297
|
+
Exit.succeed({
|
|
298
|
+
balance: 0,
|
|
299
|
+
closed: false
|
|
300
|
+
}),
|
|
301
|
+
Exit.succeed({
|
|
302
|
+
balance: 5,
|
|
303
|
+
closed: false
|
|
304
|
+
}),
|
|
305
|
+
Exit.succeed({
|
|
306
|
+
balance: 5,
|
|
307
|
+
closed: false
|
|
308
|
+
}),
|
|
309
|
+
Exit.succeed([5])
|
|
310
|
+
]);
|
|
311
|
+
});
|
|
312
|
+
const onFresh = (engine, guarantee) => Effect.scoped(Effect.flatMap(engine, guarantee));
|
|
313
|
+
const guarantees = {
|
|
314
|
+
callersBeforeHandler,
|
|
315
|
+
engineTablesOwned,
|
|
316
|
+
fileRuleDecides,
|
|
317
|
+
liveAfterCommit,
|
|
318
|
+
migrationsOnOpen,
|
|
319
|
+
patchedJobs,
|
|
320
|
+
queriesNeverWrite,
|
|
321
|
+
replyMeansCommitted,
|
|
322
|
+
retriedCommandId,
|
|
323
|
+
rollsBack,
|
|
324
|
+
sentAfterCommit,
|
|
325
|
+
signalAfterCommit,
|
|
326
|
+
timerFiresOnce
|
|
327
|
+
};
|
|
328
|
+
/** Every guarantee, by name. */
|
|
329
|
+
const everyGuarantee = Record.keys(guarantees);
|
|
330
|
+
const conformanceOf = ({ engine, hostTables, ledgers = effectLedgers, skip }) => (test) => {
|
|
331
|
+
const declared = {
|
|
332
|
+
hostTables: Option.fromNullishOr(hostTables),
|
|
333
|
+
ledgers
|
|
334
|
+
};
|
|
335
|
+
const refused = Option.flatMap(declared.hostTables, refusedPrefix);
|
|
336
|
+
if (Option.isSome(refused)) {
|
|
337
|
+
test.effect(`the engine's host table prefix "${String(hostTables)}" is refused: ${refused.value}`, () => Effect.die(refused.value));
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
for (const name of everyGuarantee) {
|
|
341
|
+
const title = guaranteeTitles[name];
|
|
342
|
+
const run = () => onFresh(engine, (storage) => guarantees[name](storage, declared));
|
|
343
|
+
Option.match(Option.fromNullishOr(skip[name]), {
|
|
344
|
+
onNone: () => {
|
|
345
|
+
test.effect(title, run);
|
|
346
|
+
},
|
|
347
|
+
onSome: (reason) => {
|
|
348
|
+
test.effect.skip(`${title} (skipped: ${reason})`, run);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
const conformance = (engine) => conformanceOf({
|
|
354
|
+
engine,
|
|
355
|
+
skip: {}
|
|
356
|
+
});
|
|
357
|
+
export { conformance, conformanceOf };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Engine } from "../engine-conformance.suite.mjs";
|
|
2
|
+
import { forkGuaranteeTitles } from "../guarantee-titles.pure.mjs";
|
|
3
|
+
import { Vitest } from "@effect/vitest";
|
|
4
|
+
type ForkGuaranteeName = keyof typeof forkGuaranteeTitles;
|
|
5
|
+
interface ForkSkipping<Failure, Services> {
|
|
6
|
+
/**
|
|
7
|
+
* An engine that serves `Ledger/acct-1` and `Ledger/acct-5`, a key the suite forks it into, and `Workbench/bench-1`
|
|
8
|
+
* and `Workbench/bench-250`, with their computers bound to the job runtime's `Actor.computer(key)` and reached by the
|
|
9
|
+
* engine's fork. An engine that places an actor by its key must place each fork's key on its parent's shard: Akter
|
|
10
|
+
* Cloud forks only within a shard.
|
|
11
|
+
*/
|
|
12
|
+
readonly engine: Engine<Failure, Services>;
|
|
13
|
+
readonly skip: Readonly<Partial<Record<ForkGuaranteeName, string>>>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Registers every fork guarantee against `engine`, each on fresh storage, skipping only those `skip` names with a
|
|
17
|
+
* reason. A guarantee added later runs on every engine that registers the suite.
|
|
18
|
+
*/
|
|
19
|
+
declare const forkConformanceOf: <Failure, Services>({ engine, skip }: ForkSkipping<Failure, Services>) => ((test: Vitest.MethodsNonLive<Services>) => void);
|
|
20
|
+
export { type ForkGuaranteeName, forkConformanceOf };
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { forkGuaranteeTitles } from "../guarantee-titles.pure.mjs";
|
|
2
|
+
import { effectLedgers } from "../ledger/ledger.actor.mjs";
|
|
3
|
+
import { amount, amountLater, depositOpening, forked, ledgerHost, longAfterDue } from "../ledger/ledger-calls.pure.mjs";
|
|
4
|
+
import { WorkbenchLive } from "../workbench/workbench.actor.mjs";
|
|
5
|
+
import { bench, ensure, forkedBench, get, list, stop, workbenchHost } from "../workbench/workbench-calls.pure.mjs";
|
|
6
|
+
import { expect } from "@effect/vitest";
|
|
7
|
+
import { Clock, Context, Effect, Exit, Layer, Option, Record } from "effect";
|
|
8
|
+
import { ActorHost } from "@akterjs/effect";
|
|
9
|
+
import { TestClock } from "effect/testing";
|
|
10
|
+
// src/fork/fork-conformance.suite.ts
|
|
11
|
+
const { account, amounts, fork, send } = ledgerHost;
|
|
12
|
+
const onEngine = (storage, use) => {
|
|
13
|
+
const engine = storage.pipe(Layer.provide(effectLedgers.live));
|
|
14
|
+
const run = Effect.flatMap(Layer.build(engine), (services) => use(Context.get(services, ActorHost)));
|
|
15
|
+
return Effect.scoped(run);
|
|
16
|
+
};
|
|
17
|
+
/** Both accounts: the parent `acct-1`, then the fork `acct-5`, each with its balance and its entries. */
|
|
18
|
+
const both = (host) => Effect.all([
|
|
19
|
+
account(host),
|
|
20
|
+
amounts(host),
|
|
21
|
+
account(host, forked),
|
|
22
|
+
amounts(host, forked)
|
|
23
|
+
]);
|
|
24
|
+
const startsQuiet = (storage) => Effect.gen(function* forkThenDiverge() {
|
|
25
|
+
const [cut, retried, apart] = yield* onEngine(storage, (host) => Effect.gen(function* forkWithATimerPending() {
|
|
26
|
+
yield* send(host, "deposit", depositOpening);
|
|
27
|
+
yield* send(host, "depositLater", {
|
|
28
|
+
commandId: "l",
|
|
29
|
+
payload: amountLater(5)
|
|
30
|
+
});
|
|
31
|
+
const version = yield* fork(host, "f");
|
|
32
|
+
yield* send(host, "deposit", {
|
|
33
|
+
commandId: "b",
|
|
34
|
+
payload: amount(7)
|
|
35
|
+
});
|
|
36
|
+
yield* send(host, "deposit", {
|
|
37
|
+
account: forked,
|
|
38
|
+
commandId: "c",
|
|
39
|
+
payload: amount(1)
|
|
40
|
+
});
|
|
41
|
+
const again = yield* fork(host, "f");
|
|
42
|
+
yield* TestClock.adjust(longAfterDue);
|
|
43
|
+
yield* account(host);
|
|
44
|
+
yield* host.drain;
|
|
45
|
+
return [
|
|
46
|
+
version,
|
|
47
|
+
again,
|
|
48
|
+
yield* both(host)
|
|
49
|
+
];
|
|
50
|
+
}));
|
|
51
|
+
const reopened = yield* onEngine(storage, both);
|
|
52
|
+
const settled = [
|
|
53
|
+
Exit.succeed({
|
|
54
|
+
balance: 17,
|
|
55
|
+
closed: false
|
|
56
|
+
}),
|
|
57
|
+
Exit.succeed([
|
|
58
|
+
5,
|
|
59
|
+
7,
|
|
60
|
+
5
|
|
61
|
+
]),
|
|
62
|
+
Exit.succeed({
|
|
63
|
+
balance: 6,
|
|
64
|
+
closed: false
|
|
65
|
+
}),
|
|
66
|
+
Exit.succeed([5, 1])
|
|
67
|
+
];
|
|
68
|
+
expect(retried).toBe(cut);
|
|
69
|
+
expect([apart, reopened]).toStrictEqual([settled, settled]);
|
|
70
|
+
});
|
|
71
|
+
const { exec, ran, ranAt } = workbenchHost;
|
|
72
|
+
/** Runs `use` on the Workbench on the real clock: a computer's calls wait on real processes and machines. */
|
|
73
|
+
const onBench = (storage, use) => {
|
|
74
|
+
const engine = storage.pipe(Layer.provide(WorkbenchLive));
|
|
75
|
+
const run = Effect.flatMap(Layer.build(engine), (services) => use(Context.get(services, ActorHost)));
|
|
76
|
+
return Effect.provideService(Effect.scoped(run), Clock.Clock, Clock.Clock.defaultValue());
|
|
77
|
+
};
|
|
78
|
+
const forkBench = (host, commandId) => host.fork({
|
|
79
|
+
actor: "Workbench",
|
|
80
|
+
commandId,
|
|
81
|
+
into: forkedBench,
|
|
82
|
+
key: bench
|
|
83
|
+
});
|
|
84
|
+
const forkedWithComputers = (host) => Effect.gen(function* forkTwiceThenDiverge() {
|
|
85
|
+
const made = yield* ran(host, "fc-1", [
|
|
86
|
+
ensure("box"),
|
|
87
|
+
exec("box", "write", "echo at-cut > notes"),
|
|
88
|
+
ensure("idle"),
|
|
89
|
+
stop("idle")
|
|
90
|
+
]);
|
|
91
|
+
const cut = yield* forkBench(host, "fc");
|
|
92
|
+
const retried = yield* forkBench(host, "fc");
|
|
93
|
+
const original = yield* ran(host, "fc-2", [
|
|
94
|
+
get("box"),
|
|
95
|
+
get("idle"),
|
|
96
|
+
exec("box", "later", "echo later >> notes")
|
|
97
|
+
]);
|
|
98
|
+
const copy = yield* ranAt(host, forkedBench, {
|
|
99
|
+
commandId: "fc-3",
|
|
100
|
+
ops: [
|
|
101
|
+
list,
|
|
102
|
+
get("box"),
|
|
103
|
+
get("idle"),
|
|
104
|
+
exec("box", "copy-read", "cat notes"),
|
|
105
|
+
exec("box", "copy-write", "echo copy >> notes")
|
|
106
|
+
]
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
after: yield* ran(host, "fc-4", [exec("box", "reread", "cat notes")]),
|
|
110
|
+
copy,
|
|
111
|
+
cut,
|
|
112
|
+
made,
|
|
113
|
+
original,
|
|
114
|
+
retried
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
const copiesComputers = (storage) => Effect.gen(function* forkWithComputers() {
|
|
118
|
+
const { cut, retried, ...outcomes } = yield* onBench(storage, forkedWithComputers);
|
|
119
|
+
expect(retried).toBe(cut);
|
|
120
|
+
expect(outcomes).toStrictEqual({
|
|
121
|
+
after: ["exit 0: at-cut\nlater\n"],
|
|
122
|
+
copy: [
|
|
123
|
+
"[box,idle]",
|
|
124
|
+
"stopped",
|
|
125
|
+
"stopped",
|
|
126
|
+
"exit 0: at-cut\n",
|
|
127
|
+
"exit 0: "
|
|
128
|
+
],
|
|
129
|
+
made: [
|
|
130
|
+
"up",
|
|
131
|
+
"exit 0: ",
|
|
132
|
+
"up",
|
|
133
|
+
"stopped"
|
|
134
|
+
],
|
|
135
|
+
original: [
|
|
136
|
+
"up",
|
|
137
|
+
"stopped",
|
|
138
|
+
"exit 0: "
|
|
139
|
+
]
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
/** The fork guarantees, by name; each runs the Ledger on a fresh store. */
|
|
143
|
+
const forkGuarantees = {
|
|
144
|
+
forkCopiesComputers: copiesComputers,
|
|
145
|
+
forkStartsQuiet: startsQuiet
|
|
146
|
+
};
|
|
147
|
+
const everyForkGuarantee = Record.keys(forkGuarantees);
|
|
148
|
+
/**
|
|
149
|
+
* Registers every fork guarantee against `engine`, each on fresh storage, skipping only those `skip` names with a
|
|
150
|
+
* reason. A guarantee added later runs on every engine that registers the suite.
|
|
151
|
+
*/
|
|
152
|
+
const forkConformanceOf = ({ engine, skip }) => (test) => {
|
|
153
|
+
for (const name of everyForkGuarantee) {
|
|
154
|
+
const title = forkGuaranteeTitles[name];
|
|
155
|
+
const guarantee = forkGuarantees[name];
|
|
156
|
+
const run = () => Effect.scoped(Effect.flatMap(engine, (storage) => guarantee(storage)));
|
|
157
|
+
Option.match(Option.fromNullishOr(skip[name]), {
|
|
158
|
+
onNone: () => {
|
|
159
|
+
test.effect(title, run);
|
|
160
|
+
},
|
|
161
|
+
onSome: (reason) => {
|
|
162
|
+
test.effect.skip(`${title} (skipped: ${reason})`, run);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
export { forkConformanceOf };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
declare const guaranteeTitles: {
|
|
2
|
+
readonly callersBeforeHandler: "caller checks run before the handler: a caller the command doesn't allow changes nothing";
|
|
3
|
+
readonly engineTablesOwned: "a handler may read akter_* tables but never write them: the write fails, names the table and changes nothing";
|
|
4
|
+
readonly fileRuleDecides: "a file call is decided by the actor's rule from the verified caller and state: a kind it doesn't declare is refused, and a payload naming another caller changes nothing";
|
|
5
|
+
readonly liveAfterCommit: "live queries update only after a commit: a subscriber sees each committed change and nothing from a rolled-back command";
|
|
6
|
+
readonly migrationsOnOpen: "migrations run when an actor opens, before its first call, and a migration added later runs on the next open";
|
|
7
|
+
readonly patchedJobs: "a job keeps the code its start declared: after a deploy adds a patch, a job started before it takes the old path and one started after takes the new";
|
|
8
|
+
readonly queriesNeverWrite: "queries never write: a write inside a query fails and leaves the actor unchanged";
|
|
9
|
+
readonly replyMeansCommitted: "a reply means the command committed: an engine reopened on the same storage sees every replied command";
|
|
10
|
+
readonly retriedCommandId: "a retried command id returns the recorded outcome and runs the handler once";
|
|
11
|
+
readonly rollsBack: "a handler failure rolls back every write the command made";
|
|
12
|
+
readonly sentAfterCommit: "a message a command sends is delivered after the command commits, at least once, and a rolled-back command sends none";
|
|
13
|
+
readonly signalAfterCommit: "a command's signal reaches its own actor's job after the command commits, once: a rolled-back command signals nothing and a finished job ignores it";
|
|
14
|
+
readonly timerFiresOnce: "a timer fires once at its due time, and not again after the engine restarts";
|
|
15
|
+
};
|
|
16
|
+
/** The computer guarantees' titles, which `@akterjs/conformance/computers` registers. */
|
|
17
|
+
declare const computerGuaranteeTitles: {
|
|
18
|
+
readonly computerHeldStaysPaused: "a held computer stays paused, or stopped if it was stopped: an ensure, a step, and a resume or pause without its hold are refused until its hold resumes it";
|
|
19
|
+
readonly computerKeepsFiles: "a computer keeps its files across pause, resume, stop and an engine restart";
|
|
20
|
+
readonly computerLimitHolds: "a computer past the store's limit is refused with ComputerLimitError and makes nothing, and one fits again once another is removed";
|
|
21
|
+
readonly computerRemovedIsGone: "a removed computer is gone with its disk: calls to it fail with ComputerNotFoundError, and ensuring it again starts it empty";
|
|
22
|
+
readonly computerStepJournaled: "a durable computer step journals its start and outcome in the actor, and its steps read back newest first, one with its output";
|
|
23
|
+
readonly computerStepRunsOnce: "a computer step runs once by its key: sent again, even after the engine restarts, it reads back the outcome it recorded";
|
|
24
|
+
readonly computerStepSurvivesRestart: "a durable computer step cut off by an engine restart never runs again: its replayed job settles it with its recorded outcome or unknown";
|
|
25
|
+
};
|
|
26
|
+
/** The fork guarantees' titles, which `@akterjs/conformance/fork` registers. */
|
|
27
|
+
declare const forkGuaranteeTitles: {
|
|
28
|
+
readonly forkCopiesComputers: "a fork copies each of the actor's computers by its disk at the cut, stopped: the copy holds the files written before the cut, the original's computers go back to where they were, and the two change independently";
|
|
29
|
+
readonly forkStartsQuiet: "a fork copies an actor as committed at its cut and starts quiet: the copy keeps no pending timer, the two change independently, and a retried fork id makes one copy";
|
|
30
|
+
};
|
|
31
|
+
export { computerGuaranteeTitles, forkGuaranteeTitles, guaranteeTitles };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// src/guarantee-titles.pure.ts
|
|
2
|
+
const guaranteeTitles = {
|
|
3
|
+
callersBeforeHandler: "caller checks run before the handler: a caller the command doesn't allow changes nothing",
|
|
4
|
+
engineTablesOwned: "a handler may read akter_* tables but never write them: the write fails, names the table and changes nothing",
|
|
5
|
+
fileRuleDecides: "a file call is decided by the actor's rule from the verified caller and state: a kind it doesn't declare is refused, and a payload naming another caller changes nothing",
|
|
6
|
+
liveAfterCommit: "live queries update only after a commit: a subscriber sees each committed change and nothing from a rolled-back command",
|
|
7
|
+
migrationsOnOpen: "migrations run when an actor opens, before its first call, and a migration added later runs on the next open",
|
|
8
|
+
patchedJobs: "a job keeps the code its start declared: after a deploy adds a patch, a job started before it takes the old path and one started after takes the new",
|
|
9
|
+
queriesNeverWrite: "queries never write: a write inside a query fails and leaves the actor unchanged",
|
|
10
|
+
replyMeansCommitted: "a reply means the command committed: an engine reopened on the same storage sees every replied command",
|
|
11
|
+
retriedCommandId: "a retried command id returns the recorded outcome and runs the handler once",
|
|
12
|
+
rollsBack: "a handler failure rolls back every write the command made",
|
|
13
|
+
sentAfterCommit: "a message a command sends is delivered after the command commits, at least once, and a rolled-back command sends none",
|
|
14
|
+
signalAfterCommit: "a command's signal reaches its own actor's job after the command commits, once: a rolled-back command signals nothing and a finished job ignores it",
|
|
15
|
+
timerFiresOnce: "a timer fires once at its due time, and not again after the engine restarts"
|
|
16
|
+
};
|
|
17
|
+
/** The computer guarantees' titles, which `@akterjs/conformance/computers` registers. */
|
|
18
|
+
const computerGuaranteeTitles = {
|
|
19
|
+
computerHeldStaysPaused: "a held computer stays paused, or stopped if it was stopped: an ensure, a step, and a resume or pause without its hold are refused until its hold resumes it",
|
|
20
|
+
computerKeepsFiles: "a computer keeps its files across pause, resume, stop and an engine restart",
|
|
21
|
+
computerLimitHolds: "a computer past the store's limit is refused with ComputerLimitError and makes nothing, and one fits again once another is removed",
|
|
22
|
+
computerRemovedIsGone: "a removed computer is gone with its disk: calls to it fail with ComputerNotFoundError, and ensuring it again starts it empty",
|
|
23
|
+
computerStepJournaled: "a durable computer step journals its start and outcome in the actor, and its steps read back newest first, one with its output",
|
|
24
|
+
computerStepRunsOnce: "a computer step runs once by its key: sent again, even after the engine restarts, it reads back the outcome it recorded",
|
|
25
|
+
computerStepSurvivesRestart: "a durable computer step cut off by an engine restart never runs again: its replayed job settles it with its recorded outcome or unknown"
|
|
26
|
+
};
|
|
27
|
+
/** The fork guarantees' titles, which `@akterjs/conformance/fork` registers. */
|
|
28
|
+
const forkGuaranteeTitles = {
|
|
29
|
+
forkCopiesComputers: "a fork copies each of the actor's computers by its disk at the cut, stopped: the copy holds the files written before the cut, the original's computers go back to where they were, and the two change independently",
|
|
30
|
+
forkStartsQuiet: "a fork copies an actor as committed at its cut and starts quiet: the copy keeps no pending timer, the two change independently, and a retried fork id makes one copy"
|
|
31
|
+
};
|
|
32
|
+
export { computerGuaranteeTitles, forkGuaranteeTitles, guaranteeTitles };
|