@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,471 @@
1
+ import { expect } from 'bun:test';
2
+ import { brandSchemas } from '@boxxy/model';
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 { buildMockV1LeverRow } from '@boxxy/model/test-utils/build-mock-v1-lever-row';
7
+ import { buildMockClaimChange } from './build-mock-claim-change.ts';
8
+ import { buildMockFileItemChange } from './build-mock-file-item-change.ts';
9
+ import { buildMockLeverChange } from './build-mock-lever-change.ts';
10
+ import { buildMockNoteChange } from './build-mock-note-change.ts';
11
+ import { buildMockPullRequestChange } from './build-mock-pull-request-change.ts';
12
+ import { buildMockRelationChange } from './build-mock-relation-change.ts';
13
+ import { buildMockSettleChange } from './build-mock-settle-change.ts';
14
+ import type { ClaimStoreCase } from './types.ts';
15
+
16
+ export const claimStoreItemReaderCases: readonly ClaimStoreCase[] = [
17
+ [
18
+ '#readSnapshot holds the open items, the store version and the limits',
19
+ async (store) => {
20
+ const open = buildMockV1ItemRow();
21
+ const archived = buildMockV1ItemRow({ closedAt: brandSchemas.instant.parse(1500) });
22
+
23
+ await store.exclusive(async (transaction) => {
24
+ await transaction.syncLimits(
25
+ [{ name: brandSchemas.limitName.parse('workers'), max: 2, counts: {} }],
26
+ brandSchemas.instant.parse(500),
27
+ );
28
+
29
+ await transaction.applyChange(buildMockFileItemChange(open));
30
+ await transaction.applyChange(buildMockFileItemChange(archived));
31
+ });
32
+
33
+ const snapshot: unknown = await store.read((transaction) =>
34
+ transaction.readSnapshot(brandSchemas.instant.parse(5000), buildMockItemRules()),
35
+ );
36
+
37
+ return {
38
+ received: snapshot,
39
+ expected: {
40
+ storeVersion: 2,
41
+ items: [
42
+ {
43
+ ...open,
44
+ derived: { state: 'ready' },
45
+ position: { kind: 'phase', phase: 'build', round: 1 },
46
+ },
47
+ ],
48
+ relations: [],
49
+ levers: [],
50
+ limits: [{ name: 'workers', max: 2, counts: {}, held: 0 }],
51
+ wakes: [],
52
+ },
53
+ };
54
+ },
55
+ ],
56
+ [
57
+ '#readSnapshot lists the sleeping attempts asked to wake, oldest request first',
58
+ async (store) => {
59
+ const later = buildMockV1AttemptRow({
60
+ state: 'sleeping',
61
+ wakeRequestedAt: brandSchemas.instant.parse(4000),
62
+ fields: { designMode: ['attended'] },
63
+ });
64
+
65
+ const earlier = buildMockV1AttemptRow({
66
+ state: 'sleeping',
67
+ wakeRequestedAt: brandSchemas.instant.parse(3000),
68
+ });
69
+
70
+ const unasked = buildMockV1AttemptRow({ state: 'sleeping' });
71
+
72
+ await store.exclusive(async (transaction) => {
73
+ for (const attempt of [later, earlier, unasked]) {
74
+ await transaction.applyChange(
75
+ buildMockFileItemChange(buildMockV1ItemRow({ id: attempt.item })),
76
+ );
77
+
78
+ await transaction.applyChange(buildMockClaimChange(attempt));
79
+ }
80
+ });
81
+
82
+ const snapshot = await store.read((transaction) =>
83
+ transaction.readSnapshot(brandSchemas.instant.parse(5000), buildMockItemRules()),
84
+ );
85
+
86
+ return {
87
+ received: snapshot.wakes,
88
+ expected: [
89
+ {
90
+ attempt: earlier.id,
91
+ item: earlier.item,
92
+ phase: 'build',
93
+ pool: 'main',
94
+ fields: {},
95
+ requestedAt: 3000,
96
+ },
97
+ {
98
+ attempt: later.id,
99
+ item: later.item,
100
+ phase: 'build',
101
+ pool: 'main',
102
+ fields: { designMode: ['attended'] },
103
+ requestedAt: 4000,
104
+ },
105
+ ],
106
+ };
107
+ },
108
+ ],
109
+ [
110
+ '#readSnapshot drops a done item',
111
+ async (store) => {
112
+ const item = buildMockV1ItemRow();
113
+ const attempt = buildMockV1AttemptRow({ item: item.id, state: 'starting' });
114
+
115
+ await store.exclusive(async (transaction) => {
116
+ await transaction.applyChange(buildMockFileItemChange(item));
117
+ await transaction.applyChange(buildMockClaimChange(attempt));
118
+ await transaction.applyChange(buildMockSettleChange({ attempt, state: 'succeeded' }));
119
+ await transaction.applyChange(buildMockPullRequestChange({ attempt, state: 'merged' }));
120
+ });
121
+
122
+ const snapshot = await store.read((transaction) =>
123
+ transaction.readSnapshot(brandSchemas.instant.parse(5000), buildMockItemRules()),
124
+ );
125
+
126
+ return { received: snapshot.items, expected: [] };
127
+ },
128
+ ],
129
+ [
130
+ '#readItem returns null for an unknown item',
131
+ async (store) => {
132
+ const read = await store.read((transaction) =>
133
+ transaction.readItem(
134
+ buildMockV1ItemRow().id,
135
+ brandSchemas.instant.parse(5000),
136
+ buildMockItemRules(),
137
+ ),
138
+ );
139
+
140
+ return { received: read, expected: null };
141
+ },
142
+ ],
143
+ [
144
+ '#readItem derives blocked for the target of a live relation from a ready item',
145
+ async (store) => {
146
+ const source = buildMockV1ItemRow();
147
+ const target = buildMockV1ItemRow();
148
+
149
+ await store.exclusive(async (transaction) => {
150
+ await transaction.applyChange(buildMockFileItemChange(source));
151
+ await transaction.applyChange(buildMockFileItemChange(target));
152
+
153
+ await transaction.applyChange(
154
+ buildMockRelationChange({ source: source.id, target: target.id }),
155
+ );
156
+ });
157
+
158
+ const read = await store.read((transaction) =>
159
+ transaction.readItem(target.id, brandSchemas.instant.parse(5000), buildMockItemRules()),
160
+ );
161
+
162
+ return { received: read?.derived, expected: { state: 'blocked' } };
163
+ },
164
+ ],
165
+ [
166
+ '#readItem derives ready for the target of a relation from an archived item',
167
+ async (store) => {
168
+ const source = buildMockV1ItemRow({ closedAt: brandSchemas.instant.parse(1500) });
169
+ const target = buildMockV1ItemRow();
170
+
171
+ await store.exclusive(async (transaction) => {
172
+ await transaction.applyChange(buildMockFileItemChange(source));
173
+ await transaction.applyChange(buildMockFileItemChange(target));
174
+
175
+ await transaction.applyChange(
176
+ buildMockRelationChange({ source: source.id, target: target.id }),
177
+ );
178
+ });
179
+
180
+ const read = await store.read((transaction) =>
181
+ transaction.readItem(target.id, brandSchemas.instant.parse(5000), buildMockItemRules()),
182
+ );
183
+
184
+ return { received: read?.derived, expected: { state: 'ready' } };
185
+ },
186
+ ],
187
+ [
188
+ '#readItem holds an item whose phase lost its attempts in a row',
189
+ async (store) => {
190
+ const item = buildMockV1ItemRow();
191
+ const first = buildMockV1AttemptRow({ item: item.id, state: 'starting' });
192
+
193
+ const second = buildMockV1AttemptRow({
194
+ item: item.id,
195
+ state: 'starting',
196
+ number: 2,
197
+ leaseGen: 2,
198
+ });
199
+
200
+ await store.exclusive(async (transaction) => {
201
+ await transaction.applyChange(buildMockFileItemChange(item));
202
+
203
+ for (const attempt of [first, second]) {
204
+ await transaction.applyChange(buildMockClaimChange(attempt));
205
+ await transaction.applyChange(buildMockSettleChange({ attempt, state: 'failed' }));
206
+ }
207
+ });
208
+
209
+ const read = await store.read((transaction) =>
210
+ transaction.readItem(item.id, brandSchemas.instant.parse(5000), buildMockItemRules()),
211
+ );
212
+
213
+ return {
214
+ received: { derived: read?.derived, position: read?.position.kind },
215
+ expected: { derived: { state: 'waiting', reason: 'attempts used up' }, position: 'hold' },
216
+ };
217
+ },
218
+ ],
219
+ [
220
+ '#readItem returns a held item to its phase after a retry lever',
221
+ async (store) => {
222
+ const item = buildMockV1ItemRow();
223
+ const first = buildMockV1AttemptRow({ item: item.id, state: 'starting' });
224
+
225
+ const second = buildMockV1AttemptRow({
226
+ item: item.id,
227
+ state: 'starting',
228
+ number: 2,
229
+ leaseGen: 2,
230
+ });
231
+
232
+ const retry = buildMockV1LeverRow({
233
+ item: item.id,
234
+ kind: 'item.retry',
235
+ at: brandSchemas.instant.parse(4000),
236
+ });
237
+
238
+ await store.exclusive(async (transaction) => {
239
+ await transaction.applyChange(buildMockFileItemChange(item));
240
+
241
+ for (const attempt of [first, second]) {
242
+ await transaction.applyChange(buildMockClaimChange(attempt));
243
+ await transaction.applyChange(buildMockSettleChange({ attempt, state: 'failed' }));
244
+ }
245
+
246
+ await transaction.applyChange(buildMockLeverChange(retry));
247
+ });
248
+
249
+ const read = await store.read((transaction) =>
250
+ transaction.readItem(item.id, brandSchemas.instant.parse(5000), buildMockItemRules()),
251
+ );
252
+
253
+ return {
254
+ received: { derived: read?.derived, position: read?.position },
255
+ expected: {
256
+ derived: { state: 'ready' },
257
+ position: { kind: 'phase', phase: 'build', round: 1 },
258
+ },
259
+ };
260
+ },
261
+ ],
262
+ [
263
+ '#readItem ends an item whose last phase of the pipeline succeeded',
264
+ async (store) => {
265
+ const item = buildMockV1ItemRow();
266
+ const attempt = buildMockV1AttemptRow({ item: item.id, state: 'starting' });
267
+
268
+ await store.exclusive(async (transaction) => {
269
+ await transaction.applyChange(buildMockFileItemChange(item));
270
+ await transaction.applyChange(buildMockClaimChange(attempt));
271
+ await transaction.applyChange(buildMockSettleChange({ attempt, state: 'succeeded' }));
272
+ });
273
+
274
+ const read = await store.read((transaction) =>
275
+ transaction.readItem(item.id, brandSchemas.instant.parse(5000), buildMockItemRules()),
276
+ );
277
+
278
+ return {
279
+ received: { derived: read?.derived, position: read?.position },
280
+ expected: {
281
+ derived: { state: 'waiting', reason: 'no pull request linked' },
282
+ position: { kind: 'end', reason: 'pipeline-done' },
283
+ },
284
+ };
285
+ },
286
+ ],
287
+ [
288
+ '#listItems keeps the items of one state and repo, oldest first',
289
+ async (store) => {
290
+ const repo = brandSchemas.repoName.parse('acme/lamps');
291
+ const older = buildMockV1ItemRow({ repo, filedAt: brandSchemas.instant.parse(1000) });
292
+ const newer = buildMockV1ItemRow({ repo, filedAt: brandSchemas.instant.parse(2000) });
293
+ const idea = buildMockV1ItemRow({ repo, acceptance: [] });
294
+ const elsewhere = buildMockV1ItemRow({ repo: brandSchemas.repoName.parse('acme/fans') });
295
+ const build = brandSchemas.phaseName.parse('build');
296
+
297
+ await store.exclusive(async (transaction) => {
298
+ await transaction.applyChange(buildMockFileItemChange(newer));
299
+ await transaction.applyChange(buildMockFileItemChange(older));
300
+ await transaction.applyChange(buildMockFileItemChange(idea));
301
+ await transaction.applyChange(buildMockFileItemChange(elsewhere));
302
+ });
303
+
304
+ const listed = await store.read((transaction) =>
305
+ transaction.listItems(
306
+ { state: 'ready', repo },
307
+ brandSchemas.instant.parse(5000),
308
+ buildMockItemRules({ phases: new Map([[build, { needsAcceptance: true }]]) }),
309
+ ),
310
+ );
311
+
312
+ return { received: listed.map((item) => item.id), expected: [older.id, newer.id] };
313
+ },
314
+ ],
315
+ [
316
+ '#listOpenIssues returns the open core.issue items of one repo',
317
+ async (store) => {
318
+ const repo = brandSchemas.repoName.parse('acme/lamps');
319
+ const open = buildMockV1ItemRow({ repo });
320
+ const closed = buildMockV1ItemRow({ repo, closedAt: brandSchemas.instant.parse(1500) });
321
+ const elsewhere = buildMockV1ItemRow({ repo: brandSchemas.repoName.parse('acme/fans') });
322
+ const task = buildMockV1ItemRow({ repo, kind: brandSchemas.itemKindName.parse('core.task') });
323
+
324
+ await store.exclusive(async (transaction) => {
325
+ await transaction.applyChange(buildMockFileItemChange(open));
326
+ await transaction.applyChange(buildMockFileItemChange(closed));
327
+ await transaction.applyChange(buildMockFileItemChange(elsewhere));
328
+ await transaction.applyChange(buildMockFileItemChange(task));
329
+ });
330
+
331
+ const listed: unknown = await store.read((transaction) =>
332
+ transaction.listOpenIssues(repo, brandSchemas.instant.parse(5000), buildMockItemRules()),
333
+ );
334
+
335
+ return {
336
+ received: listed,
337
+ expected: [
338
+ {
339
+ ...open,
340
+ derived: { state: 'ready' },
341
+ position: { kind: 'phase', phase: 'build', round: 1 },
342
+ },
343
+ ],
344
+ };
345
+ },
346
+ ],
347
+ [
348
+ '#readItemDetail returns the item with its attempts, relations, evidence, pull requests and notes',
349
+ async (store) => {
350
+ const item = buildMockV1ItemRow();
351
+ const blocker = buildMockV1ItemRow();
352
+ const attempt = buildMockV1AttemptRow({ item: item.id, state: 'starting' });
353
+
354
+ const note = {
355
+ item: item.id,
356
+ seq: 1,
357
+ text: 'The lamp hums when dimmed.',
358
+ by: 'boxxy:owner:local',
359
+ at: brandSchemas.instant.parse(4500),
360
+ } as const;
361
+
362
+ await store.exclusive(async (transaction) => {
363
+ await transaction.applyChange(buildMockFileItemChange(item));
364
+ await transaction.applyChange(buildMockFileItemChange(blocker));
365
+ await transaction.applyChange(buildMockClaimChange(attempt));
366
+ await transaction.applyChange(buildMockSettleChange({ attempt, state: 'succeeded' }));
367
+ await transaction.applyChange(buildMockPullRequestChange({ attempt, state: 'open' }));
368
+
369
+ await transaction.applyChange(
370
+ buildMockRelationChange({ source: blocker.id, target: item.id }),
371
+ );
372
+
373
+ await transaction.applyChange(buildMockNoteChange(note));
374
+ });
375
+
376
+ const detail: unknown = await store.read((transaction) =>
377
+ transaction.readItemDetail(item.id, brandSchemas.instant.parse(5000), buildMockItemRules()),
378
+ );
379
+
380
+ return {
381
+ received: detail,
382
+ expected: {
383
+ item: {
384
+ ...item,
385
+ derived: { state: 'waiting', reason: 'pull request open' },
386
+ position: { kind: 'end', reason: 'pipeline-done' },
387
+ levers: [],
388
+ },
389
+ attempts: [expect.objectContaining({ id: attempt.id, state: 'succeeded' })],
390
+ relations: [expect.objectContaining({ source: blocker.id, target: item.id })],
391
+ evidence: [expect.objectContaining({ attempt: attempt.id, kind: 'pull_request' })],
392
+ pullRequests: [expect.objectContaining({ attempt: attempt.id, state: 'open' })],
393
+ notes: [note],
394
+ },
395
+ };
396
+ },
397
+ ],
398
+ [
399
+ '#readItemDetail returns null for an unknown item',
400
+ async (store) => {
401
+ const detail = await store.read((transaction) =>
402
+ transaction.readItemDetail(
403
+ buildMockV1ItemRow().id,
404
+ brandSchemas.instant.parse(5000),
405
+ buildMockItemRules(),
406
+ ),
407
+ );
408
+
409
+ return { received: detail, expected: null };
410
+ },
411
+ ],
412
+ [
413
+ '#listItemNotes returns the notes of one item by seq',
414
+ async (store) => {
415
+ const item = buildMockV1ItemRow();
416
+ const other = buildMockV1ItemRow();
417
+
418
+ const note = (id: typeof item.id, seq: number, text: string) =>
419
+ buildMockNoteChange({
420
+ item: id,
421
+ seq,
422
+ text,
423
+ by: 'boxxy:owner:local',
424
+ at: brandSchemas.instant.parse(4000 + seq),
425
+ });
426
+
427
+ await store.exclusive(async (transaction) => {
428
+ await transaction.applyChange(buildMockFileItemChange(item));
429
+ await transaction.applyChange(buildMockFileItemChange(other));
430
+ await transaction.applyChange(note(item.id, 2, 'The switch sticks.'));
431
+ await transaction.applyChange(note(other.id, 1, 'The cord frays.'));
432
+ await transaction.applyChange(note(item.id, 1, 'The bulb flickers.'));
433
+ });
434
+
435
+ const notes = await store.read((transaction) => transaction.listItemNotes(item.id));
436
+
437
+ return {
438
+ received: notes.map((row) => row.text),
439
+ expected: ['The bulb flickers.', 'The switch sticks.'],
440
+ };
441
+ },
442
+ ],
443
+ [
444
+ '#applyChange throws on a second note with the same seq on one item',
445
+ async (store) => {
446
+ const item = buildMockV1ItemRow();
447
+
448
+ const note = {
449
+ item: item.id,
450
+ seq: 1,
451
+ text: 'The bulb flickers.',
452
+ by: 'boxxy:owner:local',
453
+ at: brandSchemas.instant.parse(4500),
454
+ } as const;
455
+
456
+ await store.exclusive(async (transaction) => {
457
+ await transaction.applyChange(buildMockFileItemChange(item));
458
+ await transaction.applyChange(buildMockNoteChange(note));
459
+ });
460
+
461
+ const settled = await store
462
+ .exclusive((transaction) => transaction.applyChange(buildMockNoteChange(note)))
463
+ .then(
464
+ () => 'applied',
465
+ () => 'thrown',
466
+ );
467
+
468
+ return { received: settled, expected: 'thrown' };
469
+ },
470
+ ],
471
+ ];