@boxxy/dispatch 0.1.1 → 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 (190) hide show
  1. package/dist/attempts/attempt-machine.d.ts +804 -0
  2. package/dist/attempts/attempt-moves.d.ts +2 -0
  3. package/dist/attempts/bind-attempt-session.d.ts +14 -0
  4. package/dist/attempts/build-attempt-change.d.ts +73 -0
  5. package/dist/attempts/build-attempt-fact.d.ts +3 -0
  6. package/dist/attempts/end-item-attempts.d.ts +18 -0
  7. package/dist/attempts/is-same-guard.d.ts +5 -0
  8. package/dist/attempts/pick-call-attempt.d.ts +10 -0
  9. package/dist/attempts/stop-stray-session.d.ts +11 -0
  10. package/dist/attempts/types.d.ts +142 -0
  11. package/dist/attempts/write-attempt-move.d.ts +7 -0
  12. package/dist/attempts/write-session-bind.d.ts +3 -0
  13. package/dist/claims/build-snapshot-hash.d.ts +2 -0
  14. package/dist/claims/claim-next.d.ts +6 -0
  15. package/dist/claims/pick-kept-session.d.ts +9 -0
  16. package/dist/claims/request-wake.d.ts +3 -0
  17. package/dist/claims/resume-claim.d.ts +3 -0
  18. package/dist/claims/start-claim.d.ts +3 -0
  19. package/dist/claims/types.d.ts +44 -0
  20. package/dist/closeouts/build-settle-plans.d.ts +17 -0
  21. package/dist/closeouts/close-out-attempt.d.ts +3 -0
  22. package/dist/closeouts/effect-handlers.d.ts +2 -0
  23. package/dist/closeouts/pick-settlement.d.ts +14 -0
  24. package/dist/closeouts/settle-attempt.d.ts +3 -0
  25. package/dist/closeouts/types.d.ts +50 -0
  26. package/dist/index.d.ts +38 -0
  27. package/dist/notices/constants.d.ts +4 -0
  28. package/dist/notices/create-null-notifier.d.ts +2 -0
  29. package/dist/notices/send-notices.d.ts +2 -0
  30. package/dist/notices/types.d.ts +11 -0
  31. package/dist/outbox/build-backoff-ms.d.ts +2 -0
  32. package/dist/outbox/build-initial-sender-state.d.ts +2 -0
  33. package/dist/outbox/build-outbox-status.d.ts +15 -0
  34. package/dist/outbox/constants.d.ts +2 -0
  35. package/dist/outbox/measure-wire-bytes.d.ts +2 -0
  36. package/dist/outbox/pick-sender-move.d.ts +10 -0
  37. package/dist/outbox/run-sender-once.d.ts +2 -0
  38. package/dist/outbox/take-batch.d.ts +3 -0
  39. package/dist/outbox/types.d.ts +61 -0
  40. package/dist/pull-requests/build-link-change.d.ts +11 -0
  41. package/dist/pull-requests/build-state-fact.d.ts +8 -0
  42. package/dist/pull-requests/collect-attempt-trailers.d.ts +2 -0
  43. package/dist/pull-requests/collect-seen-state.d.ts +7 -0
  44. package/dist/pull-requests/constants.d.ts +3 -0
  45. package/dist/pull-requests/link-pull-request.d.ts +2 -0
  46. package/dist/pull-requests/parse-pull-request-url.d.ts +2 -0
  47. package/dist/pull-requests/types.d.ts +37 -0
  48. package/dist/pull-requests/watch-pull-requests.d.ts +2 -0
  49. package/dist/readings/act-on-reading.d.ts +3 -0
  50. package/dist/readings/find-waiting-since.d.ts +9 -0
  51. package/dist/readings/pick-reading-action.d.ts +2 -0
  52. package/dist/readings/types.d.ts +79 -0
  53. package/dist/sessions/build-keep-writes.d.ts +9 -0
  54. package/dist/sessions/build-kept-clear.d.ts +2 -0
  55. package/dist/sessions/forget-sessions.d.ts +2 -0
  56. package/dist/sessions/is-kept-due.d.ts +2 -0
  57. package/dist/sessions/resume-kept-session.d.ts +6 -0
  58. package/dist/sessions/types.d.ts +19 -0
  59. package/dist/test-utils/apply-memory-change.d.ts +3 -0
  60. package/dist/test-utils/build-memory-reader.d.ts +3 -0
  61. package/dist/test-utils/build-mock-claim-change.d.ts +2 -2
  62. package/dist/test-utils/build-mock-new-id.d.ts +2 -0
  63. package/dist/test-utils/build-mock-note-change.d.ts +2 -0
  64. package/dist/test-utils/build-mock-pull-request-change.d.ts +1 -0
  65. package/dist/test-utils/{build-mock-link-change.d.ts → build-mock-relation-change.d.ts} +2 -2
  66. package/dist/test-utils/build-mock-running-reading.d.ts +6 -0
  67. package/dist/test-utils/build-mock-start-change.d.ts +2 -2
  68. package/dist/test-utils/claim-store-close-cases.d.ts +2 -0
  69. package/dist/test-utils/claim-store-copy-cases.d.ts +2 -0
  70. package/dist/test-utils/claim-store-outbox-cases.d.ts +2 -0
  71. package/dist/test-utils/claim-store-race-cases.d.ts +2 -0
  72. package/dist/test-utils/claim-store-seat-cases.d.ts +2 -0
  73. package/dist/test-utils/claim-store-session-cases.d.ts +2 -0
  74. package/dist/test-utils/collect-memory-limit-fills.d.ts +3 -0
  75. package/dist/test-utils/compare-text.d.ts +1 -0
  76. package/dist/test-utils/create-fake-backoff.d.ts +2 -0
  77. package/dist/test-utils/create-fake-clock.d.ts +5 -0
  78. package/dist/test-utils/create-fake-fact-sink.d.ts +8 -0
  79. package/dist/test-utils/create-fake-forge.d.ts +17 -0
  80. package/dist/test-utils/create-fake-worker.d.ts +42 -0
  81. package/dist/test-utils/create-memory-claim-store.d.ts +2 -2
  82. package/dist/test-utils/create-test-dispatch.d.ts +46 -0
  83. package/dist/test-utils/forge-contract.d.ts +15 -0
  84. package/dist/test-utils/is-same-held-seat.d.ts +2 -0
  85. package/dist/test-utils/types.d.ts +12 -2
  86. package/dist/test-utils/worker-contract.d.ts +14 -0
  87. package/dist/timeouts.d.ts +2 -0
  88. package/dist/types.d.ts +13 -0
  89. package/package.json +10 -3
  90. package/src/attempts/attempt-machine.ts +147 -0
  91. package/src/attempts/attempt-moves.ts +218 -0
  92. package/src/attempts/bind-attempt-session.ts +61 -0
  93. package/src/attempts/build-attempt-change.ts +540 -0
  94. package/src/attempts/build-attempt-fact.ts +135 -0
  95. package/src/attempts/end-item-attempts.ts +95 -0
  96. package/src/attempts/is-same-guard.ts +22 -0
  97. package/src/attempts/pick-call-attempt.ts +32 -0
  98. package/src/attempts/stop-stray-session.ts +48 -0
  99. package/src/attempts/types.ts +231 -0
  100. package/src/attempts/write-attempt-move.ts +41 -0
  101. package/src/attempts/write-session-bind.ts +47 -0
  102. package/src/claims/build-snapshot-hash.ts +9 -0
  103. package/src/claims/claim-next.ts +306 -0
  104. package/src/claims/pick-kept-session.ts +41 -0
  105. package/src/claims/request-wake.ts +17 -0
  106. package/src/claims/resume-claim.ts +56 -0
  107. package/src/claims/start-claim.ts +99 -0
  108. package/src/claims/types.ts +67 -0
  109. package/src/closeouts/build-settle-plans.ts +173 -0
  110. package/src/closeouts/close-out-attempt.ts +156 -0
  111. package/src/closeouts/effect-handlers.ts +232 -0
  112. package/src/closeouts/pick-settlement.ts +80 -0
  113. package/src/closeouts/settle-attempt.ts +161 -0
  114. package/src/closeouts/types.ts +87 -0
  115. package/src/index.ts +61 -0
  116. package/src/notices/constants.ts +7 -0
  117. package/src/notices/create-null-notifier.ts +6 -0
  118. package/src/notices/send-notices.ts +41 -0
  119. package/src/notices/types.ts +14 -0
  120. package/src/outbox/build-backoff-ms.ts +14 -0
  121. package/src/outbox/build-initial-sender-state.ts +14 -0
  122. package/src/outbox/build-outbox-status.ts +43 -0
  123. package/src/outbox/constants.ts +15 -0
  124. package/src/outbox/measure-wire-bytes.ts +31 -0
  125. package/src/outbox/pick-sender-move.ts +37 -0
  126. package/src/outbox/run-sender-once.ts +174 -0
  127. package/src/outbox/take-batch.ts +35 -0
  128. package/src/outbox/types.ts +76 -0
  129. package/src/pull-requests/build-link-change.ts +95 -0
  130. package/src/pull-requests/build-state-fact.ts +39 -0
  131. package/src/pull-requests/collect-attempt-trailers.ts +20 -0
  132. package/src/pull-requests/collect-seen-state.ts +19 -0
  133. package/src/pull-requests/constants.ts +5 -0
  134. package/src/pull-requests/link-pull-request.ts +119 -0
  135. package/src/pull-requests/parse-pull-request-url.ts +30 -0
  136. package/src/pull-requests/types.ts +63 -0
  137. package/src/pull-requests/watch-pull-requests.ts +308 -0
  138. package/src/readings/act-on-reading.ts +318 -0
  139. package/src/readings/find-waiting-since.ts +46 -0
  140. package/src/readings/pick-reading-action.ts +315 -0
  141. package/src/readings/types.ts +98 -0
  142. package/src/sessions/build-keep-writes.ts +55 -0
  143. package/src/sessions/build-kept-clear.ts +12 -0
  144. package/src/sessions/forget-sessions.ts +106 -0
  145. package/src/sessions/is-kept-due.ts +27 -0
  146. package/src/sessions/resume-kept-session.ts +25 -0
  147. package/src/sessions/types.ts +39 -0
  148. package/src/test-utils/apply-memory-change.ts +252 -0
  149. package/src/test-utils/build-memory-reader.ts +364 -0
  150. package/src/test-utils/build-mock-bind-change.ts +18 -11
  151. package/src/test-utils/build-mock-claim-change.ts +18 -8
  152. package/src/test-utils/build-mock-file-item-change.ts +3 -1
  153. package/src/test-utils/build-mock-new-id.ts +12 -0
  154. package/src/test-utils/build-mock-note-change.ts +18 -0
  155. package/src/test-utils/build-mock-pause-change.ts +2 -10
  156. package/src/test-utils/build-mock-pull-request-change.ts +19 -9
  157. package/src/test-utils/{build-mock-link-change.ts → build-mock-relation-change.ts} +6 -6
  158. package/src/test-utils/build-mock-running-reading.ts +19 -0
  159. package/src/test-utils/build-mock-settle-change.ts +18 -6
  160. package/src/test-utils/build-mock-start-change.ts +13 -6
  161. package/src/test-utils/claim-store-attempt-reader-cases.ts +163 -133
  162. package/src/test-utils/claim-store-claim-cases.ts +168 -85
  163. package/src/test-utils/claim-store-close-cases.ts +95 -0
  164. package/src/test-utils/claim-store-contract.ts +12 -2
  165. package/src/test-utils/claim-store-copy-cases.ts +100 -0
  166. package/src/test-utils/claim-store-exclusive-cases.ts +65 -19
  167. package/src/test-utils/claim-store-guard-cases.ts +92 -82
  168. package/src/test-utils/claim-store-item-reader-cases.ts +291 -77
  169. package/src/test-utils/claim-store-outbox-cases.ts +281 -0
  170. package/src/test-utils/claim-store-race-cases.ts +150 -0
  171. package/src/test-utils/claim-store-rollback-cases.ts +50 -50
  172. package/src/test-utils/claim-store-seat-cases.ts +270 -0
  173. package/src/test-utils/claim-store-session-cases.ts +289 -0
  174. package/src/test-utils/collect-memory-limit-fills.ts +15 -0
  175. package/src/test-utils/compare-text.ts +8 -0
  176. package/src/test-utils/create-fake-backoff.ts +16 -0
  177. package/src/test-utils/create-fake-clock.ts +20 -0
  178. package/src/test-utils/create-fake-fact-sink.ts +74 -0
  179. package/src/test-utils/create-fake-forge.ts +117 -0
  180. package/src/test-utils/create-fake-worker.ts +303 -0
  181. package/src/test-utils/create-memory-claim-store.ts +73 -484
  182. package/src/test-utils/create-test-dispatch.ts +597 -0
  183. package/src/test-utils/forge-contract.ts +171 -0
  184. package/src/test-utils/is-same-held-seat.ts +11 -0
  185. package/src/test-utils/types.ts +14 -2
  186. package/src/test-utils/worker-contract.ts +192 -0
  187. package/src/timeouts.ts +17 -0
  188. package/src/types.ts +32 -0
  189. package/dist/test-utils/claim-store-pool-cases.d.ts +0 -2
  190. package/src/test-utils/claim-store-pool-cases.ts +0 -178
@@ -1,66 +1,126 @@
1
1
  import { expect } from 'bun:test';
2
2
  import { brandSchemas } from '@boxxy/model';
3
- import { buildMockAttemptRow } from '@boxxy/model/test-utils/build-mock-attempt-row';
4
- import { buildMockItemRow } from '@boxxy/model/test-utils/build-mock-item-row';
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';
5
7
  import { buildMockClaimChange } from './build-mock-claim-change.ts';
6
8
  import { buildMockFileItemChange } from './build-mock-file-item-change.ts';
7
- import { buildMockLinkChange } from './build-mock-link-change.ts';
9
+ import { buildMockLeverChange } from './build-mock-lever-change.ts';
10
+ import { buildMockNoteChange } from './build-mock-note-change.ts';
8
11
  import { buildMockPullRequestChange } from './build-mock-pull-request-change.ts';
12
+ import { buildMockRelationChange } from './build-mock-relation-change.ts';
9
13
  import { buildMockSettleChange } from './build-mock-settle-change.ts';
10
14
  import type { ClaimStoreCase } from './types.ts';
11
15
 
12
16
  export const claimStoreItemReaderCases: readonly ClaimStoreCase[] = [
13
17
  [
14
- '#readSnapshot holds the open items, the store version and the free slots',
18
+ '#readSnapshot holds the open items, the store version and the limits',
15
19
  async (store) => {
16
- const open = buildMockItemRow();
17
- const archived = buildMockItemRow({ closedAt: brandSchemas.instant.parse(1500) });
20
+ const open = buildMockV1ItemRow();
21
+ const archived = buildMockV1ItemRow({ closedAt: brandSchemas.instant.parse(1500) });
18
22
 
19
23
  await store.exclusive(async (transaction) => {
20
- await transaction.syncPoolSlots([{ name: brandSchemas.poolName.parse('build'), slots: 2 }]);
24
+ await transaction.syncLimits(
25
+ [{ name: brandSchemas.limitName.parse('workers'), max: 2, counts: {} }],
26
+ brandSchemas.instant.parse(500),
27
+ );
28
+
21
29
  await transaction.applyChange(buildMockFileItemChange(open));
22
30
  await transaction.applyChange(buildMockFileItemChange(archived));
23
31
  });
24
32
 
25
33
  const snapshot: unknown = await store.read((transaction) =>
26
- transaction.readSnapshot(brandSchemas.instant.parse(5000), new Map()),
34
+ transaction.readSnapshot(brandSchemas.instant.parse(5000), buildMockItemRules()),
27
35
  );
28
36
 
29
37
  return {
30
38
  received: snapshot,
31
39
  expected: {
32
40
  storeVersion: 2,
33
- items: [{ ...open, derived: { state: 'ready' } }],
34
- links: [],
41
+ items: [
42
+ {
43
+ ...open,
44
+ derived: { state: 'ready' },
45
+ position: { kind: 'phase', phase: 'build', round: 1 },
46
+ },
47
+ ],
48
+ relations: [],
35
49
  levers: [],
36
- freeSlots: { build: 2 },
50
+ limits: [{ name: 'workers', max: 2, counts: {}, held: 0 }],
51
+ wakes: [],
37
52
  },
38
53
  };
39
54
  },
40
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
+ ],
41
109
  [
42
110
  '#readSnapshot drops a done item',
43
111
  async (store) => {
44
- const item = buildMockItemRow();
45
- const attempt = buildMockAttemptRow({ item: item.id, state: 'starting' });
112
+ const item = buildMockV1ItemRow();
113
+ const attempt = buildMockV1AttemptRow({ item: item.id, state: 'starting' });
46
114
 
47
115
  await store.exclusive(async (transaction) => {
48
- await transaction.syncPoolSlots([{ name: attempt.pool, slots: 1 }]);
49
116
  await transaction.applyChange(buildMockFileItemChange(item));
50
117
  await transaction.applyChange(buildMockClaimChange(attempt));
51
-
52
- await transaction.applyChange(
53
- buildMockSettleChange({
54
- attempt: { ...attempt, slot: 1, slotGen: 1 },
55
- state: 'succeeded',
56
- }),
57
- );
58
-
118
+ await transaction.applyChange(buildMockSettleChange({ attempt, state: 'succeeded' }));
59
119
  await transaction.applyChange(buildMockPullRequestChange({ attempt, state: 'merged' }));
60
120
  });
61
121
 
62
122
  const snapshot = await store.read((transaction) =>
63
- transaction.readSnapshot(brandSchemas.instant.parse(5000), new Map()),
123
+ transaction.readSnapshot(brandSchemas.instant.parse(5000), buildMockItemRules()),
64
124
  );
65
125
 
66
126
  return { received: snapshot.items, expected: [] };
@@ -70,85 +130,157 @@ export const claimStoreItemReaderCases: readonly ClaimStoreCase[] = [
70
130
  '#readItem returns null for an unknown item',
71
131
  async (store) => {
72
132
  const read = await store.read((transaction) =>
73
- transaction.readItem(buildMockItemRow().id, brandSchemas.instant.parse(5000), new Map()),
133
+ transaction.readItem(
134
+ buildMockV1ItemRow().id,
135
+ brandSchemas.instant.parse(5000),
136
+ buildMockItemRules(),
137
+ ),
74
138
  );
75
139
 
76
140
  return { received: read, expected: null };
77
141
  },
78
142
  ],
79
143
  [
80
- '#readItem derives blocked for the target of a live link from a ready item',
144
+ '#readItem derives blocked for the target of a live relation from a ready item',
81
145
  async (store) => {
82
- const source = buildMockItemRow();
83
- const target = buildMockItemRow();
146
+ const source = buildMockV1ItemRow();
147
+ const target = buildMockV1ItemRow();
84
148
 
85
149
  await store.exclusive(async (transaction) => {
86
150
  await transaction.applyChange(buildMockFileItemChange(source));
87
151
  await transaction.applyChange(buildMockFileItemChange(target));
88
152
 
89
153
  await transaction.applyChange(
90
- buildMockLinkChange({ source: source.id, target: target.id }),
154
+ buildMockRelationChange({ source: source.id, target: target.id }),
91
155
  );
92
156
  });
93
157
 
94
158
  const read = await store.read((transaction) =>
95
- transaction.readItem(target.id, brandSchemas.instant.parse(5000), new Map()),
159
+ transaction.readItem(target.id, brandSchemas.instant.parse(5000), buildMockItemRules()),
96
160
  );
97
161
 
98
162
  return { received: read?.derived, expected: { state: 'blocked' } };
99
163
  },
100
164
  ],
101
165
  [
102
- '#readItem derives ready for the target of a link from an archived item',
166
+ '#readItem derives ready for the target of a relation from an archived item',
103
167
  async (store) => {
104
- const source = buildMockItemRow({ closedAt: brandSchemas.instant.parse(1500) });
105
- const target = buildMockItemRow();
168
+ const source = buildMockV1ItemRow({ closedAt: brandSchemas.instant.parse(1500) });
169
+ const target = buildMockV1ItemRow();
106
170
 
107
171
  await store.exclusive(async (transaction) => {
108
172
  await transaction.applyChange(buildMockFileItemChange(source));
109
173
  await transaction.applyChange(buildMockFileItemChange(target));
110
174
 
111
175
  await transaction.applyChange(
112
- buildMockLinkChange({ source: source.id, target: target.id }),
176
+ buildMockRelationChange({ source: source.id, target: target.id }),
113
177
  );
114
178
  });
115
179
 
116
180
  const read = await store.read((transaction) =>
117
- transaction.readItem(target.id, brandSchemas.instant.parse(5000), new Map()),
181
+ transaction.readItem(target.id, brandSchemas.instant.parse(5000), buildMockItemRules()),
118
182
  );
119
183
 
120
184
  return { received: read?.derived, expected: { state: 'ready' } };
121
185
  },
122
186
  ],
123
187
  [
124
- '#readItem derives attempts used up from the max attempts of the kind',
188
+ '#readItem holds an item whose phase lost its attempts in a row',
125
189
  async (store) => {
126
- const item = buildMockItemRow();
127
- const attempt = buildMockAttemptRow({ item: item.id, state: 'starting' });
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
+ });
128
199
 
129
- const phases = new Map([[attempt.phase, 1]]);
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
+ });
130
237
 
131
238
  await store.exclusive(async (transaction) => {
132
- await transaction.syncPoolSlots([{ name: attempt.pool, slots: 1 }]);
133
239
  await transaction.applyChange(buildMockFileItemChange(item));
134
- await transaction.applyChange(buildMockClaimChange(attempt));
135
240
 
136
- await transaction.applyChange(
137
- buildMockSettleChange({ attempt: { ...attempt, slot: 1, slotGen: 1 }, state: 'failed' }),
138
- );
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));
139
247
  });
140
248
 
141
249
  const read = await store.read((transaction) =>
142
- transaction.readItem(
143
- item.id,
144
- brandSchemas.instant.parse(5000),
145
- new Map([[item.kind, phases]]),
146
- ),
250
+ transaction.readItem(item.id, brandSchemas.instant.parse(5000), buildMockItemRules()),
147
251
  );
148
252
 
149
253
  return {
150
- received: read?.derived,
151
- expected: { state: 'waiting', reason: 'attempts used up' },
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
+ },
152
284
  };
153
285
  },
154
286
  ],
@@ -156,10 +288,11 @@ export const claimStoreItemReaderCases: readonly ClaimStoreCase[] = [
156
288
  '#listItems keeps the items of one state and repo, oldest first',
157
289
  async (store) => {
158
290
  const repo = brandSchemas.repoName.parse('acme/lamps');
159
- const older = buildMockItemRow({ repo, filedAt: brandSchemas.instant.parse(1000) });
160
- const newer = buildMockItemRow({ repo, filedAt: brandSchemas.instant.parse(2000) });
161
- const idea = buildMockItemRow({ repo, acceptance: [] });
162
- const elsewhere = buildMockItemRow({ repo: brandSchemas.repoName.parse('acme/fans') });
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');
163
296
 
164
297
  await store.exclusive(async (transaction) => {
165
298
  await transaction.applyChange(buildMockFileItemChange(newer));
@@ -172,7 +305,7 @@ export const claimStoreItemReaderCases: readonly ClaimStoreCase[] = [
172
305
  transaction.listItems(
173
306
  { state: 'ready', repo },
174
307
  brandSchemas.instant.parse(5000),
175
- new Map(),
308
+ buildMockItemRules({ phases: new Map([[build, { needsAcceptance: true }]]) }),
176
309
  ),
177
310
  );
178
311
 
@@ -183,59 +316,81 @@ export const claimStoreItemReaderCases: readonly ClaimStoreCase[] = [
183
316
  '#listOpenIssues returns the open core.issue items of one repo',
184
317
  async (store) => {
185
318
  const repo = brandSchemas.repoName.parse('acme/lamps');
186
- const open = buildMockItemRow({ repo });
187
- const closed = buildMockItemRow({ repo, closedAt: brandSchemas.instant.parse(1500) });
188
- const elsewhere = buildMockItemRow({ repo: brandSchemas.repoName.parse('acme/fans') });
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') });
189
323
 
190
324
  await store.exclusive(async (transaction) => {
191
325
  await transaction.applyChange(buildMockFileItemChange(open));
192
326
  await transaction.applyChange(buildMockFileItemChange(closed));
193
327
  await transaction.applyChange(buildMockFileItemChange(elsewhere));
328
+ await transaction.applyChange(buildMockFileItemChange(task));
194
329
  });
195
330
 
196
331
  const listed: unknown = await store.read((transaction) =>
197
- transaction.listOpenIssues(repo, brandSchemas.instant.parse(5000), new Map()),
332
+ transaction.listOpenIssues(repo, brandSchemas.instant.parse(5000), buildMockItemRules()),
198
333
  );
199
334
 
200
- return { received: listed, expected: [{ ...open, derived: { state: 'ready' } }] };
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
+ };
201
345
  },
202
346
  ],
203
347
  [
204
- '#readItemDetail returns the item with its attempts, links, evidence and PRs',
348
+ '#readItemDetail returns the item with its attempts, relations, evidence, pull requests and notes',
205
349
  async (store) => {
206
- const item = buildMockItemRow();
207
- const blocker = buildMockItemRow();
208
- const attempt = buildMockAttemptRow({ item: item.id, state: 'starting' });
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;
209
361
 
210
362
  await store.exclusive(async (transaction) => {
211
- await transaction.syncPoolSlots([{ name: attempt.pool, slots: 1 }]);
212
363
  await transaction.applyChange(buildMockFileItemChange(item));
213
364
  await transaction.applyChange(buildMockFileItemChange(blocker));
214
365
  await transaction.applyChange(buildMockClaimChange(attempt));
366
+ await transaction.applyChange(buildMockSettleChange({ attempt, state: 'succeeded' }));
367
+ await transaction.applyChange(buildMockPullRequestChange({ attempt, state: 'open' }));
215
368
 
216
369
  await transaction.applyChange(
217
- buildMockSettleChange({
218
- attempt: { ...attempt, slot: 1, slotGen: 1 },
219
- state: 'succeeded',
220
- }),
370
+ buildMockRelationChange({ source: blocker.id, target: item.id }),
221
371
  );
222
372
 
223
- await transaction.applyChange(buildMockPullRequestChange({ attempt, state: 'open' }));
224
- await transaction.applyChange(buildMockLinkChange({ source: blocker.id, target: item.id }));
373
+ await transaction.applyChange(buildMockNoteChange(note));
225
374
  });
226
375
 
227
376
  const detail: unknown = await store.read((transaction) =>
228
- transaction.readItemDetail(item.id, brandSchemas.instant.parse(5000), new Map()),
377
+ transaction.readItemDetail(item.id, brandSchemas.instant.parse(5000), buildMockItemRules()),
229
378
  );
230
379
 
231
380
  return {
232
381
  received: detail,
233
382
  expected: {
234
- item: { ...item, derived: { state: 'waiting', reason: 'PR open' }, levers: [] },
383
+ item: {
384
+ ...item,
385
+ derived: { state: 'waiting', reason: 'pull request open' },
386
+ position: { kind: 'end', reason: 'pipeline-done' },
387
+ levers: [],
388
+ },
235
389
  attempts: [expect.objectContaining({ id: attempt.id, state: 'succeeded' })],
236
- links: [expect.objectContaining({ source: blocker.id, target: item.id })],
237
- evidence: [expect.objectContaining({ attempt: attempt.id, kind: 'pr' })],
390
+ relations: [expect.objectContaining({ source: blocker.id, target: item.id })],
391
+ evidence: [expect.objectContaining({ attempt: attempt.id, kind: 'pull_request' })],
238
392
  pullRequests: [expect.objectContaining({ attempt: attempt.id, state: 'open' })],
393
+ notes: [note],
239
394
  },
240
395
  };
241
396
  },
@@ -245,13 +400,72 @@ export const claimStoreItemReaderCases: readonly ClaimStoreCase[] = [
245
400
  async (store) => {
246
401
  const detail = await store.read((transaction) =>
247
402
  transaction.readItemDetail(
248
- buildMockItemRow().id,
403
+ buildMockV1ItemRow().id,
249
404
  brandSchemas.instant.parse(5000),
250
- new Map(),
405
+ buildMockItemRules(),
251
406
  ),
252
407
  );
253
408
 
254
409
  return { received: detail, expected: null };
255
410
  },
256
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
+ ],
257
471
  ];