@boxxy/dispatch 0.1.0 → 0.1.3

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 (202) hide show
  1. package/README.md +4 -1
  2. package/dist/attempts/attempt-machine.d.ts +804 -0
  3. package/dist/attempts/attempt-moves.d.ts +2 -0
  4. package/dist/attempts/bind-attempt-session.d.ts +14 -0
  5. package/dist/attempts/build-attempt-change.d.ts +73 -0
  6. package/dist/attempts/build-attempt-fact.d.ts +3 -0
  7. package/dist/attempts/end-item-attempts.d.ts +18 -0
  8. package/dist/attempts/is-same-guard.d.ts +5 -0
  9. package/dist/attempts/pick-call-attempt.d.ts +10 -0
  10. package/dist/attempts/stop-stray-session.d.ts +11 -0
  11. package/dist/attempts/types.d.ts +142 -0
  12. package/dist/attempts/write-attempt-move.d.ts +7 -0
  13. package/dist/attempts/write-session-bind.d.ts +3 -0
  14. package/dist/claims/build-snapshot-hash.d.ts +2 -0
  15. package/dist/claims/claim-next.d.ts +6 -0
  16. package/dist/claims/pick-kept-session.d.ts +9 -0
  17. package/dist/claims/request-wake.d.ts +3 -0
  18. package/dist/claims/resume-claim.d.ts +3 -0
  19. package/dist/claims/start-claim.d.ts +3 -0
  20. package/dist/claims/types.d.ts +44 -0
  21. package/dist/closeouts/build-settle-plans.d.ts +17 -0
  22. package/dist/closeouts/close-out-attempt.d.ts +3 -0
  23. package/dist/closeouts/effect-handlers.d.ts +2 -0
  24. package/dist/closeouts/pick-settlement.d.ts +14 -0
  25. package/dist/closeouts/settle-attempt.d.ts +3 -0
  26. package/dist/closeouts/types.d.ts +50 -0
  27. package/dist/index.d.ts +38 -0
  28. package/dist/notices/constants.d.ts +4 -0
  29. package/dist/notices/create-null-notifier.d.ts +2 -0
  30. package/dist/notices/send-notices.d.ts +2 -0
  31. package/dist/notices/types.d.ts +11 -0
  32. package/dist/outbox/build-backoff-ms.d.ts +2 -0
  33. package/dist/outbox/build-initial-sender-state.d.ts +2 -0
  34. package/dist/outbox/build-outbox-status.d.ts +15 -0
  35. package/dist/outbox/constants.d.ts +2 -0
  36. package/dist/outbox/measure-wire-bytes.d.ts +2 -0
  37. package/dist/outbox/pick-sender-move.d.ts +10 -0
  38. package/dist/outbox/run-sender-once.d.ts +2 -0
  39. package/dist/outbox/take-batch.d.ts +3 -0
  40. package/dist/outbox/types.d.ts +61 -0
  41. package/dist/pull-requests/build-link-change.d.ts +11 -0
  42. package/dist/pull-requests/build-state-fact.d.ts +8 -0
  43. package/dist/pull-requests/collect-attempt-trailers.d.ts +2 -0
  44. package/dist/pull-requests/collect-seen-state.d.ts +7 -0
  45. package/dist/pull-requests/constants.d.ts +3 -0
  46. package/dist/pull-requests/link-pull-request.d.ts +2 -0
  47. package/dist/pull-requests/parse-pull-request-url.d.ts +2 -0
  48. package/dist/pull-requests/types.d.ts +37 -0
  49. package/dist/pull-requests/watch-pull-requests.d.ts +2 -0
  50. package/dist/readings/act-on-reading.d.ts +3 -0
  51. package/dist/readings/find-waiting-since.d.ts +9 -0
  52. package/dist/readings/pick-reading-action.d.ts +2 -0
  53. package/dist/readings/types.d.ts +79 -0
  54. package/dist/sessions/build-keep-writes.d.ts +9 -0
  55. package/dist/sessions/build-kept-clear.d.ts +2 -0
  56. package/dist/sessions/forget-sessions.d.ts +2 -0
  57. package/dist/sessions/is-kept-due.d.ts +2 -0
  58. package/dist/sessions/resume-kept-session.d.ts +6 -0
  59. package/dist/sessions/types.d.ts +19 -0
  60. package/dist/test-utils/apply-memory-change.d.ts +3 -0
  61. package/dist/test-utils/build-memory-reader.d.ts +3 -0
  62. package/dist/test-utils/build-mock-bind-change.d.ts +7 -0
  63. package/dist/test-utils/build-mock-claim-change.d.ts +2 -0
  64. package/dist/test-utils/build-mock-file-item-change.d.ts +2 -0
  65. package/dist/test-utils/build-mock-lever-change.d.ts +2 -0
  66. package/dist/test-utils/build-mock-new-id.d.ts +2 -0
  67. package/dist/test-utils/build-mock-note-change.d.ts +2 -0
  68. package/dist/test-utils/build-mock-pause-change.d.ts +2 -0
  69. package/dist/test-utils/build-mock-pull-request-change.d.ts +8 -0
  70. package/dist/test-utils/build-mock-relation-change.d.ts +7 -0
  71. package/dist/test-utils/build-mock-running-reading.d.ts +6 -0
  72. package/dist/test-utils/build-mock-settle-change.d.ts +7 -0
  73. package/dist/test-utils/build-mock-start-change.d.ts +2 -0
  74. package/dist/test-utils/claim-store-attempt-reader-cases.d.ts +2 -0
  75. package/dist/test-utils/claim-store-claim-cases.d.ts +2 -0
  76. package/dist/test-utils/claim-store-close-cases.d.ts +2 -0
  77. package/dist/test-utils/claim-store-contract.d.ts +2 -0
  78. package/dist/test-utils/claim-store-copy-cases.d.ts +2 -0
  79. package/dist/test-utils/claim-store-exclusive-cases.d.ts +2 -0
  80. package/dist/test-utils/claim-store-guard-cases.d.ts +2 -0
  81. package/dist/test-utils/claim-store-item-reader-cases.d.ts +2 -0
  82. package/dist/test-utils/claim-store-outbox-cases.d.ts +2 -0
  83. package/dist/test-utils/claim-store-race-cases.d.ts +2 -0
  84. package/dist/test-utils/claim-store-rollback-cases.d.ts +2 -0
  85. package/dist/test-utils/claim-store-seat-cases.d.ts +2 -0
  86. package/dist/test-utils/claim-store-session-cases.d.ts +2 -0
  87. package/dist/test-utils/collect-memory-limit-fills.d.ts +3 -0
  88. package/dist/test-utils/compare-text.d.ts +1 -0
  89. package/dist/test-utils/create-fake-backoff.d.ts +2 -0
  90. package/dist/test-utils/create-fake-clock.d.ts +5 -0
  91. package/dist/test-utils/create-fake-fact-sink.d.ts +8 -0
  92. package/dist/test-utils/create-fake-forge.d.ts +17 -0
  93. package/dist/test-utils/create-fake-worker.d.ts +42 -0
  94. package/dist/test-utils/create-memory-claim-store.d.ts +2 -0
  95. package/dist/test-utils/create-test-dispatch.d.ts +46 -0
  96. package/dist/test-utils/forge-contract.d.ts +15 -0
  97. package/dist/test-utils/is-same-held-seat.d.ts +2 -0
  98. package/dist/test-utils/types.d.ts +19 -0
  99. package/dist/test-utils/worker-contract.d.ts +14 -0
  100. package/dist/timeouts.d.ts +2 -0
  101. package/dist/types.d.ts +13 -0
  102. package/package.json +15 -2
  103. package/src/attempts/attempt-machine.ts +147 -0
  104. package/src/attempts/attempt-moves.ts +218 -0
  105. package/src/attempts/bind-attempt-session.ts +61 -0
  106. package/src/attempts/build-attempt-change.ts +540 -0
  107. package/src/attempts/build-attempt-fact.ts +135 -0
  108. package/src/attempts/end-item-attempts.ts +95 -0
  109. package/src/attempts/is-same-guard.ts +22 -0
  110. package/src/attempts/pick-call-attempt.ts +32 -0
  111. package/src/attempts/stop-stray-session.ts +48 -0
  112. package/src/attempts/types.ts +231 -0
  113. package/src/attempts/write-attempt-move.ts +41 -0
  114. package/src/attempts/write-session-bind.ts +47 -0
  115. package/src/claims/build-snapshot-hash.ts +9 -0
  116. package/src/claims/claim-next.ts +306 -0
  117. package/src/claims/pick-kept-session.ts +41 -0
  118. package/src/claims/request-wake.ts +17 -0
  119. package/src/claims/resume-claim.ts +56 -0
  120. package/src/claims/start-claim.ts +99 -0
  121. package/src/claims/types.ts +67 -0
  122. package/src/closeouts/build-settle-plans.ts +173 -0
  123. package/src/closeouts/close-out-attempt.ts +156 -0
  124. package/src/closeouts/effect-handlers.ts +232 -0
  125. package/src/closeouts/pick-settlement.ts +80 -0
  126. package/src/closeouts/settle-attempt.ts +161 -0
  127. package/src/closeouts/types.ts +87 -0
  128. package/src/index.ts +61 -0
  129. package/src/notices/constants.ts +7 -0
  130. package/src/notices/create-null-notifier.ts +6 -0
  131. package/src/notices/send-notices.ts +41 -0
  132. package/src/notices/types.ts +14 -0
  133. package/src/outbox/build-backoff-ms.ts +14 -0
  134. package/src/outbox/build-initial-sender-state.ts +14 -0
  135. package/src/outbox/build-outbox-status.ts +43 -0
  136. package/src/outbox/constants.ts +15 -0
  137. package/src/outbox/measure-wire-bytes.ts +31 -0
  138. package/src/outbox/pick-sender-move.ts +37 -0
  139. package/src/outbox/run-sender-once.ts +174 -0
  140. package/src/outbox/take-batch.ts +35 -0
  141. package/src/outbox/types.ts +76 -0
  142. package/src/pull-requests/build-link-change.ts +95 -0
  143. package/src/pull-requests/build-state-fact.ts +39 -0
  144. package/src/pull-requests/collect-attempt-trailers.ts +20 -0
  145. package/src/pull-requests/collect-seen-state.ts +19 -0
  146. package/src/pull-requests/constants.ts +5 -0
  147. package/src/pull-requests/link-pull-request.ts +119 -0
  148. package/src/pull-requests/parse-pull-request-url.ts +30 -0
  149. package/src/pull-requests/types.ts +63 -0
  150. package/src/pull-requests/watch-pull-requests.ts +308 -0
  151. package/src/readings/act-on-reading.ts +318 -0
  152. package/src/readings/find-waiting-since.ts +46 -0
  153. package/src/readings/pick-reading-action.ts +315 -0
  154. package/src/readings/types.ts +98 -0
  155. package/src/sessions/build-keep-writes.ts +55 -0
  156. package/src/sessions/build-kept-clear.ts +12 -0
  157. package/src/sessions/forget-sessions.ts +106 -0
  158. package/src/sessions/is-kept-due.ts +27 -0
  159. package/src/sessions/resume-kept-session.ts +25 -0
  160. package/src/sessions/types.ts +39 -0
  161. package/src/test-utils/apply-memory-change.ts +252 -0
  162. package/src/test-utils/build-memory-reader.ts +364 -0
  163. package/src/test-utils/build-mock-bind-change.ts +43 -0
  164. package/src/test-utils/build-mock-claim-change.ts +52 -0
  165. package/src/test-utils/build-mock-file-item-change.ts +20 -0
  166. package/src/test-utils/build-mock-lever-change.ts +18 -0
  167. package/src/test-utils/build-mock-new-id.ts +12 -0
  168. package/src/test-utils/build-mock-note-change.ts +18 -0
  169. package/src/test-utils/build-mock-pause-change.ts +19 -0
  170. package/src/test-utils/build-mock-pull-request-change.ts +63 -0
  171. package/src/test-utils/build-mock-relation-change.ts +40 -0
  172. package/src/test-utils/build-mock-running-reading.ts +19 -0
  173. package/src/test-utils/build-mock-settle-change.ts +70 -0
  174. package/src/test-utils/build-mock-start-change.ts +38 -0
  175. package/src/test-utils/claim-store-attempt-reader-cases.ts +357 -0
  176. package/src/test-utils/claim-store-claim-cases.ts +314 -0
  177. package/src/test-utils/claim-store-close-cases.ts +95 -0
  178. package/src/test-utils/claim-store-contract.ts +28 -0
  179. package/src/test-utils/claim-store-copy-cases.ts +100 -0
  180. package/src/test-utils/claim-store-exclusive-cases.ts +243 -0
  181. package/src/test-utils/claim-store-guard-cases.ts +383 -0
  182. package/src/test-utils/claim-store-item-reader-cases.ts +471 -0
  183. package/src/test-utils/claim-store-outbox-cases.ts +281 -0
  184. package/src/test-utils/claim-store-race-cases.ts +150 -0
  185. package/src/test-utils/claim-store-rollback-cases.ts +175 -0
  186. package/src/test-utils/claim-store-seat-cases.ts +270 -0
  187. package/src/test-utils/claim-store-session-cases.ts +289 -0
  188. package/src/test-utils/collect-memory-limit-fills.ts +15 -0
  189. package/src/test-utils/compare-text.ts +8 -0
  190. package/src/test-utils/create-fake-backoff.ts +16 -0
  191. package/src/test-utils/create-fake-clock.ts +20 -0
  192. package/src/test-utils/create-fake-fact-sink.ts +74 -0
  193. package/src/test-utils/create-fake-forge.ts +117 -0
  194. package/src/test-utils/create-fake-worker.ts +303 -0
  195. package/src/test-utils/create-memory-claim-store.ts +163 -0
  196. package/src/test-utils/create-test-dispatch.ts +597 -0
  197. package/src/test-utils/forge-contract.ts +171 -0
  198. package/src/test-utils/is-same-held-seat.ts +11 -0
  199. package/src/test-utils/types.ts +23 -0
  200. package/src/test-utils/worker-contract.ts +192 -0
  201. package/src/timeouts.ts +17 -0
  202. package/src/types.ts +32 -0
@@ -0,0 +1,243 @@
1
+ import { setImmediate } from 'node:timers/promises';
2
+ import { brandSchemas } from '@boxxy/model';
3
+ import { buildMockItemRules } from '@boxxy/model/test-utils/build-mock-item-rules';
4
+ import { buildMockV1ItemRow } from '@boxxy/model/test-utils/build-mock-v1-item-row';
5
+ import { buildMockFileItemChange } from './build-mock-file-item-change.ts';
6
+ import type { ClaimStoreCase } from './types.ts';
7
+
8
+ export const claimStoreExclusiveCases: readonly ClaimStoreCase[] = [
9
+ [
10
+ '#exclusive runs two calls started together one at a time',
11
+ async (store) => {
12
+ const events: string[] = [];
13
+ const gate = Promise.withResolvers<void>();
14
+
15
+ const first = store.exclusive(async () => {
16
+ events.push('first start');
17
+
18
+ await gate.promise;
19
+
20
+ events.push('first end');
21
+ });
22
+
23
+ const second = store.exclusive(() => {
24
+ events.push('second start');
25
+
26
+ return Promise.resolve();
27
+ });
28
+
29
+ // one turn of the event loop lets a store that doesn't wait start the second call
30
+ await setImmediate();
31
+
32
+ gate.resolve();
33
+
34
+ await Promise.all([first, second]);
35
+
36
+ return { received: events, expected: ['first start', 'first end', 'second start'] };
37
+ },
38
+ ],
39
+ [
40
+ '#exclusive leaves the state as it was when work throws',
41
+ async (store) => {
42
+ const item = buildMockV1ItemRow();
43
+
44
+ const writing = store.exclusive(async (transaction) => {
45
+ await transaction.applyChange(buildMockFileItemChange(item));
46
+
47
+ throw new Error('the work failed');
48
+ });
49
+
50
+ await writing.catch(() => null);
51
+
52
+ const snapshot: unknown = await store.read((transaction) =>
53
+ transaction.readSnapshot(brandSchemas.instant.parse(5000), buildMockItemRules()),
54
+ );
55
+
56
+ return {
57
+ received: snapshot,
58
+ expected: {
59
+ storeVersion: 0,
60
+ items: [],
61
+ relations: [],
62
+ levers: [],
63
+ limits: [],
64
+ wakes: [],
65
+ },
66
+ };
67
+ },
68
+ ],
69
+ [
70
+ '#exclusive rejects with the error work throws',
71
+ async (store) => {
72
+ const writing = store.exclusive(() => {
73
+ throw new Error('the work failed');
74
+ });
75
+
76
+ const settled = await writing.then(
77
+ () => 'resolved',
78
+ (error: unknown) => error,
79
+ );
80
+
81
+ return { received: settled, expected: new Error('the work failed') };
82
+ },
83
+ ],
84
+ [
85
+ '#exclusive runs a later call after an earlier call throws',
86
+ async (store) => {
87
+ const item = buildMockV1ItemRow();
88
+
89
+ const failing = store.exclusive(() => {
90
+ throw new Error('the work failed');
91
+ });
92
+
93
+ await failing.catch(() => null);
94
+
95
+ await store.exclusive((transaction) =>
96
+ transaction.applyChange(buildMockFileItemChange(item)),
97
+ );
98
+
99
+ const read = await store.read((transaction) =>
100
+ transaction.readItem(item.id, brandSchemas.instant.parse(5000), buildMockItemRules()),
101
+ );
102
+
103
+ return { received: read?.id, expected: item.id };
104
+ },
105
+ ],
106
+ [
107
+ '#read during a running exclusive call sees the last committed state',
108
+ async (store) => {
109
+ const committed = buildMockV1ItemRow({ filedAt: brandSchemas.instant.parse(1000) });
110
+ const pending = buildMockV1ItemRow({ filedAt: brandSchemas.instant.parse(2000) });
111
+ const gate = Promise.withResolvers<void>();
112
+ const written = Promise.withResolvers<void>();
113
+
114
+ await store.exclusive((transaction) =>
115
+ transaction.applyChange(buildMockFileItemChange(committed)),
116
+ );
117
+
118
+ const writing = store.exclusive(async (transaction) => {
119
+ await transaction.applyChange(buildMockFileItemChange(pending));
120
+
121
+ written.resolve();
122
+
123
+ await gate.promise;
124
+ });
125
+
126
+ await written.promise;
127
+
128
+ const seen = await store.read((transaction) =>
129
+ transaction.listItems({}, brandSchemas.instant.parse(5000), buildMockItemRules()),
130
+ );
131
+
132
+ gate.resolve();
133
+
134
+ await writing;
135
+
136
+ return { received: seen.map((item) => item.id), expected: [committed.id] };
137
+ },
138
+ ],
139
+ [
140
+ '#read keeps one snapshot across a commit made during the call',
141
+ async (store) => {
142
+ const committed = buildMockV1ItemRow({ filedAt: brandSchemas.instant.parse(1000) });
143
+ const later = buildMockV1ItemRow({ filedAt: brandSchemas.instant.parse(2000) });
144
+
145
+ await store.exclusive((transaction) =>
146
+ transaction.applyChange(buildMockFileItemChange(committed)),
147
+ );
148
+
149
+ const seen: unknown = await store.read(async (transaction) => {
150
+ const before = await transaction.listItems(
151
+ {},
152
+ brandSchemas.instant.parse(5000),
153
+ buildMockItemRules(),
154
+ );
155
+
156
+ await store.exclusive((writer) => writer.applyChange(buildMockFileItemChange(later)));
157
+
158
+ const after = await transaction.listItems(
159
+ {},
160
+ brandSchemas.instant.parse(5000),
161
+ buildMockItemRules(),
162
+ );
163
+
164
+ return { before: before.map((item) => item.id), after: after.map((item) => item.id) };
165
+ });
166
+
167
+ return { received: seen, expected: { before: [committed.id], after: [committed.id] } };
168
+ },
169
+ ],
170
+ [
171
+ '#read rejects with the error work throws',
172
+ async (store) => {
173
+ const reading = store.read(() => {
174
+ throw new Error('the read failed');
175
+ });
176
+
177
+ const settled = await reading.then(
178
+ () => 'resolved',
179
+ (error: unknown) => error,
180
+ );
181
+
182
+ return { received: settled, expected: new Error('the read failed') };
183
+ },
184
+ ],
185
+ [
186
+ '#read runs a read started inside another read',
187
+ async (store) => {
188
+ const item = buildMockV1ItemRow();
189
+
190
+ await store.exclusive((transaction) =>
191
+ transaction.applyChange(buildMockFileItemChange(item)),
192
+ );
193
+
194
+ const seen = await store.read(() =>
195
+ store.read((transaction) =>
196
+ transaction.readItem(item.id, brandSchemas.instant.parse(5000), buildMockItemRules()),
197
+ ),
198
+ );
199
+
200
+ return { received: seen?.id, expected: item.id };
201
+ },
202
+ ],
203
+ [
204
+ '#applyChange writes the rows and the outbox facts of a change',
205
+ async (store) => {
206
+ const item = buildMockV1ItemRow({ filedAt: brandSchemas.instant.parse(1000) });
207
+ const { keptSession: _keptSession, ...filed } = item;
208
+ const change = buildMockFileItemChange(item);
209
+
210
+ await store.exclusive((transaction) => transaction.applyChange(change));
211
+
212
+ const facts: unknown = await store.read((transaction) => transaction.readUnsentFacts(10));
213
+
214
+ return {
215
+ received: facts,
216
+ expected: [
217
+ {
218
+ seq: 1,
219
+ factId: change.facts[0]?.id,
220
+ type: 'boxxy.item.filed',
221
+ entity: `boxxy:item:${item.id}`,
222
+ actor: 'boxxy:owner:local',
223
+ at: 1000,
224
+ payload: filed,
225
+ sentAt: null,
226
+ parkedAt: null,
227
+ parkedReason: null,
228
+ },
229
+ ],
230
+ };
231
+ },
232
+ ],
233
+ [
234
+ '#applyChange returns applied for a change that it writes',
235
+ async (store) => {
236
+ const change = buildMockFileItemChange(buildMockV1ItemRow());
237
+
238
+ const result = await store.exclusive((transaction) => transaction.applyChange(change));
239
+
240
+ return { received: result, expected: { kind: 'applied' } };
241
+ },
242
+ ],
243
+ ];
@@ -0,0 +1,383 @@
1
+ import { brandSchemas } from '@boxxy/model';
2
+ import { buildMockFactId } from '@boxxy/model/test-utils/build-mock-fact-id';
3
+ import { buildMockItemRules } from '@boxxy/model/test-utils/build-mock-item-rules';
4
+ import { buildMockV1AttemptRow } from '@boxxy/model/test-utils/build-mock-v1-attempt-row';
5
+ import { buildMockV1ItemRow } from '@boxxy/model/test-utils/build-mock-v1-item-row';
6
+ import { buildMockBindChange } from './build-mock-bind-change.ts';
7
+ import { buildMockClaimChange } from './build-mock-claim-change.ts';
8
+ import { buildMockFileItemChange } from './build-mock-file-item-change.ts';
9
+ import { buildMockSettleChange } from './build-mock-settle-change.ts';
10
+ import { buildMockStartChange } from './build-mock-start-change.ts';
11
+ import type { ClaimStoreCase } from './types.ts';
12
+
13
+ export const claimStoreGuardCases: readonly ClaimStoreCase[] = [
14
+ [
15
+ '#applyChange returns stale-generation and keeps the seats for a settle under an old hold gen',
16
+ async (store) => {
17
+ const workers = brandSchemas.limitName.parse('workers');
18
+ const item = buildMockV1ItemRow();
19
+ const attempt = buildMockV1AttemptRow({ item: item.id, state: 'starting', holdGen: 2 });
20
+
21
+ await store.exclusive(async (transaction) => {
22
+ await transaction.syncLimits(
23
+ [{ name: workers, max: 1, counts: {} }],
24
+ brandSchemas.instant.parse(500),
25
+ );
26
+
27
+ await transaction.applyChange(buildMockFileItemChange(item));
28
+ await transaction.applyChange(buildMockClaimChange(attempt, [workers]));
29
+ });
30
+
31
+ const result = await store.exclusive((transaction) =>
32
+ transaction.applyChange(
33
+ buildMockSettleChange({ attempt: { ...attempt, holdGen: 1 }, state: 'failed' }),
34
+ ),
35
+ );
36
+
37
+ const seats = await store.read((transaction) => transaction.listLimitSeats());
38
+
39
+ return {
40
+ received: { result, seats },
41
+ expected: {
42
+ result: { kind: 'stale-generation' },
43
+ seats: [
44
+ {
45
+ limit: 'workers',
46
+ attempt: attempt.id,
47
+ holdGen: 2,
48
+ overCap: false,
49
+ takenAt: attempt.claimedAt,
50
+ releasedAt: null,
51
+ },
52
+ ],
53
+ },
54
+ };
55
+ },
56
+ ],
57
+ [
58
+ '#applyChange releases the lease and the seats of a settle under the live generations',
59
+ async (store) => {
60
+ const workers = brandSchemas.limitName.parse('workers');
61
+ const item = buildMockV1ItemRow();
62
+ const attempt = buildMockV1AttemptRow({ item: item.id, state: 'starting' });
63
+
64
+ await store.exclusive(async (transaction) => {
65
+ await transaction.syncLimits(
66
+ [{ name: workers, max: 1, counts: {} }],
67
+ brandSchemas.instant.parse(500),
68
+ );
69
+
70
+ await transaction.applyChange(buildMockFileItemChange(item));
71
+ await transaction.applyChange(buildMockClaimChange(attempt, [workers]));
72
+ });
73
+
74
+ const result = await store.exclusive((transaction) =>
75
+ transaction.applyChange(buildMockSettleChange({ attempt, state: 'succeeded' })),
76
+ );
77
+
78
+ const snapshot = await store.read((transaction) =>
79
+ transaction.readSnapshot(brandSchemas.instant.parse(5000), buildMockItemRules()),
80
+ );
81
+
82
+ return {
83
+ received: {
84
+ result,
85
+ limits: snapshot.limits,
86
+ derived: snapshot.items[0]?.derived,
87
+ },
88
+ expected: {
89
+ result: { kind: 'applied' },
90
+ limits: [{ name: 'workers', max: 1, counts: {}, held: 0 }],
91
+ derived: { state: 'waiting', reason: 'no pull request linked' },
92
+ },
93
+ };
94
+ },
95
+ ],
96
+ [
97
+ '#applyChange returns stale-generation and writes no fact under a stale lease generation',
98
+ async (store) => {
99
+ const item = buildMockV1ItemRow();
100
+ const attempt = buildMockV1AttemptRow({ item: item.id, state: 'starting' });
101
+
102
+ await store.exclusive(async (transaction) => {
103
+ await transaction.applyChange(buildMockFileItemChange(item));
104
+ await transaction.applyChange(buildMockClaimChange(attempt));
105
+ });
106
+
107
+ const result = await store.exclusive((transaction) =>
108
+ transaction.applyChange(buildMockStartChange({ ...attempt, leaseGen: 2 })),
109
+ );
110
+
111
+ const state: unknown = await store.read(async (transaction) => {
112
+ const read = await transaction.readAttempt(attempt.id);
113
+ const facts = await transaction.readUnsentFacts(10);
114
+
115
+ return { attempt: read?.state, facts: facts.length };
116
+ });
117
+
118
+ return {
119
+ received: { result, state },
120
+ expected: {
121
+ result: { kind: 'stale-generation' },
122
+ state: { attempt: 'starting', facts: 2 },
123
+ },
124
+ };
125
+ },
126
+ ],
127
+ [
128
+ '#applyChange returns stale-generation under a released lease',
129
+ async (store) => {
130
+ const item = buildMockV1ItemRow();
131
+ const attempt = buildMockV1AttemptRow({ item: item.id, state: 'starting' });
132
+
133
+ await store.exclusive(async (transaction) => {
134
+ await transaction.applyChange(buildMockFileItemChange(item));
135
+ await transaction.applyChange(buildMockClaimChange(attempt));
136
+ await transaction.applyChange(buildMockSettleChange({ attempt, state: 'failed' }));
137
+ });
138
+
139
+ const result = await store.exclusive((transaction) =>
140
+ transaction.applyChange(buildMockStartChange(attempt)),
141
+ );
142
+
143
+ return { received: result, expected: { kind: 'stale-generation' } };
144
+ },
145
+ ],
146
+ [
147
+ '#applyChange applies a change under the live lease generation',
148
+ async (store) => {
149
+ const item = buildMockV1ItemRow();
150
+ const attempt = buildMockV1AttemptRow({ item: item.id, state: 'starting' });
151
+
152
+ await store.exclusive(async (transaction) => {
153
+ await transaction.applyChange(buildMockFileItemChange(item));
154
+ await transaction.applyChange(buildMockClaimChange(attempt));
155
+ await transaction.applyChange(buildMockStartChange(attempt));
156
+ });
157
+
158
+ const read = await store.read((transaction) => transaction.readAttempt(attempt.id));
159
+
160
+ return { received: read?.state, expected: 'running' };
161
+ },
162
+ ],
163
+ [
164
+ '#applyChange returns guard-mismatch and writes nothing when an expect fails',
165
+ async (store) => {
166
+ const item = buildMockV1ItemRow();
167
+
168
+ await store.exclusive((transaction) =>
169
+ transaction.applyChange(buildMockFileItemChange(item)),
170
+ );
171
+
172
+ const result = await store.exclusive((transaction) =>
173
+ transaction.applyChange({
174
+ at: brandSchemas.instant.parse(3000),
175
+ writes: [
176
+ {
177
+ op: 'update',
178
+ table: 'items',
179
+ key: { id: item.id },
180
+ expect: { closedAt: null },
181
+ set: { title: 'Mend the gate' },
182
+ },
183
+ {
184
+ op: 'update',
185
+ table: 'items',
186
+ key: { id: item.id },
187
+ expect: { title: 'Paint the fence' },
188
+ set: { closedAt: brandSchemas.instant.parse(3000), closeReason: 'done by hand' },
189
+ },
190
+ ],
191
+ facts: [
192
+ {
193
+ id: buildMockFactId(),
194
+ type: 'boxxy.item.closed',
195
+ entity: `boxxy:item:${item.id}`,
196
+ actor: 'boxxy:owner:local',
197
+ payload: { reason: 'done by hand', byAttempt: null, ended: [] },
198
+ },
199
+ ],
200
+ }),
201
+ );
202
+
203
+ const state: unknown = await store.read(async (transaction) => {
204
+ const read = await transaction.readItem(
205
+ item.id,
206
+ brandSchemas.instant.parse(5000),
207
+ buildMockItemRules(),
208
+ );
209
+
210
+ const facts = await transaction.readUnsentFacts(10);
211
+
212
+ return { item: read, facts: facts.length };
213
+ });
214
+
215
+ return {
216
+ received: { result, state },
217
+ expected: {
218
+ result: { kind: 'guard-mismatch', table: 'items', key: { id: item.id } },
219
+ state: {
220
+ item: {
221
+ ...item,
222
+ derived: { state: 'ready' },
223
+ position: { kind: 'phase', phase: 'build', round: 1 },
224
+ },
225
+ facts: 1,
226
+ },
227
+ },
228
+ };
229
+ },
230
+ ],
231
+ [
232
+ '#applyChange returns guard-mismatch for an update that finds no row',
233
+ async (store) => {
234
+ const item = buildMockV1ItemRow();
235
+
236
+ const result = await store.exclusive((transaction) =>
237
+ transaction.applyChange({
238
+ at: brandSchemas.instant.parse(3000),
239
+ writes: [
240
+ {
241
+ op: 'update',
242
+ table: 'items',
243
+ key: { id: item.id },
244
+ expect: {},
245
+ set: { title: 'Mend the gate' },
246
+ },
247
+ ],
248
+ facts: [
249
+ {
250
+ id: buildMockFactId(),
251
+ type: 'boxxy.item.edited',
252
+ entity: `boxxy:item:${item.id}`,
253
+ actor: 'boxxy:owner:local',
254
+ payload: { title: 'Mend the gate', byAttempt: null },
255
+ },
256
+ ],
257
+ }),
258
+ );
259
+
260
+ return {
261
+ received: result,
262
+ expected: {
263
+ kind: 'guard-mismatch',
264
+ table: 'items',
265
+ key: { id: item.id },
266
+ },
267
+ };
268
+ },
269
+ ],
270
+ [
271
+ '#applyChange matches a JSON column guard whose keys come in another order',
272
+ async (store) => {
273
+ const criterion = {
274
+ id: brandSchemas.criterionId.parse('a1'),
275
+ text: 'The gate swings shut',
276
+ evidence: [],
277
+ };
278
+
279
+ const item = buildMockV1ItemRow({ acceptance: [criterion] });
280
+
281
+ await store.exclusive((transaction) =>
282
+ transaction.applyChange(buildMockFileItemChange(item)),
283
+ );
284
+
285
+ const result = await store.exclusive((transaction) =>
286
+ transaction.applyChange({
287
+ at: brandSchemas.instant.parse(3000),
288
+ writes: [
289
+ {
290
+ op: 'update',
291
+ table: 'items',
292
+ key: { id: item.id },
293
+ expect: {
294
+ acceptance: [
295
+ { evidence: criterion.evidence, text: criterion.text, id: criterion.id },
296
+ ],
297
+ },
298
+ set: { title: 'Mend the gate' },
299
+ },
300
+ ],
301
+ facts: [
302
+ {
303
+ id: buildMockFactId(),
304
+ type: 'boxxy.item.edited',
305
+ entity: `boxxy:item:${item.id}`,
306
+ actor: 'boxxy:owner:local',
307
+ payload: { title: 'Mend the gate', byAttempt: null },
308
+ },
309
+ ],
310
+ }),
311
+ );
312
+
313
+ return { received: result, expected: { kind: 'applied' } };
314
+ },
315
+ ],
316
+ [
317
+ '#findAttemptByAgentSession returns the attempt that holds an agent session id',
318
+ async (store) => {
319
+ const item = buildMockV1ItemRow();
320
+ const attempt = buildMockV1AttemptRow({ item: item.id, state: 'starting' });
321
+ const agentSession = brandSchemas.agentSessionId.parse('agent-session-1');
322
+
323
+ await store.exclusive(async (transaction) => {
324
+ await transaction.applyChange(buildMockFileItemChange(item));
325
+ await transaction.applyChange(buildMockClaimChange(attempt));
326
+ await transaction.applyChange(buildMockBindChange({ attempt, agentSession }));
327
+ });
328
+
329
+ const found = await store.read((transaction) =>
330
+ transaction.findAttemptByAgentSession(agentSession),
331
+ );
332
+
333
+ return { received: found, expected: attempt.id };
334
+ },
335
+ ],
336
+ [
337
+ '#findAttemptByAgentSession returns null for an unknown agent session id',
338
+ async (store) => {
339
+ const found = await store.read((transaction) =>
340
+ transaction.findAttemptByAgentSession(brandSchemas.agentSessionId.parse('agent-session-9')),
341
+ );
342
+
343
+ return { received: found, expected: null };
344
+ },
345
+ ],
346
+ [
347
+ '#findAttemptByAgentSession returns the later of two attempts bound to one agent session',
348
+ async (store) => {
349
+ const item = buildMockV1ItemRow();
350
+ const agentSession = brandSchemas.agentSessionId.parse('agent-session-1');
351
+
352
+ const earlier = buildMockV1AttemptRow({
353
+ item: item.id,
354
+ state: 'starting',
355
+ claimedAt: brandSchemas.instant.parse(1000),
356
+ });
357
+
358
+ const later = buildMockV1AttemptRow({
359
+ item: item.id,
360
+ state: 'starting',
361
+ number: 2,
362
+ leaseGen: 2,
363
+ atcSession: earlier.atcSession,
364
+ claimedAt: brandSchemas.instant.parse(4000),
365
+ });
366
+
367
+ await store.exclusive(async (transaction) => {
368
+ await transaction.applyChange(buildMockFileItemChange(item));
369
+ await transaction.applyChange(buildMockClaimChange(earlier));
370
+ await transaction.applyChange(buildMockBindChange({ attempt: earlier, agentSession }));
371
+ await transaction.applyChange(buildMockSettleChange({ attempt: earlier, state: 'failed' }));
372
+ await transaction.applyChange(buildMockClaimChange(later));
373
+ await transaction.applyChange(buildMockBindChange({ attempt: later, agentSession }));
374
+ });
375
+
376
+ const found = await store.read((transaction) =>
377
+ transaction.findAttemptByAgentSession(agentSession),
378
+ );
379
+
380
+ return { received: found, expected: later.id };
381
+ },
382
+ ],
383
+ ];