@boxxy/dispatch 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/test-utils/build-mock-bind-change.d.ts +7 -0
- package/dist/test-utils/build-mock-claim-change.d.ts +2 -0
- package/dist/test-utils/build-mock-file-item-change.d.ts +2 -0
- package/dist/test-utils/build-mock-lever-change.d.ts +2 -0
- package/dist/test-utils/build-mock-link-change.d.ts +7 -0
- package/dist/test-utils/build-mock-pause-change.d.ts +2 -0
- package/dist/test-utils/build-mock-pull-request-change.d.ts +7 -0
- package/dist/test-utils/build-mock-settle-change.d.ts +7 -0
- package/dist/test-utils/build-mock-start-change.d.ts +2 -0
- package/dist/test-utils/claim-store-attempt-reader-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-claim-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-contract.d.ts +2 -0
- package/dist/test-utils/claim-store-exclusive-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-guard-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-item-reader-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-pool-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-rollback-cases.d.ts +2 -0
- package/dist/test-utils/create-memory-claim-store.d.ts +2 -0
- package/dist/test-utils/types.d.ts +9 -0
- package/package.json +8 -2
- package/src/test-utils/build-mock-bind-change.ts +36 -0
- package/src/test-utils/build-mock-claim-change.ts +42 -0
- package/src/test-utils/build-mock-file-item-change.ts +18 -0
- package/src/test-utils/build-mock-lever-change.ts +18 -0
- package/src/test-utils/build-mock-link-change.ts +40 -0
- package/src/test-utils/build-mock-pause-change.ts +27 -0
- package/src/test-utils/build-mock-pull-request-change.ts +53 -0
- package/src/test-utils/build-mock-settle-change.ts +58 -0
- package/src/test-utils/build-mock-start-change.ts +31 -0
- package/src/test-utils/claim-store-attempt-reader-cases.ts +327 -0
- package/src/test-utils/claim-store-claim-cases.ts +231 -0
- package/src/test-utils/claim-store-contract.ts +18 -0
- package/src/test-utils/claim-store-exclusive-cases.ts +197 -0
- package/src/test-utils/claim-store-guard-cases.ts +373 -0
- package/src/test-utils/claim-store-item-reader-cases.ts +257 -0
- package/src/test-utils/claim-store-pool-cases.ts +178 -0
- package/src/test-utils/claim-store-rollback-cases.ts +175 -0
- package/src/test-utils/create-memory-claim-store.ts +574 -0
- package/src/test-utils/types.ts +11 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { expect } from 'bun:test';
|
|
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';
|
|
5
|
+
import { buildMockClaimChange } from './build-mock-claim-change.ts';
|
|
6
|
+
import { buildMockFileItemChange } from './build-mock-file-item-change.ts';
|
|
7
|
+
import { buildMockLinkChange } from './build-mock-link-change.ts';
|
|
8
|
+
import { buildMockPullRequestChange } from './build-mock-pull-request-change.ts';
|
|
9
|
+
import { buildMockSettleChange } from './build-mock-settle-change.ts';
|
|
10
|
+
import type { ClaimStoreCase } from './types.ts';
|
|
11
|
+
|
|
12
|
+
export const claimStoreItemReaderCases: readonly ClaimStoreCase[] = [
|
|
13
|
+
[
|
|
14
|
+
'#readSnapshot holds the open items, the store version and the free slots',
|
|
15
|
+
async (store) => {
|
|
16
|
+
const open = buildMockItemRow();
|
|
17
|
+
const archived = buildMockItemRow({ closedAt: brandSchemas.instant.parse(1500) });
|
|
18
|
+
|
|
19
|
+
await store.exclusive(async (transaction) => {
|
|
20
|
+
await transaction.syncPoolSlots([{ name: brandSchemas.poolName.parse('build'), slots: 2 }]);
|
|
21
|
+
await transaction.applyChange(buildMockFileItemChange(open));
|
|
22
|
+
await transaction.applyChange(buildMockFileItemChange(archived));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const snapshot: unknown = await store.read((transaction) =>
|
|
26
|
+
transaction.readSnapshot(brandSchemas.instant.parse(5000), new Map()),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
received: snapshot,
|
|
31
|
+
expected: {
|
|
32
|
+
storeVersion: 2,
|
|
33
|
+
items: [{ ...open, derived: { state: 'ready' } }],
|
|
34
|
+
links: [],
|
|
35
|
+
levers: [],
|
|
36
|
+
freeSlots: { build: 2 },
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
[
|
|
42
|
+
'#readSnapshot drops a done item',
|
|
43
|
+
async (store) => {
|
|
44
|
+
const item = buildMockItemRow();
|
|
45
|
+
const attempt = buildMockAttemptRow({ item: item.id, state: 'starting' });
|
|
46
|
+
|
|
47
|
+
await store.exclusive(async (transaction) => {
|
|
48
|
+
await transaction.syncPoolSlots([{ name: attempt.pool, slots: 1 }]);
|
|
49
|
+
await transaction.applyChange(buildMockFileItemChange(item));
|
|
50
|
+
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
|
+
|
|
59
|
+
await transaction.applyChange(buildMockPullRequestChange({ attempt, state: 'merged' }));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const snapshot = await store.read((transaction) =>
|
|
63
|
+
transaction.readSnapshot(brandSchemas.instant.parse(5000), new Map()),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return { received: snapshot.items, expected: [] };
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
[
|
|
70
|
+
'#readItem returns null for an unknown item',
|
|
71
|
+
async (store) => {
|
|
72
|
+
const read = await store.read((transaction) =>
|
|
73
|
+
transaction.readItem(buildMockItemRow().id, brandSchemas.instant.parse(5000), new Map()),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return { received: read, expected: null };
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
[
|
|
80
|
+
'#readItem derives blocked for the target of a live link from a ready item',
|
|
81
|
+
async (store) => {
|
|
82
|
+
const source = buildMockItemRow();
|
|
83
|
+
const target = buildMockItemRow();
|
|
84
|
+
|
|
85
|
+
await store.exclusive(async (transaction) => {
|
|
86
|
+
await transaction.applyChange(buildMockFileItemChange(source));
|
|
87
|
+
await transaction.applyChange(buildMockFileItemChange(target));
|
|
88
|
+
|
|
89
|
+
await transaction.applyChange(
|
|
90
|
+
buildMockLinkChange({ source: source.id, target: target.id }),
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const read = await store.read((transaction) =>
|
|
95
|
+
transaction.readItem(target.id, brandSchemas.instant.parse(5000), new Map()),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
return { received: read?.derived, expected: { state: 'blocked' } };
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
[
|
|
102
|
+
'#readItem derives ready for the target of a link from an archived item',
|
|
103
|
+
async (store) => {
|
|
104
|
+
const source = buildMockItemRow({ closedAt: brandSchemas.instant.parse(1500) });
|
|
105
|
+
const target = buildMockItemRow();
|
|
106
|
+
|
|
107
|
+
await store.exclusive(async (transaction) => {
|
|
108
|
+
await transaction.applyChange(buildMockFileItemChange(source));
|
|
109
|
+
await transaction.applyChange(buildMockFileItemChange(target));
|
|
110
|
+
|
|
111
|
+
await transaction.applyChange(
|
|
112
|
+
buildMockLinkChange({ source: source.id, target: target.id }),
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const read = await store.read((transaction) =>
|
|
117
|
+
transaction.readItem(target.id, brandSchemas.instant.parse(5000), new Map()),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
return { received: read?.derived, expected: { state: 'ready' } };
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
[
|
|
124
|
+
'#readItem derives attempts used up from the max attempts of the kind',
|
|
125
|
+
async (store) => {
|
|
126
|
+
const item = buildMockItemRow();
|
|
127
|
+
const attempt = buildMockAttemptRow({ item: item.id, state: 'starting' });
|
|
128
|
+
|
|
129
|
+
const phases = new Map([[attempt.phase, 1]]);
|
|
130
|
+
|
|
131
|
+
await store.exclusive(async (transaction) => {
|
|
132
|
+
await transaction.syncPoolSlots([{ name: attempt.pool, slots: 1 }]);
|
|
133
|
+
await transaction.applyChange(buildMockFileItemChange(item));
|
|
134
|
+
await transaction.applyChange(buildMockClaimChange(attempt));
|
|
135
|
+
|
|
136
|
+
await transaction.applyChange(
|
|
137
|
+
buildMockSettleChange({ attempt: { ...attempt, slot: 1, slotGen: 1 }, state: 'failed' }),
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const read = await store.read((transaction) =>
|
|
142
|
+
transaction.readItem(
|
|
143
|
+
item.id,
|
|
144
|
+
brandSchemas.instant.parse(5000),
|
|
145
|
+
new Map([[item.kind, phases]]),
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
received: read?.derived,
|
|
151
|
+
expected: { state: 'waiting', reason: 'attempts used up' },
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
[
|
|
156
|
+
'#listItems keeps the items of one state and repo, oldest first',
|
|
157
|
+
async (store) => {
|
|
158
|
+
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') });
|
|
163
|
+
|
|
164
|
+
await store.exclusive(async (transaction) => {
|
|
165
|
+
await transaction.applyChange(buildMockFileItemChange(newer));
|
|
166
|
+
await transaction.applyChange(buildMockFileItemChange(older));
|
|
167
|
+
await transaction.applyChange(buildMockFileItemChange(idea));
|
|
168
|
+
await transaction.applyChange(buildMockFileItemChange(elsewhere));
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const listed = await store.read((transaction) =>
|
|
172
|
+
transaction.listItems(
|
|
173
|
+
{ state: 'ready', repo },
|
|
174
|
+
brandSchemas.instant.parse(5000),
|
|
175
|
+
new Map(),
|
|
176
|
+
),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
return { received: listed.map((item) => item.id), expected: [older.id, newer.id] };
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
[
|
|
183
|
+
'#listOpenIssues returns the open core.issue items of one repo',
|
|
184
|
+
async (store) => {
|
|
185
|
+
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') });
|
|
189
|
+
|
|
190
|
+
await store.exclusive(async (transaction) => {
|
|
191
|
+
await transaction.applyChange(buildMockFileItemChange(open));
|
|
192
|
+
await transaction.applyChange(buildMockFileItemChange(closed));
|
|
193
|
+
await transaction.applyChange(buildMockFileItemChange(elsewhere));
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const listed: unknown = await store.read((transaction) =>
|
|
197
|
+
transaction.listOpenIssues(repo, brandSchemas.instant.parse(5000), new Map()),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
return { received: listed, expected: [{ ...open, derived: { state: 'ready' } }] };
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
[
|
|
204
|
+
'#readItemDetail returns the item with its attempts, links, evidence and PRs',
|
|
205
|
+
async (store) => {
|
|
206
|
+
const item = buildMockItemRow();
|
|
207
|
+
const blocker = buildMockItemRow();
|
|
208
|
+
const attempt = buildMockAttemptRow({ item: item.id, state: 'starting' });
|
|
209
|
+
|
|
210
|
+
await store.exclusive(async (transaction) => {
|
|
211
|
+
await transaction.syncPoolSlots([{ name: attempt.pool, slots: 1 }]);
|
|
212
|
+
await transaction.applyChange(buildMockFileItemChange(item));
|
|
213
|
+
await transaction.applyChange(buildMockFileItemChange(blocker));
|
|
214
|
+
await transaction.applyChange(buildMockClaimChange(attempt));
|
|
215
|
+
|
|
216
|
+
await transaction.applyChange(
|
|
217
|
+
buildMockSettleChange({
|
|
218
|
+
attempt: { ...attempt, slot: 1, slotGen: 1 },
|
|
219
|
+
state: 'succeeded',
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
await transaction.applyChange(buildMockPullRequestChange({ attempt, state: 'open' }));
|
|
224
|
+
await transaction.applyChange(buildMockLinkChange({ source: blocker.id, target: item.id }));
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const detail: unknown = await store.read((transaction) =>
|
|
228
|
+
transaction.readItemDetail(item.id, brandSchemas.instant.parse(5000), new Map()),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
received: detail,
|
|
233
|
+
expected: {
|
|
234
|
+
item: { ...item, derived: { state: 'waiting', reason: 'PR open' }, levers: [] },
|
|
235
|
+
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' })],
|
|
238
|
+
pullRequests: [expect.objectContaining({ attempt: attempt.id, state: 'open' })],
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
[
|
|
244
|
+
'#readItemDetail returns null for an unknown item',
|
|
245
|
+
async (store) => {
|
|
246
|
+
const detail = await store.read((transaction) =>
|
|
247
|
+
transaction.readItemDetail(
|
|
248
|
+
buildMockItemRow().id,
|
|
249
|
+
brandSchemas.instant.parse(5000),
|
|
250
|
+
new Map(),
|
|
251
|
+
),
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
return { received: detail, expected: null };
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
];
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { brandSchemas } from '@boxxy/model';
|
|
2
|
+
import { buildMockAttemptRow } from '@boxxy/model/test-utils/build-mock-attempt-row';
|
|
3
|
+
import { buildMockItemRow } from '@boxxy/model/test-utils/build-mock-item-row';
|
|
4
|
+
import { buildMockClaimChange } from './build-mock-claim-change.ts';
|
|
5
|
+
import { buildMockFileItemChange } from './build-mock-file-item-change.ts';
|
|
6
|
+
import { buildMockSettleChange } from './build-mock-settle-change.ts';
|
|
7
|
+
import type { ClaimStoreCase } from './types.ts';
|
|
8
|
+
|
|
9
|
+
export const claimStorePoolCases: readonly ClaimStoreCase[] = [
|
|
10
|
+
[
|
|
11
|
+
'#syncPoolSlots adds a row for each slot of each pool',
|
|
12
|
+
async (store) => {
|
|
13
|
+
await store.exclusive((transaction) =>
|
|
14
|
+
transaction.syncPoolSlots([
|
|
15
|
+
{ name: brandSchemas.poolName.parse('build'), slots: 2 },
|
|
16
|
+
{ name: brandSchemas.poolName.parse('review'), slots: 1 },
|
|
17
|
+
]),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const slots: unknown = await store.read((transaction) => transaction.listPoolSlots());
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
received: slots,
|
|
24
|
+
expected: [
|
|
25
|
+
{ pool: 'build', slot: 1, holder: null, gen: 0, retired: false },
|
|
26
|
+
{ pool: 'build', slot: 2, holder: null, gen: 0, retired: false },
|
|
27
|
+
{ pool: 'review', slot: 1, holder: null, gen: 0, retired: false },
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
'#syncPoolSlots retires the rows above a smaller count and deletes none',
|
|
34
|
+
async (store) => {
|
|
35
|
+
const pool = brandSchemas.poolName.parse('build');
|
|
36
|
+
|
|
37
|
+
await store.exclusive(async (transaction) => {
|
|
38
|
+
await transaction.syncPoolSlots([{ name: pool, slots: 3 }]);
|
|
39
|
+
await transaction.syncPoolSlots([{ name: pool, slots: 1 }]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const slots = await store.read((transaction) => transaction.listPoolSlots());
|
|
43
|
+
|
|
44
|
+
const retired: unknown = slots.map((slot) => [slot.slot, slot.retired]);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
received: retired,
|
|
48
|
+
expected: [
|
|
49
|
+
[1, false],
|
|
50
|
+
[2, true],
|
|
51
|
+
[3, true],
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
[
|
|
57
|
+
'#syncPoolSlots retires every row of a pool the rules no longer name',
|
|
58
|
+
async (store) => {
|
|
59
|
+
const build = brandSchemas.poolName.parse('build');
|
|
60
|
+
const review = brandSchemas.poolName.parse('review');
|
|
61
|
+
|
|
62
|
+
await store.exclusive(async (transaction) => {
|
|
63
|
+
await transaction.syncPoolSlots([
|
|
64
|
+
{ name: build, slots: 1 },
|
|
65
|
+
{ name: review, slots: 2 },
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
await transaction.syncPoolSlots([{ name: build, slots: 1 }]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const slots = await store.read((transaction) => transaction.listPoolSlots());
|
|
72
|
+
|
|
73
|
+
const retired: unknown = slots.map((slot) => [slot.pool, slot.slot, slot.retired]);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
received: retired,
|
|
77
|
+
expected: [
|
|
78
|
+
['build', 1, false],
|
|
79
|
+
['review', 1, true],
|
|
80
|
+
['review', 2, true],
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
[
|
|
86
|
+
'#syncPoolSlots leaves the holder on a slot it retires',
|
|
87
|
+
async (store) => {
|
|
88
|
+
const first = buildMockItemRow();
|
|
89
|
+
const second = buildMockItemRow();
|
|
90
|
+
const lower = buildMockAttemptRow({ item: first.id, state: 'starting' });
|
|
91
|
+
const upper = buildMockAttemptRow({ item: second.id, state: 'starting' });
|
|
92
|
+
|
|
93
|
+
await store.exclusive(async (transaction) => {
|
|
94
|
+
await transaction.syncPoolSlots([{ name: lower.pool, slots: 2 }]);
|
|
95
|
+
await transaction.applyChange(buildMockFileItemChange(first));
|
|
96
|
+
await transaction.applyChange(buildMockFileItemChange(second));
|
|
97
|
+
await transaction.applyChange(buildMockClaimChange(lower));
|
|
98
|
+
await transaction.applyChange(buildMockClaimChange(upper));
|
|
99
|
+
await transaction.syncPoolSlots([{ name: lower.pool, slots: 1 }]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const slots = await store.read((transaction) => transaction.listPoolSlots());
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
received: slots[1],
|
|
106
|
+
expected: {
|
|
107
|
+
pool: upper.pool,
|
|
108
|
+
slot: 2,
|
|
109
|
+
holder: upper.id,
|
|
110
|
+
gen: 1,
|
|
111
|
+
retired: true,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
[
|
|
117
|
+
'#applyChange never takes a retired slot',
|
|
118
|
+
async (store) => {
|
|
119
|
+
const first = buildMockItemRow();
|
|
120
|
+
const second = buildMockItemRow();
|
|
121
|
+
const held = buildMockAttemptRow({ item: first.id, state: 'starting' });
|
|
122
|
+
const refused = buildMockAttemptRow({ item: second.id, state: 'starting' });
|
|
123
|
+
|
|
124
|
+
await store.exclusive(async (transaction) => {
|
|
125
|
+
await transaction.syncPoolSlots([{ name: held.pool, slots: 2 }]);
|
|
126
|
+
await transaction.syncPoolSlots([{ name: held.pool, slots: 1 }]);
|
|
127
|
+
await transaction.applyChange(buildMockFileItemChange(first));
|
|
128
|
+
await transaction.applyChange(buildMockFileItemChange(second));
|
|
129
|
+
await transaction.applyChange(buildMockClaimChange(held));
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const result = await store.exclusive((transaction) =>
|
|
133
|
+
transaction.applyChange(buildMockClaimChange(refused)),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
return { received: result, expected: { kind: 'no-free-slot' } };
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
[
|
|
140
|
+
'#syncPoolSlots reuses a retired row with its gen when the pool grows again',
|
|
141
|
+
async (store) => {
|
|
142
|
+
const first = buildMockItemRow();
|
|
143
|
+
const second = buildMockItemRow();
|
|
144
|
+
const third = buildMockItemRow();
|
|
145
|
+
const lower = buildMockAttemptRow({ item: first.id, state: 'starting' });
|
|
146
|
+
const upper = buildMockAttemptRow({ item: second.id, state: 'starting' });
|
|
147
|
+
const later = buildMockAttemptRow({ item: third.id, state: 'starting' });
|
|
148
|
+
|
|
149
|
+
await store.exclusive(async (transaction) => {
|
|
150
|
+
await transaction.syncPoolSlots([{ name: lower.pool, slots: 2 }]);
|
|
151
|
+
await transaction.applyChange(buildMockFileItemChange(first));
|
|
152
|
+
await transaction.applyChange(buildMockFileItemChange(second));
|
|
153
|
+
await transaction.applyChange(buildMockFileItemChange(third));
|
|
154
|
+
await transaction.applyChange(buildMockClaimChange(lower));
|
|
155
|
+
await transaction.applyChange(buildMockClaimChange(upper));
|
|
156
|
+
|
|
157
|
+
await transaction.applyChange(
|
|
158
|
+
buildMockSettleChange({ attempt: { ...upper, slot: 2, slotGen: 1 }, state: 'failed' }),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
await transaction.syncPoolSlots([{ name: lower.pool, slots: 1 }]);
|
|
162
|
+
await transaction.syncPoolSlots([{ name: lower.pool, slots: 2 }]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const result = await store.exclusive((transaction) =>
|
|
166
|
+
transaction.applyChange(buildMockClaimChange(later)),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
received: result,
|
|
171
|
+
expected: {
|
|
172
|
+
kind: 'applied',
|
|
173
|
+
slot: { pool: later.pool, slot: 2, gen: 2 },
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
];
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { brandSchemas } from '@boxxy/model';
|
|
2
|
+
import type { StoreChange } from '@boxxy/model';
|
|
3
|
+
import { buildMockAttemptRow } from '@boxxy/model/test-utils/build-mock-attempt-row';
|
|
4
|
+
import { buildMockFactId } from '@boxxy/model/test-utils/build-mock-fact-id';
|
|
5
|
+
import { buildMockItemRow } from '@boxxy/model/test-utils/build-mock-item-row';
|
|
6
|
+
import { buildMockClaimChange } from './build-mock-claim-change.ts';
|
|
7
|
+
import { buildMockFileItemChange } from './build-mock-file-item-change.ts';
|
|
8
|
+
import { buildMockSettleChange } from './build-mock-settle-change.ts';
|
|
9
|
+
import type { ClaimStoreCase } from './types.ts';
|
|
10
|
+
|
|
11
|
+
export const claimStoreRollbackCases: readonly ClaimStoreCase[] = [
|
|
12
|
+
[
|
|
13
|
+
'#applyChange refuses a settle whose slot release names another holder whole',
|
|
14
|
+
async (store) => {
|
|
15
|
+
const item = buildMockItemRow();
|
|
16
|
+
const attempt = buildMockAttemptRow({ item: item.id, state: 'starting' });
|
|
17
|
+
const other = buildMockAttemptRow();
|
|
18
|
+
|
|
19
|
+
await store.exclusive(async (transaction) => {
|
|
20
|
+
await transaction.syncPoolSlots([{ name: attempt.pool, slots: 1 }]);
|
|
21
|
+
await transaction.applyChange(buildMockFileItemChange(item));
|
|
22
|
+
await transaction.applyChange(buildMockClaimChange(attempt));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const settle = buildMockSettleChange({
|
|
26
|
+
attempt: { ...attempt, slot: 1, slotGen: 1 },
|
|
27
|
+
state: 'succeeded',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const misnamed: StoreChange = {
|
|
31
|
+
...settle,
|
|
32
|
+
writes: [
|
|
33
|
+
...settle.writes.filter((write) => write.op !== 'release-slot'),
|
|
34
|
+
{ op: 'release-slot', pool: attempt.pool, slot: 1, holder: other.id },
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const result = await store.exclusive((transaction) => transaction.applyChange(misnamed));
|
|
39
|
+
|
|
40
|
+
const state: unknown = await store.read(async (transaction) => {
|
|
41
|
+
const read = await transaction.readAttempt(attempt.id);
|
|
42
|
+
const facts = await transaction.readUnsentFacts(10);
|
|
43
|
+
|
|
44
|
+
const view = await transaction.readItem(
|
|
45
|
+
item.id,
|
|
46
|
+
brandSchemas.instant.parse(5000),
|
|
47
|
+
new Map(),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
return { attempt: read?.state, facts: facts.length, item: view?.derived };
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
received: { result, state },
|
|
55
|
+
expected: {
|
|
56
|
+
result: { kind: 'stale-generation' },
|
|
57
|
+
state: { attempt: 'starting', facts: 2, item: { state: 'running' } },
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
'#applyChange takes back every write of a change that throws',
|
|
64
|
+
async (store) => {
|
|
65
|
+
const filed = buildMockItemRow();
|
|
66
|
+
const claimed = buildMockItemRow();
|
|
67
|
+
const attempt = buildMockAttemptRow({ item: claimed.id, state: 'starting' });
|
|
68
|
+
const fileChange = buildMockFileItemChange(filed);
|
|
69
|
+
const claim = buildMockClaimChange(attempt);
|
|
70
|
+
const reusedFactId: StoreChange = { ...claim, facts: [...claim.facts, ...fileChange.facts] };
|
|
71
|
+
|
|
72
|
+
await store.exclusive(async (transaction) => {
|
|
73
|
+
await transaction.syncPoolSlots([{ name: attempt.pool, slots: 1 }]);
|
|
74
|
+
await transaction.applyChange(fileChange);
|
|
75
|
+
await transaction.applyChange(buildMockFileItemChange(claimed));
|
|
76
|
+
await transaction.applyChange(reusedFactId).catch(() => null);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const state: unknown = await store.read(async (transaction) => ({
|
|
80
|
+
attempt: await transaction.readAttempt(attempt.id),
|
|
81
|
+
slots: await transaction.listPoolSlots(),
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
received: state,
|
|
86
|
+
expected: {
|
|
87
|
+
attempt: null,
|
|
88
|
+
slots: [{ pool: attempt.pool, slot: 1, holder: null, gen: 0, retired: false }],
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
[
|
|
94
|
+
'#applyChange keeps a heartbeat started beside a change it refuses',
|
|
95
|
+
async (store) => {
|
|
96
|
+
const filed = buildMockItemRow();
|
|
97
|
+
const missing = buildMockItemRow();
|
|
98
|
+
const runner = brandSchemas.runnerId.parse('runner-1');
|
|
99
|
+
|
|
100
|
+
await store.exclusive((transaction) =>
|
|
101
|
+
Promise.all([
|
|
102
|
+
transaction.applyChange({
|
|
103
|
+
at: brandSchemas.instant.parse(3000),
|
|
104
|
+
writes: [
|
|
105
|
+
{ op: 'insert', table: 'items', row: filed },
|
|
106
|
+
{
|
|
107
|
+
op: 'update',
|
|
108
|
+
table: 'items',
|
|
109
|
+
key: { id: missing.id },
|
|
110
|
+
expect: {},
|
|
111
|
+
set: { title: 'Mend the gate' },
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
facts: [
|
|
115
|
+
{
|
|
116
|
+
id: buildMockFactId(),
|
|
117
|
+
type: 'boxxy.item.edited',
|
|
118
|
+
entity: `boxxy:item:${missing.id}`,
|
|
119
|
+
actor: 'boxxy:owner:local',
|
|
120
|
+
payload: { title: 'Mend the gate' },
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
}),
|
|
124
|
+
transaction
|
|
125
|
+
.listPoolSlots()
|
|
126
|
+
.then(() => transaction.writeHeartbeat(runner, brandSchemas.instant.parse(1000))),
|
|
127
|
+
]),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const read: unknown = await store.read((transaction) => transaction.readRunner(runner));
|
|
131
|
+
|
|
132
|
+
return { received: read, expected: { id: 'runner-1', heartbeatAt: 1000, paused: false } };
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
[
|
|
136
|
+
'#applyChange runs two changes started together on one call one at a time',
|
|
137
|
+
async (store) => {
|
|
138
|
+
const claimed = buildMockItemRow();
|
|
139
|
+
const later = buildMockItemRow();
|
|
140
|
+
const first = buildMockAttemptRow({ item: claimed.id, state: 'starting', leaseGen: 1 });
|
|
141
|
+
|
|
142
|
+
const second = buildMockAttemptRow({
|
|
143
|
+
item: claimed.id,
|
|
144
|
+
state: 'starting',
|
|
145
|
+
leaseGen: 2,
|
|
146
|
+
number: 2,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await store.exclusive(async (transaction) => {
|
|
150
|
+
await transaction.syncPoolSlots([{ name: first.pool, slots: 2 }]);
|
|
151
|
+
await transaction.applyChange(buildMockFileItemChange(claimed));
|
|
152
|
+
await transaction.applyChange(buildMockClaimChange(first));
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const results = await store.exclusive((transaction) =>
|
|
156
|
+
Promise.all([
|
|
157
|
+
transaction.applyChange(buildMockClaimChange(second)),
|
|
158
|
+
transaction.applyChange(buildMockFileItemChange(later)),
|
|
159
|
+
]),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const read = await store.read((transaction) =>
|
|
163
|
+
transaction.readItem(later.id, brandSchemas.instant.parse(5000), new Map()),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
received: { results, item: read?.id },
|
|
168
|
+
expected: {
|
|
169
|
+
results: [{ kind: 'lease-held' }, { kind: 'applied', slot: null }],
|
|
170
|
+
item: later.id,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
];
|