@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.
- package/README.md +4 -1
- package/dist/attempts/attempt-machine.d.ts +804 -0
- package/dist/attempts/attempt-moves.d.ts +2 -0
- package/dist/attempts/bind-attempt-session.d.ts +14 -0
- package/dist/attempts/build-attempt-change.d.ts +73 -0
- package/dist/attempts/build-attempt-fact.d.ts +3 -0
- package/dist/attempts/end-item-attempts.d.ts +18 -0
- package/dist/attempts/is-same-guard.d.ts +5 -0
- package/dist/attempts/pick-call-attempt.d.ts +10 -0
- package/dist/attempts/stop-stray-session.d.ts +11 -0
- package/dist/attempts/types.d.ts +142 -0
- package/dist/attempts/write-attempt-move.d.ts +7 -0
- package/dist/attempts/write-session-bind.d.ts +3 -0
- package/dist/claims/build-snapshot-hash.d.ts +2 -0
- package/dist/claims/claim-next.d.ts +6 -0
- package/dist/claims/pick-kept-session.d.ts +9 -0
- package/dist/claims/request-wake.d.ts +3 -0
- package/dist/claims/resume-claim.d.ts +3 -0
- package/dist/claims/start-claim.d.ts +3 -0
- package/dist/claims/types.d.ts +44 -0
- package/dist/closeouts/build-settle-plans.d.ts +17 -0
- package/dist/closeouts/close-out-attempt.d.ts +3 -0
- package/dist/closeouts/effect-handlers.d.ts +2 -0
- package/dist/closeouts/pick-settlement.d.ts +14 -0
- package/dist/closeouts/settle-attempt.d.ts +3 -0
- package/dist/closeouts/types.d.ts +50 -0
- package/dist/index.d.ts +38 -0
- package/dist/notices/constants.d.ts +4 -0
- package/dist/notices/create-null-notifier.d.ts +2 -0
- package/dist/notices/send-notices.d.ts +2 -0
- package/dist/notices/types.d.ts +11 -0
- package/dist/outbox/build-backoff-ms.d.ts +2 -0
- package/dist/outbox/build-initial-sender-state.d.ts +2 -0
- package/dist/outbox/build-outbox-status.d.ts +15 -0
- package/dist/outbox/constants.d.ts +2 -0
- package/dist/outbox/measure-wire-bytes.d.ts +2 -0
- package/dist/outbox/pick-sender-move.d.ts +10 -0
- package/dist/outbox/run-sender-once.d.ts +2 -0
- package/dist/outbox/take-batch.d.ts +3 -0
- package/dist/outbox/types.d.ts +61 -0
- package/dist/pull-requests/build-link-change.d.ts +11 -0
- package/dist/pull-requests/build-state-fact.d.ts +8 -0
- package/dist/pull-requests/collect-attempt-trailers.d.ts +2 -0
- package/dist/pull-requests/collect-seen-state.d.ts +7 -0
- package/dist/pull-requests/constants.d.ts +3 -0
- package/dist/pull-requests/link-pull-request.d.ts +2 -0
- package/dist/pull-requests/parse-pull-request-url.d.ts +2 -0
- package/dist/pull-requests/types.d.ts +37 -0
- package/dist/pull-requests/watch-pull-requests.d.ts +2 -0
- package/dist/readings/act-on-reading.d.ts +3 -0
- package/dist/readings/find-waiting-since.d.ts +9 -0
- package/dist/readings/pick-reading-action.d.ts +2 -0
- package/dist/readings/types.d.ts +79 -0
- package/dist/sessions/build-keep-writes.d.ts +9 -0
- package/dist/sessions/build-kept-clear.d.ts +2 -0
- package/dist/sessions/forget-sessions.d.ts +2 -0
- package/dist/sessions/is-kept-due.d.ts +2 -0
- package/dist/sessions/resume-kept-session.d.ts +6 -0
- package/dist/sessions/types.d.ts +19 -0
- package/dist/test-utils/apply-memory-change.d.ts +3 -0
- package/dist/test-utils/build-memory-reader.d.ts +3 -0
- 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-new-id.d.ts +2 -0
- package/dist/test-utils/build-mock-note-change.d.ts +2 -0
- package/dist/test-utils/build-mock-pause-change.d.ts +2 -0
- package/dist/test-utils/build-mock-pull-request-change.d.ts +8 -0
- package/dist/test-utils/build-mock-relation-change.d.ts +7 -0
- package/dist/test-utils/build-mock-running-reading.d.ts +6 -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-close-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-contract.d.ts +2 -0
- package/dist/test-utils/claim-store-copy-cases.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-outbox-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-race-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-rollback-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-seat-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-session-cases.d.ts +2 -0
- package/dist/test-utils/collect-memory-limit-fills.d.ts +3 -0
- package/dist/test-utils/compare-text.d.ts +1 -0
- package/dist/test-utils/create-fake-backoff.d.ts +2 -0
- package/dist/test-utils/create-fake-clock.d.ts +5 -0
- package/dist/test-utils/create-fake-fact-sink.d.ts +8 -0
- package/dist/test-utils/create-fake-forge.d.ts +17 -0
- package/dist/test-utils/create-fake-worker.d.ts +42 -0
- package/dist/test-utils/create-memory-claim-store.d.ts +2 -0
- package/dist/test-utils/create-test-dispatch.d.ts +46 -0
- package/dist/test-utils/forge-contract.d.ts +15 -0
- package/dist/test-utils/is-same-held-seat.d.ts +2 -0
- package/dist/test-utils/types.d.ts +19 -0
- package/dist/test-utils/worker-contract.d.ts +14 -0
- package/dist/timeouts.d.ts +2 -0
- package/dist/types.d.ts +13 -0
- package/package.json +15 -2
- package/src/attempts/attempt-machine.ts +147 -0
- package/src/attempts/attempt-moves.ts +218 -0
- package/src/attempts/bind-attempt-session.ts +61 -0
- package/src/attempts/build-attempt-change.ts +540 -0
- package/src/attempts/build-attempt-fact.ts +135 -0
- package/src/attempts/end-item-attempts.ts +95 -0
- package/src/attempts/is-same-guard.ts +22 -0
- package/src/attempts/pick-call-attempt.ts +32 -0
- package/src/attempts/stop-stray-session.ts +48 -0
- package/src/attempts/types.ts +231 -0
- package/src/attempts/write-attempt-move.ts +41 -0
- package/src/attempts/write-session-bind.ts +47 -0
- package/src/claims/build-snapshot-hash.ts +9 -0
- package/src/claims/claim-next.ts +306 -0
- package/src/claims/pick-kept-session.ts +41 -0
- package/src/claims/request-wake.ts +17 -0
- package/src/claims/resume-claim.ts +56 -0
- package/src/claims/start-claim.ts +99 -0
- package/src/claims/types.ts +67 -0
- package/src/closeouts/build-settle-plans.ts +173 -0
- package/src/closeouts/close-out-attempt.ts +156 -0
- package/src/closeouts/effect-handlers.ts +232 -0
- package/src/closeouts/pick-settlement.ts +80 -0
- package/src/closeouts/settle-attempt.ts +161 -0
- package/src/closeouts/types.ts +87 -0
- package/src/index.ts +61 -0
- package/src/notices/constants.ts +7 -0
- package/src/notices/create-null-notifier.ts +6 -0
- package/src/notices/send-notices.ts +41 -0
- package/src/notices/types.ts +14 -0
- package/src/outbox/build-backoff-ms.ts +14 -0
- package/src/outbox/build-initial-sender-state.ts +14 -0
- package/src/outbox/build-outbox-status.ts +43 -0
- package/src/outbox/constants.ts +15 -0
- package/src/outbox/measure-wire-bytes.ts +31 -0
- package/src/outbox/pick-sender-move.ts +37 -0
- package/src/outbox/run-sender-once.ts +174 -0
- package/src/outbox/take-batch.ts +35 -0
- package/src/outbox/types.ts +76 -0
- package/src/pull-requests/build-link-change.ts +95 -0
- package/src/pull-requests/build-state-fact.ts +39 -0
- package/src/pull-requests/collect-attempt-trailers.ts +20 -0
- package/src/pull-requests/collect-seen-state.ts +19 -0
- package/src/pull-requests/constants.ts +5 -0
- package/src/pull-requests/link-pull-request.ts +119 -0
- package/src/pull-requests/parse-pull-request-url.ts +30 -0
- package/src/pull-requests/types.ts +63 -0
- package/src/pull-requests/watch-pull-requests.ts +308 -0
- package/src/readings/act-on-reading.ts +318 -0
- package/src/readings/find-waiting-since.ts +46 -0
- package/src/readings/pick-reading-action.ts +315 -0
- package/src/readings/types.ts +98 -0
- package/src/sessions/build-keep-writes.ts +55 -0
- package/src/sessions/build-kept-clear.ts +12 -0
- package/src/sessions/forget-sessions.ts +106 -0
- package/src/sessions/is-kept-due.ts +27 -0
- package/src/sessions/resume-kept-session.ts +25 -0
- package/src/sessions/types.ts +39 -0
- package/src/test-utils/apply-memory-change.ts +252 -0
- package/src/test-utils/build-memory-reader.ts +364 -0
- package/src/test-utils/build-mock-bind-change.ts +43 -0
- package/src/test-utils/build-mock-claim-change.ts +52 -0
- package/src/test-utils/build-mock-file-item-change.ts +20 -0
- package/src/test-utils/build-mock-lever-change.ts +18 -0
- package/src/test-utils/build-mock-new-id.ts +12 -0
- package/src/test-utils/build-mock-note-change.ts +18 -0
- package/src/test-utils/build-mock-pause-change.ts +19 -0
- package/src/test-utils/build-mock-pull-request-change.ts +63 -0
- package/src/test-utils/build-mock-relation-change.ts +40 -0
- package/src/test-utils/build-mock-running-reading.ts +19 -0
- package/src/test-utils/build-mock-settle-change.ts +70 -0
- package/src/test-utils/build-mock-start-change.ts +38 -0
- package/src/test-utils/claim-store-attempt-reader-cases.ts +357 -0
- package/src/test-utils/claim-store-claim-cases.ts +314 -0
- package/src/test-utils/claim-store-close-cases.ts +95 -0
- package/src/test-utils/claim-store-contract.ts +28 -0
- package/src/test-utils/claim-store-copy-cases.ts +100 -0
- package/src/test-utils/claim-store-exclusive-cases.ts +243 -0
- package/src/test-utils/claim-store-guard-cases.ts +383 -0
- package/src/test-utils/claim-store-item-reader-cases.ts +471 -0
- package/src/test-utils/claim-store-outbox-cases.ts +281 -0
- package/src/test-utils/claim-store-race-cases.ts +150 -0
- package/src/test-utils/claim-store-rollback-cases.ts +175 -0
- package/src/test-utils/claim-store-seat-cases.ts +270 -0
- package/src/test-utils/claim-store-session-cases.ts +289 -0
- package/src/test-utils/collect-memory-limit-fills.ts +15 -0
- package/src/test-utils/compare-text.ts +8 -0
- package/src/test-utils/create-fake-backoff.ts +16 -0
- package/src/test-utils/create-fake-clock.ts +20 -0
- package/src/test-utils/create-fake-fact-sink.ts +74 -0
- package/src/test-utils/create-fake-forge.ts +117 -0
- package/src/test-utils/create-fake-worker.ts +303 -0
- package/src/test-utils/create-memory-claim-store.ts +163 -0
- package/src/test-utils/create-test-dispatch.ts +597 -0
- package/src/test-utils/forge-contract.ts +171 -0
- package/src/test-utils/is-same-held-seat.ts +11 -0
- package/src/test-utils/types.ts +23 -0
- package/src/test-utils/worker-contract.ts +192 -0
- package/src/timeouts.ts +17 -0
- package/src/types.ts +32 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
import { phaseKinds } from '@boxxy/closeout';
|
|
2
|
+
import { buildMockNoslopReport } from '@boxxy/closeout/test-utils/build-mock-noslop-report';
|
|
3
|
+
import type {
|
|
4
|
+
ApplyResult,
|
|
5
|
+
AtcSessionId,
|
|
6
|
+
AttemptId,
|
|
7
|
+
AttemptRow,
|
|
8
|
+
CallerIdentity,
|
|
9
|
+
CapacityBackoff,
|
|
10
|
+
Claim,
|
|
11
|
+
DesignMode,
|
|
12
|
+
FactType,
|
|
13
|
+
ItemId,
|
|
14
|
+
ItemRow,
|
|
15
|
+
LoadedRules,
|
|
16
|
+
NewId,
|
|
17
|
+
OutboxFact,
|
|
18
|
+
PhaseName,
|
|
19
|
+
PipelineStep,
|
|
20
|
+
RunnerId,
|
|
21
|
+
StoreChange,
|
|
22
|
+
} from '@boxxy/model';
|
|
23
|
+
import { brandSchemas } from '@boxxy/model';
|
|
24
|
+
import { conceptPins, concepts } from '@boxxy/planning';
|
|
25
|
+
import { loadRules } from '@boxxy/rules';
|
|
26
|
+
import { buildExampleLoadInput } from '@boxxy/rules/test-utils/build-example-load-input';
|
|
27
|
+
import invariant from 'tiny-invariant';
|
|
28
|
+
import { endItemAttempts } from '../attempts/end-item-attempts.ts';
|
|
29
|
+
import { writeAttemptMove } from '../attempts/write-attempt-move.ts';
|
|
30
|
+
import { claimNext } from '../claims/claim-next.ts';
|
|
31
|
+
import { resumeClaim } from '../claims/resume-claim.ts';
|
|
32
|
+
import { startClaim } from '../claims/start-claim.ts';
|
|
33
|
+
import type { SpawnSettings } from '../claims/types.ts';
|
|
34
|
+
import { closeOutAttempt } from '../closeouts/close-out-attempt.ts';
|
|
35
|
+
import { settleAttempt } from '../closeouts/settle-attempt.ts';
|
|
36
|
+
import type { CallRef } from '../closeouts/types.ts';
|
|
37
|
+
import { actOnReading } from '../readings/act-on-reading.ts';
|
|
38
|
+
import { pickReadingAction } from '../readings/pick-reading-action.ts';
|
|
39
|
+
import { buildMockNewId } from './build-mock-new-id.ts';
|
|
40
|
+
import { createFakeBackoff } from './create-fake-backoff.ts';
|
|
41
|
+
import type { FakeClock } from './create-fake-clock.ts';
|
|
42
|
+
import { createFakeClock } from './create-fake-clock.ts';
|
|
43
|
+
import type { FakeWorker } from './create-fake-worker.ts';
|
|
44
|
+
import { createFakeWorker } from './create-fake-worker.ts';
|
|
45
|
+
import { createMemoryClaimStore } from './create-memory-claim-store.ts';
|
|
46
|
+
import type { ClosableClaimStore } from './types.ts';
|
|
47
|
+
|
|
48
|
+
export interface SeedItemInput {
|
|
49
|
+
// the phase the item stands at, through an `item.retry --phase` lever; with none, the item
|
|
50
|
+
// stands at its pipeline's first phase and has no lever
|
|
51
|
+
readonly phase?: PhaseName;
|
|
52
|
+
readonly designMode?: DesignMode;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ReworkInput {
|
|
56
|
+
// the caller that the item's kept session holds; the first build's caller when left out
|
|
57
|
+
readonly keptCaller?: CallerIdentity | null;
|
|
58
|
+
|
|
59
|
+
// the second build starts through the worker's wake of the kept session; when left out, the
|
|
60
|
+
// test writes the start itself
|
|
61
|
+
readonly startByWorker?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface Rework {
|
|
65
|
+
readonly first: AttemptRow;
|
|
66
|
+
|
|
67
|
+
// the second build as its claim wrote it, and after its start
|
|
68
|
+
readonly claimed: AttemptRow;
|
|
69
|
+
readonly second: AttemptRow;
|
|
70
|
+
|
|
71
|
+
// the body of the first build's close-out
|
|
72
|
+
readonly firstBody: unknown;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface TestDispatch {
|
|
76
|
+
readonly store: ClosableClaimStore;
|
|
77
|
+
readonly clock: FakeClock;
|
|
78
|
+
readonly worker: FakeWorker;
|
|
79
|
+
readonly backoff: CapacityBackoff;
|
|
80
|
+
readonly newId: NewId;
|
|
81
|
+
readonly runner: RunnerId;
|
|
82
|
+
readonly spawn: SpawnSettings;
|
|
83
|
+
readonly rules: () => LoadedRules;
|
|
84
|
+
readonly setRules: (rules: LoadedRules) => void;
|
|
85
|
+
|
|
86
|
+
// the claim loop and the readings of one runner tick, as the server's tick runs them
|
|
87
|
+
readonly tick: () => Promise<void>;
|
|
88
|
+
|
|
89
|
+
// one claim, started or resumed; null when `decide` gives no pick
|
|
90
|
+
readonly claim: () => Promise<Claim | null>;
|
|
91
|
+
readonly seedItem: (input: SeedItemInput) => Promise<ItemId>;
|
|
92
|
+
|
|
93
|
+
// a build that closed out as `built` and settled, so it succeeded; its session is kept
|
|
94
|
+
readonly seedBuild: () => Promise<AttemptRow>;
|
|
95
|
+
|
|
96
|
+
// A build that settled `built` with its session kept, a review that asked for changes, and
|
|
97
|
+
// the claim of the second build through the `resume: true` edge, started on the kept session
|
|
98
|
+
// as the resume of the kept session starts it.
|
|
99
|
+
readonly seedRework: (input: ReworkInput) => Promise<Rework>;
|
|
100
|
+
|
|
101
|
+
// after `seedBuild`: a review asks for changes, and the worker starts the rework on the kept session
|
|
102
|
+
readonly seedReworkAfter: (first: AttemptRow) => Promise<AttemptRow>;
|
|
103
|
+
|
|
104
|
+
// the change of `item.close`: the item's close and the ends of its live attempts
|
|
105
|
+
readonly closeItem: (item: ItemId, reason: string) => Promise<ApplyResult>;
|
|
106
|
+
readonly readAttempt: (id: AttemptId) => Promise<AttemptRow>;
|
|
107
|
+
|
|
108
|
+
// in name order
|
|
109
|
+
readonly heldSeats: (attempt: AttemptId) => Promise<string[]>;
|
|
110
|
+
readonly listFacts: (type: FactType) => Promise<OutboxFact[]>;
|
|
111
|
+
|
|
112
|
+
// what the edge gives the core for a call from the attempt's own session
|
|
113
|
+
readonly buildCallRef: (attempt: AttemptRow) => CallRef;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const repo = brandSchemas.repoName.parse('example/app');
|
|
117
|
+
|
|
118
|
+
export interface TestDispatchOptions {
|
|
119
|
+
// the memory store when left out
|
|
120
|
+
readonly store?: ClosableClaimStore;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// The example rules with the four phase kinds, the store with its limits synced, the fake
|
|
124
|
+
// worker, and a clock that moves only when the test moves it.
|
|
125
|
+
export async function createTestDispatch(options: TestDispatchOptions = {}): Promise<TestDispatch> {
|
|
126
|
+
const store = options.store ?? createMemoryClaimStore();
|
|
127
|
+
const clock = createFakeClock();
|
|
128
|
+
const worker = createFakeWorker({ clock });
|
|
129
|
+
const backoff = createFakeBackoff();
|
|
130
|
+
const newId = buildMockNewId();
|
|
131
|
+
const runner = brandSchemas.runnerId.parse('runner-1');
|
|
132
|
+
const example = buildExampleLoadInput();
|
|
133
|
+
|
|
134
|
+
const loaded = loadRules({
|
|
135
|
+
checkout: example.checkout,
|
|
136
|
+
concepts,
|
|
137
|
+
pins: conceptPins,
|
|
138
|
+
phases: phaseKinds,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
invariant(loaded.kind === 'loaded', 'the example rules load');
|
|
142
|
+
|
|
143
|
+
let rules = loaded.rules;
|
|
144
|
+
|
|
145
|
+
const decoder = new TextDecoder();
|
|
146
|
+
|
|
147
|
+
const spawn: SpawnSettings = {
|
|
148
|
+
target: 'worker-host',
|
|
149
|
+
repoPaths: new Map([[repo, '/tmp/boxxy-test/app']]),
|
|
150
|
+
prompts: new Map(
|
|
151
|
+
[...example.checkout.files].map(([path, bytes]) => [path, decoder.decode(bytes)]),
|
|
152
|
+
),
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
await store.exclusive((tx) => tx.syncLimits(rules.limits, clock.now()));
|
|
156
|
+
|
|
157
|
+
const claim = async (): Promise<Claim | null> => {
|
|
158
|
+
const now = clock.now();
|
|
159
|
+
const deps = { store, worker, rules, backoff, now, newId };
|
|
160
|
+
|
|
161
|
+
const picked = await claimNext({ ...deps, runner, spawn });
|
|
162
|
+
|
|
163
|
+
if (picked?.kind === 'start') {
|
|
164
|
+
await startClaim({ ...deps, claim: picked });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (picked?.kind === 'wake') {
|
|
168
|
+
await resumeClaim({ ...deps, claim: picked });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return picked;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const dispatch: TestDispatch = {
|
|
175
|
+
store,
|
|
176
|
+
clock,
|
|
177
|
+
worker,
|
|
178
|
+
backoff,
|
|
179
|
+
newId,
|
|
180
|
+
runner,
|
|
181
|
+
spawn,
|
|
182
|
+
seedBuild: async () => {
|
|
183
|
+
const built = await seedBuiltAttempt(dispatch);
|
|
184
|
+
|
|
185
|
+
return dispatch.readAttempt(built.first.id);
|
|
186
|
+
},
|
|
187
|
+
seedRework: (input) => seedReworkAttempts(dispatch, input),
|
|
188
|
+
seedReworkAfter: async (first) => {
|
|
189
|
+
await requestChanges(dispatch, first.item);
|
|
190
|
+
|
|
191
|
+
const resumed = await claimResumed(dispatch, first, { startByWorker: true });
|
|
192
|
+
|
|
193
|
+
return resumed.second;
|
|
194
|
+
},
|
|
195
|
+
rules: () => rules,
|
|
196
|
+
setRules: (next) => {
|
|
197
|
+
rules = next;
|
|
198
|
+
},
|
|
199
|
+
claim,
|
|
200
|
+
tick: async () => {
|
|
201
|
+
const claimed = new Set<ItemId>();
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
for (let picked = await claim(); picked !== null; picked = await claim()) {
|
|
205
|
+
if (claimed.has(picked.attempt.item)) {
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
claimed.add(picked.attempt.item);
|
|
210
|
+
}
|
|
211
|
+
} catch {
|
|
212
|
+
// an error in the claim loop ends the loop, not the tick
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const now = clock.now();
|
|
216
|
+
|
|
217
|
+
const live = await store.read((tx) => tx.listLiveAttempts());
|
|
218
|
+
|
|
219
|
+
for (const attempt of live) {
|
|
220
|
+
const reading = await worker.reconcile(attempt);
|
|
221
|
+
const step = await readAttemptStep(store, rules, attempt);
|
|
222
|
+
|
|
223
|
+
const action = pickReadingAction({ attempt, reading, step, sleep: rules.sleep, now });
|
|
224
|
+
|
|
225
|
+
await actOnReading({ store, worker, rules, backoff, attempt, reading, action, now, newId });
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
seedItem: async (input) => {
|
|
229
|
+
const row = buildSeedItemRow(newId, clock.now(), input);
|
|
230
|
+
|
|
231
|
+
await store.exclusive((tx) => tx.applyChange(buildSeedItemChange(newId, row, input.phase)));
|
|
232
|
+
|
|
233
|
+
// a lever orders after an attempt that ends at the same instant, so time moves on
|
|
234
|
+
clock.advance(1000);
|
|
235
|
+
|
|
236
|
+
return row.id;
|
|
237
|
+
},
|
|
238
|
+
closeItem: (item, reason) =>
|
|
239
|
+
store.exclusive(async (tx) => {
|
|
240
|
+
const attempts = await tx.listItemAttempts(item);
|
|
241
|
+
|
|
242
|
+
const now = clock.now();
|
|
243
|
+
const actor = 'boxxy:owner:local';
|
|
244
|
+
const ended = endItemAttempts({ attempts, actor, now, newId });
|
|
245
|
+
|
|
246
|
+
return tx.applyChange({
|
|
247
|
+
at: now,
|
|
248
|
+
under: ended.under,
|
|
249
|
+
writes: [
|
|
250
|
+
...ended.writes,
|
|
251
|
+
{
|
|
252
|
+
op: 'update',
|
|
253
|
+
table: 'items',
|
|
254
|
+
key: { id: item },
|
|
255
|
+
expect: { closedAt: null },
|
|
256
|
+
set: { closedAt: now, closeReason: reason },
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
facts: [
|
|
260
|
+
...ended.facts,
|
|
261
|
+
{
|
|
262
|
+
id: brandSchemas.factId.parse(newId()),
|
|
263
|
+
type: 'boxxy.item.closed',
|
|
264
|
+
entity: `boxxy:item:${item}`,
|
|
265
|
+
actor,
|
|
266
|
+
payload: { reason, byAttempt: null, ended: ended.ended },
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
});
|
|
270
|
+
}),
|
|
271
|
+
readAttempt: async (id) => {
|
|
272
|
+
const row = await store.read((tx) => tx.readAttempt(id));
|
|
273
|
+
|
|
274
|
+
invariant(row !== null, `the test reads a stored attempt: ${id}`);
|
|
275
|
+
|
|
276
|
+
return row;
|
|
277
|
+
},
|
|
278
|
+
heldSeats: async (attempt) => {
|
|
279
|
+
const seats = await store.read((tx) => tx.listLimitSeats());
|
|
280
|
+
|
|
281
|
+
return seats
|
|
282
|
+
.filter((seat) => seat.attempt === attempt)
|
|
283
|
+
.map((seat) => seat.limit)
|
|
284
|
+
.toSorted((first, second) => first.localeCompare(second));
|
|
285
|
+
},
|
|
286
|
+
listFacts: async (type) => {
|
|
287
|
+
const facts = await store.read((tx) => tx.readUnsentFacts(10_000));
|
|
288
|
+
|
|
289
|
+
return facts.filter((fact) => fact.type === type);
|
|
290
|
+
},
|
|
291
|
+
buildCallRef: (attempt) => {
|
|
292
|
+
invariant(attempt.atcSession !== null, 'a call comes from a started session');
|
|
293
|
+
|
|
294
|
+
const caller: CallerIdentity = {
|
|
295
|
+
issuer: 'fake-worker',
|
|
296
|
+
subject: attempt.atcSession,
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
return { attempt: attempt.id, session: attempt.atcSession, caller };
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
return dispatch;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// A build that claimed, closed out as `built` and settled; its session is kept.
|
|
307
|
+
async function seedBuiltAttempt(
|
|
308
|
+
dispatch: TestDispatch,
|
|
309
|
+
): Promise<{ item: ItemId; first: AttemptRow; firstBody: unknown }> {
|
|
310
|
+
const item = await dispatch.seedItem({ phase: brandSchemas.phaseName.parse('build') });
|
|
311
|
+
const first = await claimStarted(dispatch, item);
|
|
312
|
+
|
|
313
|
+
const evidence = brandSchemas.factId.parse(dispatch.newId());
|
|
314
|
+
const now = dispatch.clock.now();
|
|
315
|
+
|
|
316
|
+
await dispatch.store.exclusive((tx) =>
|
|
317
|
+
tx.applyChange({
|
|
318
|
+
at: now,
|
|
319
|
+
writes: [
|
|
320
|
+
{
|
|
321
|
+
op: 'insert',
|
|
322
|
+
table: 'evidence',
|
|
323
|
+
row: {
|
|
324
|
+
factId: evidence,
|
|
325
|
+
attempt: first.id,
|
|
326
|
+
kind: 'test',
|
|
327
|
+
ref: 'bun test',
|
|
328
|
+
observed: 'pass',
|
|
329
|
+
source: 'agent',
|
|
330
|
+
at: now,
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
facts: [
|
|
335
|
+
{
|
|
336
|
+
id: evidence,
|
|
337
|
+
type: 'boxxy.evidence.recorded',
|
|
338
|
+
entity: `boxxy:item:${item}`,
|
|
339
|
+
actor: `boxxy:attempt:${first.id}`,
|
|
340
|
+
payload: { attempt: first.id, kind: 'test', ref: 'bun test', observed: 'pass' },
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
}),
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
const firstBody = {
|
|
347
|
+
result: { kind: 'built' },
|
|
348
|
+
criteria: [{ criterion: 'a1', evidence }],
|
|
349
|
+
openItems: [],
|
|
350
|
+
friction: [],
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
await closeAndSettle(dispatch, first, firstBody);
|
|
354
|
+
|
|
355
|
+
return { item, first, firstBody };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function claimStarted(dispatch: TestDispatch, item: ItemId): Promise<AttemptRow> {
|
|
359
|
+
const claim = await dispatch.claim();
|
|
360
|
+
|
|
361
|
+
invariant(claim?.attempt.item === item, 'the claim picks the seeded item');
|
|
362
|
+
|
|
363
|
+
return dispatch.readAttempt(claim.attempt.id);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function closeAndSettle(dispatch: TestDispatch, attempt: AttemptRow, body: unknown) {
|
|
367
|
+
const base = { store: dispatch.store, rules: dispatch.rules(), newId: dispatch.newId };
|
|
368
|
+
const now = dispatch.clock.now();
|
|
369
|
+
|
|
370
|
+
const answer = await closeOutAttempt({
|
|
371
|
+
...base,
|
|
372
|
+
call: dispatch.buildCallRef(attempt),
|
|
373
|
+
body,
|
|
374
|
+
now,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
invariant(answer.kind === 'recorded', `the seeded close-out records: ${JSON.stringify(answer)}`);
|
|
378
|
+
|
|
379
|
+
dispatch.clock.advance(1000);
|
|
380
|
+
|
|
381
|
+
await settleAttempt({
|
|
382
|
+
...base,
|
|
383
|
+
attempt: attempt.id,
|
|
384
|
+
forced: null,
|
|
385
|
+
sessionStopped: true,
|
|
386
|
+
actor: `boxxy:runner:${dispatch.runner}`,
|
|
387
|
+
now: dispatch.clock.now(),
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
dispatch.clock.advance(1000);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function seedReworkAttempts(dispatch: TestDispatch, input: ReworkInput): Promise<Rework> {
|
|
394
|
+
const built = await seedBuiltAttempt(dispatch);
|
|
395
|
+
|
|
396
|
+
const item = built.item;
|
|
397
|
+
const first = built.first;
|
|
398
|
+
const firstBody = built.firstBody;
|
|
399
|
+
|
|
400
|
+
if (input.keptCaller !== undefined) {
|
|
401
|
+
await dispatch.store.exclusive(async (tx) => {
|
|
402
|
+
const view = await tx.readItem(item, dispatch.clock.now(), dispatch.rules());
|
|
403
|
+
|
|
404
|
+
const kept = view?.keptSession;
|
|
405
|
+
|
|
406
|
+
invariant(
|
|
407
|
+
kept !== null && kept !== undefined,
|
|
408
|
+
'the settle of the first build keeps its session',
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
return tx.applyChange({
|
|
412
|
+
at: dispatch.clock.now(),
|
|
413
|
+
writes: [
|
|
414
|
+
{
|
|
415
|
+
op: 'update',
|
|
416
|
+
table: 'items',
|
|
417
|
+
key: { id: item },
|
|
418
|
+
expect: { keptSession: kept },
|
|
419
|
+
set: { keptSession: { ...kept, caller: input.keptCaller ?? null } },
|
|
420
|
+
},
|
|
421
|
+
],
|
|
422
|
+
facts: [],
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
await requestChanges(dispatch, item);
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
first: await dispatch.readAttempt(first.id),
|
|
431
|
+
firstBody,
|
|
432
|
+
...(await claimResumed(dispatch, first, input)),
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// The review of the item claims, asks for changes and settles.
|
|
437
|
+
async function requestChanges(dispatch: TestDispatch, item: ItemId): Promise<void> {
|
|
438
|
+
const review = await claimStarted(dispatch, item);
|
|
439
|
+
|
|
440
|
+
await closeAndSettle(dispatch, review, {
|
|
441
|
+
verdict: {
|
|
442
|
+
kind: 'changes-requested',
|
|
443
|
+
findings: [{ title: 'The theme ignores the system', body: 'Read the media query.' }],
|
|
444
|
+
},
|
|
445
|
+
noslop: buildMockNoslopReport(),
|
|
446
|
+
friction: [],
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// The claim only, then the `started` that the resume of the kept session records.
|
|
451
|
+
async function claimResumed(
|
|
452
|
+
dispatch: TestDispatch,
|
|
453
|
+
first: AttemptRow,
|
|
454
|
+
input: ReworkInput,
|
|
455
|
+
): Promise<Pick<Rework, 'claimed' | 'second'>> {
|
|
456
|
+
if (input.startByWorker === true) {
|
|
457
|
+
const started = await dispatch.claim();
|
|
458
|
+
|
|
459
|
+
invariant(started?.kind === 'start', 'the rework claims');
|
|
460
|
+
|
|
461
|
+
const attempt = await dispatch.readAttempt(started.attempt.id);
|
|
462
|
+
|
|
463
|
+
return { claimed: started.attempt, second: attempt };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const now = dispatch.clock.now();
|
|
467
|
+
|
|
468
|
+
const claim = await claimNext({
|
|
469
|
+
store: dispatch.store,
|
|
470
|
+
runner: dispatch.runner,
|
|
471
|
+
rules: dispatch.rules(),
|
|
472
|
+
now,
|
|
473
|
+
newId: dispatch.newId,
|
|
474
|
+
backoff: dispatch.backoff,
|
|
475
|
+
spawn: dispatch.spawn,
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
invariant(claim?.kind === 'start' && claim.attempt.phase === first.phase, 'the rework claims');
|
|
479
|
+
|
|
480
|
+
await dispatch.store.exclusive((tx) =>
|
|
481
|
+
writeAttemptMove(
|
|
482
|
+
tx,
|
|
483
|
+
claim.attempt.id,
|
|
484
|
+
{
|
|
485
|
+
kind: 'started',
|
|
486
|
+
via: 'start',
|
|
487
|
+
atcSession: getSession(first),
|
|
488
|
+
caller: claim.attempt.caller,
|
|
489
|
+
resumeFallback: false,
|
|
490
|
+
},
|
|
491
|
+
{ actor: `boxxy:runner:${dispatch.runner}`, now, newId: dispatch.newId },
|
|
492
|
+
),
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
return { claimed: claim.attempt, second: await dispatch.readAttempt(claim.attempt.id) };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function getSession(attempt: AttemptRow): AtcSessionId {
|
|
499
|
+
invariant(attempt.atcSession !== null, 'a started attempt has a session');
|
|
500
|
+
|
|
501
|
+
return attempt.atcSession;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function readAttemptStep(
|
|
505
|
+
store: ClosableClaimStore,
|
|
506
|
+
rules: LoadedRules,
|
|
507
|
+
attempt: AttemptRow,
|
|
508
|
+
): Promise<PipelineStep | null> {
|
|
509
|
+
const item = await store.read((tx) => tx.readItem(attempt.item, attempt.claimedAt, rules));
|
|
510
|
+
|
|
511
|
+
const pipeline = item === null ? undefined : rules.kinds.get(item.kind)?.pipeline;
|
|
512
|
+
|
|
513
|
+
return pipeline?.steps.find((step) => step.phase === attempt.phase) ?? null;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function buildSeedItemRow(
|
|
517
|
+
newId: NewId,
|
|
518
|
+
now: AttemptRow['claimedAt'],
|
|
519
|
+
input: SeedItemInput,
|
|
520
|
+
): ItemRow {
|
|
521
|
+
return {
|
|
522
|
+
id: brandSchemas.itemId.parse(newId()),
|
|
523
|
+
kind: brandSchemas.itemKindName.parse('core.issue'),
|
|
524
|
+
repo,
|
|
525
|
+
title: 'Add a dark theme',
|
|
526
|
+
outcome: 'The app follows the system theme.',
|
|
527
|
+
acceptance: [
|
|
528
|
+
{
|
|
529
|
+
id: brandSchemas.criterionId.parse('a1'),
|
|
530
|
+
text: 'The app reads dark when the system does.',
|
|
531
|
+
evidence: [],
|
|
532
|
+
},
|
|
533
|
+
],
|
|
534
|
+
fields: input.designMode === undefined ? {} : { designMode: input.designMode },
|
|
535
|
+
labels: [],
|
|
536
|
+
foundInAttempt: null,
|
|
537
|
+
filedBy: 'boxxy:owner:local',
|
|
538
|
+
filedAt: now,
|
|
539
|
+
closedAt: null,
|
|
540
|
+
closeReason: null,
|
|
541
|
+
keptSession: null,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function buildSeedItemChange(
|
|
546
|
+
newId: NewId,
|
|
547
|
+
row: ItemRow,
|
|
548
|
+
phase: PhaseName | undefined,
|
|
549
|
+
): StoreChange {
|
|
550
|
+
const { keptSession: _keptSession, ...filed } = row;
|
|
551
|
+
|
|
552
|
+
const fileFact = {
|
|
553
|
+
id: brandSchemas.factId.parse(newId()),
|
|
554
|
+
type: 'boxxy.item.filed',
|
|
555
|
+
entity: `boxxy:item:${row.id}`,
|
|
556
|
+
actor: 'boxxy:owner:local',
|
|
557
|
+
payload: filed,
|
|
558
|
+
} as const;
|
|
559
|
+
|
|
560
|
+
if (phase === undefined) {
|
|
561
|
+
return {
|
|
562
|
+
at: row.filedAt,
|
|
563
|
+
writes: [{ op: 'insert', table: 'items', row }],
|
|
564
|
+
facts: [fileFact],
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const lever = {
|
|
569
|
+
seq: 1,
|
|
570
|
+
kind: 'item.retry',
|
|
571
|
+
item: row.id,
|
|
572
|
+
until: null,
|
|
573
|
+
expiresAt: null,
|
|
574
|
+
reason: null,
|
|
575
|
+
phase,
|
|
576
|
+
actor: 'boxxy:owner:local',
|
|
577
|
+
at: row.filedAt,
|
|
578
|
+
} as const;
|
|
579
|
+
|
|
580
|
+
return {
|
|
581
|
+
at: row.filedAt,
|
|
582
|
+
writes: [
|
|
583
|
+
{ op: 'insert', table: 'items', row },
|
|
584
|
+
{ op: 'insert', table: 'levers', row: lever },
|
|
585
|
+
],
|
|
586
|
+
facts: [
|
|
587
|
+
fileFact,
|
|
588
|
+
{
|
|
589
|
+
id: brandSchemas.factId.parse(newId()),
|
|
590
|
+
type: 'boxxy.lever.recorded',
|
|
591
|
+
entity: `boxxy:item:${row.id}`,
|
|
592
|
+
actor: 'boxxy:owner:local',
|
|
593
|
+
payload: lever,
|
|
594
|
+
},
|
|
595
|
+
],
|
|
596
|
+
};
|
|
597
|
+
}
|