@boxxy/dispatch 0.0.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 +6 -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,197 @@
|
|
|
1
|
+
import { setImmediate } from 'node:timers/promises';
|
|
2
|
+
import { brandSchemas } from '@boxxy/model';
|
|
3
|
+
import { buildMockItemRow } from '@boxxy/model/test-utils/build-mock-item-row';
|
|
4
|
+
import { buildMockFileItemChange } from './build-mock-file-item-change.ts';
|
|
5
|
+
import type { ClaimStoreCase } from './types.ts';
|
|
6
|
+
|
|
7
|
+
export const claimStoreExclusiveCases: readonly ClaimStoreCase[] = [
|
|
8
|
+
[
|
|
9
|
+
'#exclusive runs two calls started together one at a time',
|
|
10
|
+
async (store) => {
|
|
11
|
+
const events: string[] = [];
|
|
12
|
+
const gate = Promise.withResolvers<void>();
|
|
13
|
+
|
|
14
|
+
const first = store.exclusive(async () => {
|
|
15
|
+
events.push('first start');
|
|
16
|
+
|
|
17
|
+
await gate.promise;
|
|
18
|
+
|
|
19
|
+
events.push('first end');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const second = store.exclusive(() => {
|
|
23
|
+
events.push('second start');
|
|
24
|
+
|
|
25
|
+
return Promise.resolve();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// one turn of the event loop lets a store that doesn't wait start the second call
|
|
29
|
+
await setImmediate();
|
|
30
|
+
|
|
31
|
+
gate.resolve();
|
|
32
|
+
|
|
33
|
+
await Promise.all([first, second]);
|
|
34
|
+
|
|
35
|
+
return { received: events, expected: ['first start', 'first end', 'second start'] };
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
[
|
|
39
|
+
'#exclusive leaves the state as it was when work throws',
|
|
40
|
+
async (store) => {
|
|
41
|
+
const item = buildMockItemRow();
|
|
42
|
+
|
|
43
|
+
const writing = store.exclusive(async (transaction) => {
|
|
44
|
+
await transaction.applyChange(buildMockFileItemChange(item));
|
|
45
|
+
|
|
46
|
+
throw new Error('the work failed');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await writing.catch(() => null);
|
|
50
|
+
|
|
51
|
+
const snapshot: unknown = await store.read((transaction) =>
|
|
52
|
+
transaction.readSnapshot(brandSchemas.instant.parse(5000), new Map()),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
received: snapshot,
|
|
57
|
+
expected: {
|
|
58
|
+
storeVersion: 0,
|
|
59
|
+
items: [],
|
|
60
|
+
links: [],
|
|
61
|
+
levers: [],
|
|
62
|
+
freeSlots: {},
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
'#exclusive rejects with the error work throws',
|
|
69
|
+
async (store) => {
|
|
70
|
+
const writing = store.exclusive(() => {
|
|
71
|
+
throw new Error('the work failed');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const settled = await writing.then(
|
|
75
|
+
() => 'resolved',
|
|
76
|
+
(error: unknown) => error,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return { received: settled, expected: new Error('the work failed') };
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
[
|
|
83
|
+
'#exclusive runs a later call after an earlier call throws',
|
|
84
|
+
async (store) => {
|
|
85
|
+
const item = buildMockItemRow();
|
|
86
|
+
|
|
87
|
+
const failing = store.exclusive(() => {
|
|
88
|
+
throw new Error('the work failed');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await failing.catch(() => null);
|
|
92
|
+
|
|
93
|
+
await store.exclusive((transaction) =>
|
|
94
|
+
transaction.applyChange(buildMockFileItemChange(item)),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const read = await store.read((transaction) =>
|
|
98
|
+
transaction.readItem(item.id, brandSchemas.instant.parse(5000), new Map()),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return { received: read?.id, expected: item.id };
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
[
|
|
105
|
+
'#read during a running exclusive call sees the last committed state',
|
|
106
|
+
async (store) => {
|
|
107
|
+
const committed = buildMockItemRow({ filedAt: brandSchemas.instant.parse(1000) });
|
|
108
|
+
const pending = buildMockItemRow({ filedAt: brandSchemas.instant.parse(2000) });
|
|
109
|
+
const gate = Promise.withResolvers<void>();
|
|
110
|
+
const written = Promise.withResolvers<void>();
|
|
111
|
+
|
|
112
|
+
await store.exclusive((transaction) =>
|
|
113
|
+
transaction.applyChange(buildMockFileItemChange(committed)),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const writing = store.exclusive(async (transaction) => {
|
|
117
|
+
await transaction.applyChange(buildMockFileItemChange(pending));
|
|
118
|
+
|
|
119
|
+
written.resolve();
|
|
120
|
+
|
|
121
|
+
await gate.promise;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await written.promise;
|
|
125
|
+
|
|
126
|
+
const seen = await store.read((transaction) =>
|
|
127
|
+
transaction.listItems({}, brandSchemas.instant.parse(5000), new Map()),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
gate.resolve();
|
|
131
|
+
|
|
132
|
+
await writing;
|
|
133
|
+
|
|
134
|
+
return { received: seen.map((item) => item.id), expected: [committed.id] };
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
[
|
|
138
|
+
'#read keeps one snapshot across a commit made during the call',
|
|
139
|
+
async (store) => {
|
|
140
|
+
const committed = buildMockItemRow({ filedAt: brandSchemas.instant.parse(1000) });
|
|
141
|
+
const later = buildMockItemRow({ filedAt: brandSchemas.instant.parse(2000) });
|
|
142
|
+
|
|
143
|
+
await store.exclusive((transaction) =>
|
|
144
|
+
transaction.applyChange(buildMockFileItemChange(committed)),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const seen: unknown = await store.read(async (transaction) => {
|
|
148
|
+
const before = await transaction.listItems({}, brandSchemas.instant.parse(5000), new Map());
|
|
149
|
+
|
|
150
|
+
await store.exclusive((writer) => writer.applyChange(buildMockFileItemChange(later)));
|
|
151
|
+
|
|
152
|
+
const after = await transaction.listItems({}, brandSchemas.instant.parse(5000), new Map());
|
|
153
|
+
|
|
154
|
+
return { before: before.map((item) => item.id), after: after.map((item) => item.id) };
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return { received: seen, expected: { before: [committed.id], after: [committed.id] } };
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
[
|
|
161
|
+
'#applyChange writes the rows and the outbox facts of a change',
|
|
162
|
+
async (store) => {
|
|
163
|
+
const item = buildMockItemRow({ filedAt: brandSchemas.instant.parse(1000) });
|
|
164
|
+
const change = buildMockFileItemChange(item);
|
|
165
|
+
|
|
166
|
+
await store.exclusive((transaction) => transaction.applyChange(change));
|
|
167
|
+
|
|
168
|
+
const facts: unknown = await store.read((transaction) => transaction.readUnsentFacts(10));
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
received: facts,
|
|
172
|
+
expected: [
|
|
173
|
+
{
|
|
174
|
+
seq: 1,
|
|
175
|
+
factId: change.facts[0]?.id,
|
|
176
|
+
type: 'boxxy.item.filed',
|
|
177
|
+
entity: `boxxy:item:${item.id}`,
|
|
178
|
+
actor: 'boxxy:owner:local',
|
|
179
|
+
at: 1000,
|
|
180
|
+
payload: { ...item },
|
|
181
|
+
sentAt: null,
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
[
|
|
188
|
+
'#applyChange returns applied with no slot for a change that takes none',
|
|
189
|
+
async (store) => {
|
|
190
|
+
const change = buildMockFileItemChange(buildMockItemRow());
|
|
191
|
+
|
|
192
|
+
const result = await store.exclusive((transaction) => transaction.applyChange(change));
|
|
193
|
+
|
|
194
|
+
return { received: result, expected: { kind: 'applied', slot: null } };
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
];
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { brandSchemas } from '@boxxy/model';
|
|
2
|
+
import { buildMockAttemptRow } from '@boxxy/model/test-utils/build-mock-attempt-row';
|
|
3
|
+
import { buildMockFactId } from '@boxxy/model/test-utils/build-mock-fact-id';
|
|
4
|
+
import { buildMockItemRow } from '@boxxy/model/test-utils/build-mock-item-row';
|
|
5
|
+
import { buildMockBindChange } from './build-mock-bind-change.ts';
|
|
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 { buildMockStartChange } from './build-mock-start-change.ts';
|
|
10
|
+
import type { ClaimStoreCase } from './types.ts';
|
|
11
|
+
|
|
12
|
+
export const claimStoreGuardCases: readonly ClaimStoreCase[] = [
|
|
13
|
+
[
|
|
14
|
+
'#applyChange returns stale-generation for a settle under an old slot gen',
|
|
15
|
+
async (store) => {
|
|
16
|
+
const first = buildMockItemRow();
|
|
17
|
+
const second = buildMockItemRow();
|
|
18
|
+
const settled = buildMockAttemptRow({ item: first.id, state: 'starting' });
|
|
19
|
+
const holder = buildMockAttemptRow({ item: second.id, state: 'starting' });
|
|
20
|
+
|
|
21
|
+
await store.exclusive(async (transaction) => {
|
|
22
|
+
await transaction.syncPoolSlots([{ name: settled.pool, slots: 1 }]);
|
|
23
|
+
await transaction.applyChange(buildMockFileItemChange(first));
|
|
24
|
+
await transaction.applyChange(buildMockFileItemChange(second));
|
|
25
|
+
await transaction.applyChange(buildMockClaimChange(settled));
|
|
26
|
+
|
|
27
|
+
await transaction.applyChange(
|
|
28
|
+
buildMockSettleChange({
|
|
29
|
+
attempt: { ...settled, slot: 1, slotGen: 1 },
|
|
30
|
+
state: 'succeeded',
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
await transaction.applyChange(buildMockClaimChange(holder));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const result = await store.exclusive((transaction) =>
|
|
38
|
+
transaction.applyChange(
|
|
39
|
+
buildMockSettleChange({ attempt: { ...holder, slot: 1, slotGen: 1 }, state: 'failed' }),
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const slots = await store.read((transaction) => transaction.listPoolSlots());
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
received: { result, slots },
|
|
47
|
+
expected: {
|
|
48
|
+
result: { kind: 'stale-generation' },
|
|
49
|
+
slots: [{ pool: holder.pool, slot: 1, holder: holder.id, gen: 2, retired: false }],
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
[
|
|
55
|
+
'#applyChange releases both leases of a settle under the live generations',
|
|
56
|
+
async (store) => {
|
|
57
|
+
const item = buildMockItemRow();
|
|
58
|
+
const attempt = buildMockAttemptRow({ item: item.id, state: 'starting' });
|
|
59
|
+
|
|
60
|
+
await store.exclusive(async (transaction) => {
|
|
61
|
+
await transaction.syncPoolSlots([{ name: attempt.pool, slots: 1 }]);
|
|
62
|
+
await transaction.applyChange(buildMockFileItemChange(item));
|
|
63
|
+
await transaction.applyChange(buildMockClaimChange(attempt));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const result = await store.exclusive((transaction) =>
|
|
67
|
+
transaction.applyChange(
|
|
68
|
+
buildMockSettleChange({
|
|
69
|
+
attempt: { ...attempt, slot: 1, slotGen: 1 },
|
|
70
|
+
state: 'succeeded',
|
|
71
|
+
}),
|
|
72
|
+
),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const snapshot = await store.read((transaction) =>
|
|
76
|
+
transaction.readSnapshot(brandSchemas.instant.parse(5000), new Map()),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
received: {
|
|
81
|
+
result,
|
|
82
|
+
freeSlots: snapshot.freeSlots,
|
|
83
|
+
derived: snapshot.items[0]?.derived,
|
|
84
|
+
},
|
|
85
|
+
expected: {
|
|
86
|
+
result: { kind: 'applied', slot: null },
|
|
87
|
+
freeSlots: { [attempt.pool]: 1 },
|
|
88
|
+
derived: { state: 'waiting', reason: 'no PR linked' },
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
[
|
|
94
|
+
'#applyChange returns stale-generation and writes no fact under a stale lease generation',
|
|
95
|
+
async (store) => {
|
|
96
|
+
const item = buildMockItemRow();
|
|
97
|
+
const attempt = buildMockAttemptRow({ item: item.id, state: 'starting' });
|
|
98
|
+
|
|
99
|
+
await store.exclusive(async (transaction) => {
|
|
100
|
+
await transaction.syncPoolSlots([{ name: attempt.pool, slots: 1 }]);
|
|
101
|
+
await transaction.applyChange(buildMockFileItemChange(item));
|
|
102
|
+
await transaction.applyChange(buildMockClaimChange(attempt));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const result = await store.exclusive((transaction) =>
|
|
106
|
+
transaction.applyChange(buildMockStartChange({ ...attempt, leaseGen: 2 })),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const state: unknown = await store.read(async (transaction) => {
|
|
110
|
+
const read = await transaction.readAttempt(attempt.id);
|
|
111
|
+
const facts = await transaction.readUnsentFacts(10);
|
|
112
|
+
|
|
113
|
+
return { attempt: read?.state, facts: facts.length };
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
received: { result, state },
|
|
118
|
+
expected: {
|
|
119
|
+
result: { kind: 'stale-generation' },
|
|
120
|
+
state: { attempt: 'starting', facts: 2 },
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
[
|
|
126
|
+
'#applyChange returns stale-generation under a released lease',
|
|
127
|
+
async (store) => {
|
|
128
|
+
const item = buildMockItemRow();
|
|
129
|
+
const attempt = buildMockAttemptRow({ item: item.id, state: 'starting' });
|
|
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 result = await store.exclusive((transaction) =>
|
|
142
|
+
transaction.applyChange(buildMockStartChange(attempt)),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return { received: result, expected: { kind: 'stale-generation' } };
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
[
|
|
149
|
+
'#applyChange applies a change under the live lease generation',
|
|
150
|
+
async (store) => {
|
|
151
|
+
const item = buildMockItemRow();
|
|
152
|
+
const attempt = buildMockAttemptRow({ item: item.id, state: 'starting' });
|
|
153
|
+
|
|
154
|
+
await store.exclusive(async (transaction) => {
|
|
155
|
+
await transaction.syncPoolSlots([{ name: attempt.pool, slots: 1 }]);
|
|
156
|
+
await transaction.applyChange(buildMockFileItemChange(item));
|
|
157
|
+
await transaction.applyChange(buildMockClaimChange(attempt));
|
|
158
|
+
await transaction.applyChange(buildMockStartChange(attempt));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const read = await store.read((transaction) => transaction.readAttempt(attempt.id));
|
|
162
|
+
|
|
163
|
+
return { received: read?.state, expected: 'running' };
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
[
|
|
167
|
+
'#applyChange returns guard-mismatch and writes nothing when an expect fails',
|
|
168
|
+
async (store) => {
|
|
169
|
+
const item = buildMockItemRow();
|
|
170
|
+
|
|
171
|
+
await store.exclusive((transaction) =>
|
|
172
|
+
transaction.applyChange(buildMockFileItemChange(item)),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const result = await store.exclusive((transaction) =>
|
|
176
|
+
transaction.applyChange({
|
|
177
|
+
at: brandSchemas.instant.parse(3000),
|
|
178
|
+
writes: [
|
|
179
|
+
{
|
|
180
|
+
op: 'update',
|
|
181
|
+
table: 'items',
|
|
182
|
+
key: { id: item.id },
|
|
183
|
+
expect: { closedAt: null },
|
|
184
|
+
set: { title: 'Mend the gate' },
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
op: 'update',
|
|
188
|
+
table: 'items',
|
|
189
|
+
key: { id: item.id },
|
|
190
|
+
expect: { title: 'Paint the fence' },
|
|
191
|
+
set: { closedAt: brandSchemas.instant.parse(3000), closeReason: 'done by hand' },
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
facts: [
|
|
195
|
+
{
|
|
196
|
+
id: buildMockFactId(),
|
|
197
|
+
type: 'boxxy.item.closed',
|
|
198
|
+
entity: `boxxy:item:${item.id}`,
|
|
199
|
+
actor: 'boxxy:owner:local',
|
|
200
|
+
payload: { reason: 'done by hand' },
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
}),
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const state: unknown = await store.read(async (transaction) => {
|
|
207
|
+
const read = await transaction.readItem(
|
|
208
|
+
item.id,
|
|
209
|
+
brandSchemas.instant.parse(5000),
|
|
210
|
+
new Map(),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const facts = await transaction.readUnsentFacts(10);
|
|
214
|
+
|
|
215
|
+
return { item: read, facts: facts.length };
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
received: { result, state },
|
|
220
|
+
expected: {
|
|
221
|
+
result: { kind: 'guard-mismatch', table: 'items', key: { id: item.id } },
|
|
222
|
+
state: { item: { ...item, derived: { state: 'ready' } }, facts: 1 },
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
[
|
|
228
|
+
'#applyChange returns guard-mismatch for an update that finds no row',
|
|
229
|
+
async (store) => {
|
|
230
|
+
const item = buildMockItemRow();
|
|
231
|
+
|
|
232
|
+
const result = await store.exclusive((transaction) =>
|
|
233
|
+
transaction.applyChange({
|
|
234
|
+
at: brandSchemas.instant.parse(3000),
|
|
235
|
+
writes: [
|
|
236
|
+
{
|
|
237
|
+
op: 'update',
|
|
238
|
+
table: 'items',
|
|
239
|
+
key: { id: item.id },
|
|
240
|
+
expect: {},
|
|
241
|
+
set: { title: 'Mend the gate' },
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
facts: [
|
|
245
|
+
{
|
|
246
|
+
id: buildMockFactId(),
|
|
247
|
+
type: 'boxxy.item.edited',
|
|
248
|
+
entity: `boxxy:item:${item.id}`,
|
|
249
|
+
actor: 'boxxy:owner:local',
|
|
250
|
+
payload: { title: 'Mend the gate' },
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
}),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
received: result,
|
|
258
|
+
expected: {
|
|
259
|
+
kind: 'guard-mismatch',
|
|
260
|
+
table: 'items',
|
|
261
|
+
key: { id: item.id },
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
[
|
|
267
|
+
'#applyChange matches a JSON column guard whose keys come in another order',
|
|
268
|
+
async (store) => {
|
|
269
|
+
const criterion = {
|
|
270
|
+
id: brandSchemas.criterionId.parse('a1'),
|
|
271
|
+
text: 'The gate swings shut',
|
|
272
|
+
evidence: [],
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const item = buildMockItemRow({ acceptance: [criterion] });
|
|
276
|
+
|
|
277
|
+
await store.exclusive((transaction) =>
|
|
278
|
+
transaction.applyChange(buildMockFileItemChange(item)),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const result = await store.exclusive((transaction) =>
|
|
282
|
+
transaction.applyChange({
|
|
283
|
+
at: brandSchemas.instant.parse(3000),
|
|
284
|
+
writes: [
|
|
285
|
+
{
|
|
286
|
+
op: 'update',
|
|
287
|
+
table: 'items',
|
|
288
|
+
key: { id: item.id },
|
|
289
|
+
expect: {
|
|
290
|
+
acceptance: [
|
|
291
|
+
{ evidence: criterion.evidence, text: criterion.text, id: criterion.id },
|
|
292
|
+
],
|
|
293
|
+
},
|
|
294
|
+
set: { title: 'Mend the gate' },
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
facts: [
|
|
298
|
+
{
|
|
299
|
+
id: buildMockFactId(),
|
|
300
|
+
type: 'boxxy.item.edited',
|
|
301
|
+
entity: `boxxy:item:${item.id}`,
|
|
302
|
+
actor: 'boxxy:owner:local',
|
|
303
|
+
payload: { title: 'Mend the gate' },
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
}),
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
return { received: result, expected: { kind: 'applied', slot: null } };
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
[
|
|
313
|
+
'#findAttemptByAgentSession returns the attempt that holds an agent session id',
|
|
314
|
+
async (store) => {
|
|
315
|
+
const item = buildMockItemRow();
|
|
316
|
+
const attempt = buildMockAttemptRow({ item: item.id, state: 'starting' });
|
|
317
|
+
const agentSession = brandSchemas.agentSessionId.parse('agent-session-1');
|
|
318
|
+
|
|
319
|
+
await store.exclusive(async (transaction) => {
|
|
320
|
+
await transaction.syncPoolSlots([{ name: attempt.pool, slots: 1 }]);
|
|
321
|
+
await transaction.applyChange(buildMockFileItemChange(item));
|
|
322
|
+
await transaction.applyChange(buildMockClaimChange(attempt));
|
|
323
|
+
await transaction.applyChange(buildMockBindChange({ attempt, agentSession }));
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const found = await store.read((transaction) =>
|
|
327
|
+
transaction.findAttemptByAgentSession(agentSession),
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
return { received: found, expected: attempt.id };
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
[
|
|
334
|
+
'#findAttemptByAgentSession returns null for an unknown agent session id',
|
|
335
|
+
async (store) => {
|
|
336
|
+
const found = await store.read((transaction) =>
|
|
337
|
+
transaction.findAttemptByAgentSession(brandSchemas.agentSessionId.parse('agent-session-9')),
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
return { received: found, expected: null };
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
[
|
|
344
|
+
'#applyChange throws on a second attempt row for one agent session id',
|
|
345
|
+
async (store) => {
|
|
346
|
+
const first = buildMockItemRow();
|
|
347
|
+
const second = buildMockItemRow();
|
|
348
|
+
const holder = buildMockAttemptRow({ item: first.id, state: 'starting' });
|
|
349
|
+
const other = buildMockAttemptRow({ item: second.id, state: 'starting' });
|
|
350
|
+
const agentSession = brandSchemas.agentSessionId.parse('agent-session-1');
|
|
351
|
+
|
|
352
|
+
await store.exclusive(async (transaction) => {
|
|
353
|
+
await transaction.syncPoolSlots([{ name: holder.pool, slots: 2 }]);
|
|
354
|
+
await transaction.applyChange(buildMockFileItemChange(first));
|
|
355
|
+
await transaction.applyChange(buildMockFileItemChange(second));
|
|
356
|
+
await transaction.applyChange(buildMockClaimChange(holder));
|
|
357
|
+
await transaction.applyChange(buildMockClaimChange(other));
|
|
358
|
+
await transaction.applyChange(buildMockBindChange({ attempt: holder, agentSession }));
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const binding = store.exclusive((transaction) =>
|
|
362
|
+
transaction.applyChange(buildMockBindChange({ attempt: other, agentSession })),
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
const settled = await binding.then(
|
|
366
|
+
() => 'bound',
|
|
367
|
+
() => 'thrown',
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
return { received: settled, expected: 'thrown' };
|
|
371
|
+
},
|
|
372
|
+
],
|
|
373
|
+
];
|