@devflow-tools/database 0.16.17 → 0.16.19
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/CHANGELOG.md +24 -0
- package/__tests__/database.host-actions.test.ts +405 -0
- package/dist/database.d.ts +14 -0
- package/dist/database.js +250 -0
- package/dist/host-actions.d.ts +47 -0
- package/dist/host-actions.js +152 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -1
- package/package.json +1 -1
- package/src/database.ts +323 -0
- package/src/host-actions.ts +211 -0
- package/src/index.ts +15 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,30 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [0.16.19](https://github.com/shilongfeicool/dev-flow/compare/v0.16.18...v0.16.19) (2026-07-28)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **database:** harden host action evidence ([b74dc11](https://github.com/shilongfeicool/dev-flow/commit/b74dc110e0b96583dd741d2772c286e414928e45))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* **database:** persist workflow host actions ([6e42606](https://github.com/shilongfeicool/dev-flow/commit/6e42606ff028a591878ade9378c5277257a35961))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## [0.16.18](https://github.com/shilongfeicool/dev-flow/compare/v0.16.17...v0.16.18) (2026-07-27)
|
|
23
|
+
|
|
24
|
+
**Note:** Version bump only for package @devflow-tools/database
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
6
30
|
## [0.16.17](https://github.com/shilongfeicool/dev-flow/compare/v0.16.16...v0.16.17) (2026-07-27)
|
|
7
31
|
|
|
8
32
|
**Note:** Version bump only for package @devflow-tools/database
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { Worker } from 'node:worker_threads';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
+
import { DevFlowDatabase } from '../src/database';
|
|
7
|
+
import type { FailHostActionInput, VerifyHostActionInput } from '../src/host-actions';
|
|
8
|
+
|
|
9
|
+
describe('DevFlowDatabase durable host actions', () => {
|
|
10
|
+
let directory: string;
|
|
11
|
+
let database: DevFlowDatabase;
|
|
12
|
+
const projectRoot = '/project-a';
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
directory = mkdtempSync(join(tmpdir(), 'devflow-host-actions-'));
|
|
16
|
+
database = new DevFlowDatabase(directory);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
database.close();
|
|
21
|
+
rmSync(directory, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function request(actionId: string, stepId = 'execute') {
|
|
25
|
+
return database.requestHostAction({
|
|
26
|
+
actionId,
|
|
27
|
+
runId: 'telemetry-run-1',
|
|
28
|
+
engineRunId: 'engine-run-1',
|
|
29
|
+
stepId,
|
|
30
|
+
projectRoot,
|
|
31
|
+
sessionId: 'session-1',
|
|
32
|
+
executionId: 'execution-1',
|
|
33
|
+
contextReceipt: 'context-1',
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function eventCount(actionId: string): number {
|
|
38
|
+
return Number((database.get(
|
|
39
|
+
'SELECT COUNT(*) AS count FROM devflow_host_action_events WHERE action_id = ?',
|
|
40
|
+
actionId,
|
|
41
|
+
) as { count: number }).count);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
it('persists the full waiting to running to reported to verified flow', () => {
|
|
45
|
+
expect(request('action-full')).toMatchObject({
|
|
46
|
+
actionId: 'action-full',
|
|
47
|
+
runId: 'telemetry-run-1',
|
|
48
|
+
engineRunId: 'engine-run-1',
|
|
49
|
+
state: 'waiting',
|
|
50
|
+
report: {},
|
|
51
|
+
finishedAt: undefined,
|
|
52
|
+
});
|
|
53
|
+
expect(database.startHostAction({ actionId: 'action-full', projectRoot })).toMatchObject({
|
|
54
|
+
state: 'running',
|
|
55
|
+
});
|
|
56
|
+
expect(database.reportHostAction({
|
|
57
|
+
actionId: 'action-full',
|
|
58
|
+
projectRoot,
|
|
59
|
+
report: { files: ['src/index.ts'], exitCode: 0 },
|
|
60
|
+
evidenceHash: 'execution-hash',
|
|
61
|
+
})).toMatchObject({ state: 'reported', evidenceHash: 'execution-hash' });
|
|
62
|
+
const verified = database.verifyHostAction({
|
|
63
|
+
actionId: 'action-full',
|
|
64
|
+
projectRoot,
|
|
65
|
+
report: { command: 'npm test', exitCode: 0 },
|
|
66
|
+
evidenceHash: 'verification-hash',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(verified).toMatchObject({
|
|
70
|
+
state: 'verified',
|
|
71
|
+
report: { command: 'npm test', exitCode: 0 },
|
|
72
|
+
evidenceHash: 'verification-hash',
|
|
73
|
+
});
|
|
74
|
+
expect(verified.finishedAt).toEqual(expect.any(Number));
|
|
75
|
+
expect(eventCount('action-full')).toBe(4);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('supports a direct waiting to reported transition for MCP reports', () => {
|
|
79
|
+
request('action-direct');
|
|
80
|
+
expect(database.reportHostAction({
|
|
81
|
+
actionId: 'action-direct',
|
|
82
|
+
projectRoot,
|
|
83
|
+
report: { toolCalls: ['write_file'] },
|
|
84
|
+
evidenceHash: 'direct-hash',
|
|
85
|
+
})).toMatchObject({ state: 'reported', finishedAt: undefined });
|
|
86
|
+
expect(eventCount('action-direct')).toBe(2);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('rolls back invalid transitions without appending an event', () => {
|
|
90
|
+
request('action-invalid');
|
|
91
|
+
const before = eventCount('action-invalid');
|
|
92
|
+
|
|
93
|
+
expect(() => database.verifyHostAction({
|
|
94
|
+
actionId: 'action-invalid',
|
|
95
|
+
projectRoot,
|
|
96
|
+
report: { command: 'npm test', exitCode: 0 },
|
|
97
|
+
evidenceHash: 'verify-hash',
|
|
98
|
+
})).toThrow('HOST_ACTION_INVALID_TRANSITION:waiting->verified');
|
|
99
|
+
expect(database.getHostAction(projectRoot, 'action-invalid')?.state).toBe('waiting');
|
|
100
|
+
expect(eventCount('action-invalid')).toBe(before);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('makes identical terminal reports idempotent and rejects changed duplicates', () => {
|
|
104
|
+
request('action-terminal');
|
|
105
|
+
database.reportHostAction({
|
|
106
|
+
actionId: 'action-terminal',
|
|
107
|
+
projectRoot,
|
|
108
|
+
report: { exitCode: 0 },
|
|
109
|
+
evidenceHash: 'execution-hash',
|
|
110
|
+
});
|
|
111
|
+
const input = {
|
|
112
|
+
actionId: 'action-terminal',
|
|
113
|
+
projectRoot,
|
|
114
|
+
report: { summary: 'passed', exitCode: 0 },
|
|
115
|
+
evidenceHash: 'terminal-hash',
|
|
116
|
+
};
|
|
117
|
+
const terminal = database.verifyHostAction(input);
|
|
118
|
+
const before = eventCount('action-terminal');
|
|
119
|
+
|
|
120
|
+
expect(database.verifyHostAction({
|
|
121
|
+
...input,
|
|
122
|
+
report: { exitCode: 0, summary: 'passed' },
|
|
123
|
+
})).toEqual(terminal);
|
|
124
|
+
expect(eventCount('action-terminal')).toBe(before);
|
|
125
|
+
expect(() => database.verifyHostAction({
|
|
126
|
+
...input,
|
|
127
|
+
evidenceHash: 'changed-hash',
|
|
128
|
+
})).toThrow('HOST_ACTION_TERMINAL_CONFLICT:action-terminal');
|
|
129
|
+
expect(() => database.verifyHostAction({
|
|
130
|
+
...input,
|
|
131
|
+
report: { summary: 'different', exitCode: 0 },
|
|
132
|
+
})).toThrow('HOST_ACTION_TERMINAL_CONFLICT:action-terminal');
|
|
133
|
+
expect(eventCount('action-terminal')).toBe(before);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it.each(['failed', 'cancelled', 'degraded'] as const)(
|
|
137
|
+
'reaches %s with a completion timestamp',
|
|
138
|
+
outcome => {
|
|
139
|
+
const actionId = `action-${outcome}`;
|
|
140
|
+
request(actionId, outcome);
|
|
141
|
+
const terminal = database.failHostAction({
|
|
142
|
+
actionId,
|
|
143
|
+
projectRoot,
|
|
144
|
+
outcome,
|
|
145
|
+
report: { reason: `${outcome} by host` },
|
|
146
|
+
evidenceHash: `${outcome}-hash`,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(terminal.state).toBe(outcome);
|
|
150
|
+
expect(terminal.finishedAt).toEqual(expect.any(Number));
|
|
151
|
+
expect(eventCount(actionId)).toBe(2);
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
it('rejects request identity conflicts instead of merging metadata', () => {
|
|
156
|
+
request('action-identity');
|
|
157
|
+
const identical = request('action-identity');
|
|
158
|
+
expect(identical.actionId).toBe('action-identity');
|
|
159
|
+
expect(eventCount('action-identity')).toBe(1);
|
|
160
|
+
|
|
161
|
+
expect(() => database.requestHostAction({
|
|
162
|
+
actionId: 'different-action',
|
|
163
|
+
runId: 'telemetry-run-1',
|
|
164
|
+
engineRunId: 'different-engine-run',
|
|
165
|
+
stepId: 'execute',
|
|
166
|
+
projectRoot,
|
|
167
|
+
sessionId: 'different-session',
|
|
168
|
+
executionId: 'execution-1',
|
|
169
|
+
contextReceipt: 'context-1',
|
|
170
|
+
})).toThrow('HOST_ACTION_IDENTITY_CONFLICT:telemetry-run-1:execute');
|
|
171
|
+
expect(database.all('SELECT action_id FROM devflow_host_actions')).toHaveLength(1);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('scopes action and run reads by project root', () => {
|
|
175
|
+
request('action-isolated');
|
|
176
|
+
|
|
177
|
+
expect(database.getHostAction('/project-b', 'action-isolated')).toBeNull();
|
|
178
|
+
expect(database.listHostActionsForRun('/project-b', 'telemetry-run-1')).toEqual([]);
|
|
179
|
+
expect(database.listHostActionsForRun(projectRoot, 'telemetry-run-1')).toHaveLength(1);
|
|
180
|
+
expect(() => database.startHostAction({
|
|
181
|
+
actionId: 'action-isolated',
|
|
182
|
+
projectRoot: '/project-b',
|
|
183
|
+
})).toThrow('HOST_ACTION_NOT_FOUND:action-isolated');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('keeps rows and schema stable across close and reopen', () => {
|
|
187
|
+
request('action-durable');
|
|
188
|
+
database.reportHostAction({
|
|
189
|
+
actionId: 'action-durable',
|
|
190
|
+
projectRoot,
|
|
191
|
+
report: { result: 'observed' },
|
|
192
|
+
evidenceHash: 'durable-hash',
|
|
193
|
+
});
|
|
194
|
+
const schemaBefore = database.all(`
|
|
195
|
+
SELECT type, name, sql FROM sqlite_master
|
|
196
|
+
WHERE name LIKE '%host_action%'
|
|
197
|
+
ORDER BY type, name
|
|
198
|
+
`);
|
|
199
|
+
database.close();
|
|
200
|
+
database = new DevFlowDatabase(directory);
|
|
201
|
+
|
|
202
|
+
expect(database.getHostAction(projectRoot, 'action-durable')).toMatchObject({
|
|
203
|
+
state: 'reported',
|
|
204
|
+
report: { result: 'observed' },
|
|
205
|
+
evidenceHash: 'durable-hash',
|
|
206
|
+
});
|
|
207
|
+
expect(eventCount('action-durable')).toBe(2);
|
|
208
|
+
expect(database.all(`
|
|
209
|
+
SELECT type, name, sql FROM sqlite_master
|
|
210
|
+
WHERE name LIKE '%host_action%'
|
|
211
|
+
ORDER BY type, name
|
|
212
|
+
`)).toEqual(schemaBefore);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('uses UUID text event IDs and enforces append-only event rows', () => {
|
|
216
|
+
request('action-events');
|
|
217
|
+
const [event] = database.all(`
|
|
218
|
+
SELECT event_id FROM devflow_host_action_events WHERE action_id = ?
|
|
219
|
+
`, 'action-events') as Array<{ event_id: string }>;
|
|
220
|
+
|
|
221
|
+
expect(event?.event_id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
|
|
222
|
+
expect(() => database.all(
|
|
223
|
+
'UPDATE devflow_host_action_events SET evidence_hash = ? WHERE event_id = ?',
|
|
224
|
+
'changed',
|
|
225
|
+
event!.event_id,
|
|
226
|
+
)).toThrow('append-only');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('rejects INSERT OR REPLACE attempts without mutating an event', () => {
|
|
230
|
+
request('action-replace');
|
|
231
|
+
const [event] = database.all(`
|
|
232
|
+
SELECT * FROM devflow_host_action_events WHERE action_id = ?
|
|
233
|
+
`, 'action-replace') as Array<Record<string, unknown>>;
|
|
234
|
+
|
|
235
|
+
expect(() => database.all(`
|
|
236
|
+
INSERT OR REPLACE INTO devflow_host_action_events (
|
|
237
|
+
event_id, action_id, project_root, run_id, step_id, from_state,
|
|
238
|
+
to_state, report_json, evidence_hash, created_at
|
|
239
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
240
|
+
`,
|
|
241
|
+
event!.event_id,
|
|
242
|
+
event!.action_id,
|
|
243
|
+
event!.project_root,
|
|
244
|
+
event!.run_id,
|
|
245
|
+
event!.step_id,
|
|
246
|
+
event!.from_state,
|
|
247
|
+
event!.to_state,
|
|
248
|
+
event!.report_json,
|
|
249
|
+
'replacement-hash',
|
|
250
|
+
event!.created_at,
|
|
251
|
+
)).toThrow('append-only');
|
|
252
|
+
|
|
253
|
+
expect(database.get(
|
|
254
|
+
'SELECT evidence_hash FROM devflow_host_action_events WHERE event_id = ?',
|
|
255
|
+
event!.event_id,
|
|
256
|
+
)).toMatchObject({ evidence_hash: null });
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it.each([
|
|
260
|
+
['undefined', { value: undefined }],
|
|
261
|
+
['NaN', { value: Number.NaN }],
|
|
262
|
+
['Infinity', { value: Number.POSITIVE_INFINITY }],
|
|
263
|
+
['negative zero', { value: -0 }],
|
|
264
|
+
['function', { value: () => undefined }],
|
|
265
|
+
['bigint', { value: 1n }],
|
|
266
|
+
['symbol', { value: Symbol('value') }],
|
|
267
|
+
['non-plain object', { value: new Date(0) }],
|
|
268
|
+
['sparse array', { value: Array(1) }],
|
|
269
|
+
])('rejects lossy %s report values', (_name, report) => {
|
|
270
|
+
request(`action-json-${_name}`);
|
|
271
|
+
expect(() => database.reportHostAction({
|
|
272
|
+
actionId: `action-json-${_name}`,
|
|
273
|
+
projectRoot,
|
|
274
|
+
report,
|
|
275
|
+
evidenceHash: 'report-hash',
|
|
276
|
+
})).toThrow('Host action report contains a non-JSON value');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('rejects cyclic reports', () => {
|
|
280
|
+
request('action-json-cycle');
|
|
281
|
+
const report: Record<string, unknown> = {};
|
|
282
|
+
report.self = report;
|
|
283
|
+
|
|
284
|
+
expect(() => database.reportHostAction({
|
|
285
|
+
actionId: 'action-json-cycle',
|
|
286
|
+
projectRoot,
|
|
287
|
+
report,
|
|
288
|
+
evidenceHash: 'report-hash',
|
|
289
|
+
})).toThrow('Host action report contains a cycle');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it.each(['not-json', '[]'])(
|
|
293
|
+
'fails closed when persisted report JSON is corrupt: %s',
|
|
294
|
+
reportJson => {
|
|
295
|
+
request('action-corrupt');
|
|
296
|
+
database.all(
|
|
297
|
+
'UPDATE devflow_host_actions SET report_json = ? WHERE action_id = ?',
|
|
298
|
+
reportJson,
|
|
299
|
+
'action-corrupt',
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
expect(() => database.getHostAction(projectRoot, 'action-corrupt'))
|
|
303
|
+
.toThrow('Invalid persisted host action report_json');
|
|
304
|
+
},
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
it('requires non-empty verification evidence and report content', () => {
|
|
308
|
+
request('action-verification-input');
|
|
309
|
+
database.reportHostAction({
|
|
310
|
+
actionId: 'action-verification-input',
|
|
311
|
+
projectRoot,
|
|
312
|
+
report: { output: 'observed' },
|
|
313
|
+
evidenceHash: 'execution-hash',
|
|
314
|
+
});
|
|
315
|
+
const baseInput = {
|
|
316
|
+
actionId: 'action-verification-input',
|
|
317
|
+
projectRoot,
|
|
318
|
+
report: { command: 'npm test', exitCode: 0 },
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
expect(() => database.verifyHostAction({
|
|
322
|
+
...baseInput,
|
|
323
|
+
evidenceHash: ' ',
|
|
324
|
+
})).toThrow('Host action verification requires a non-empty evidence hash');
|
|
325
|
+
expect(() => database.verifyHostAction(baseInput as VerifyHostActionInput))
|
|
326
|
+
.toThrow('Host action verification requires a non-empty evidence hash');
|
|
327
|
+
expect(() => database.verifyHostAction({
|
|
328
|
+
...baseInput,
|
|
329
|
+
report: {},
|
|
330
|
+
evidenceHash: 'verification-hash',
|
|
331
|
+
})).toThrow('Host action verification requires a non-empty report');
|
|
332
|
+
expect(() => database.failHostAction({
|
|
333
|
+
...baseInput,
|
|
334
|
+
outcome: 'verified',
|
|
335
|
+
} as unknown as FailHostActionInput)).toThrow('Invalid host action failure outcome');
|
|
336
|
+
expect(database.getHostAction(projectRoot, 'action-verification-input')?.state).toBe('reported');
|
|
337
|
+
expect(eventCount('action-verification-input')).toBe(2);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('waits for a competing writer and resolves the committed request idempotently', async () => {
|
|
341
|
+
const worker = new Worker(`
|
|
342
|
+
const { parentPort, workerData } = require('node:worker_threads');
|
|
343
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
344
|
+
const db = new DatabaseSync(workerData.dbPath);
|
|
345
|
+
db.exec('PRAGMA busy_timeout = 5000');
|
|
346
|
+
db.exec('BEGIN IMMEDIATE');
|
|
347
|
+
const now = Date.now();
|
|
348
|
+
db.prepare(
|
|
349
|
+
'INSERT INTO devflow_host_actions (' +
|
|
350
|
+
'action_id, run_id, engine_run_id, step_id, project_root, session_id, ' +
|
|
351
|
+
'execution_id, context_receipt, state, report_json, evidence_hash, ' +
|
|
352
|
+
'created_at, updated_at, finished_at' +
|
|
353
|
+
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'waiting', '{}', NULL, ?, ?, NULL)",
|
|
354
|
+
).run(
|
|
355
|
+
'action-contended', 'telemetry-run-1', 'engine-run-1', 'execute', '/project-a',
|
|
356
|
+
'session-1', 'execution-1', 'context-1', now, now,
|
|
357
|
+
);
|
|
358
|
+
db.prepare(
|
|
359
|
+
'INSERT INTO devflow_host_action_events (' +
|
|
360
|
+
'event_id, action_id, project_root, run_id, step_id, from_state, ' +
|
|
361
|
+
'to_state, report_json, evidence_hash, created_at' +
|
|
362
|
+
") VALUES (?, ?, ?, ?, ?, NULL, 'waiting', '{}', NULL, ?)",
|
|
363
|
+
).run(
|
|
364
|
+
'worker-event-contended', 'action-contended', '/project-a',
|
|
365
|
+
'telemetry-run-1', 'execute', now,
|
|
366
|
+
);
|
|
367
|
+
parentPort.postMessage('locked');
|
|
368
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
|
|
369
|
+
db.exec('COMMIT');
|
|
370
|
+
db.close();
|
|
371
|
+
parentPort.postMessage('committed');
|
|
372
|
+
`, {
|
|
373
|
+
eval: true,
|
|
374
|
+
workerData: { dbPath: join(directory, '.devflow', 'devflow.db') },
|
|
375
|
+
});
|
|
376
|
+
let duplicate: ReturnType<typeof request> | undefined;
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
await new Promise<void>((resolve, reject) => {
|
|
380
|
+
worker.on('message', message => {
|
|
381
|
+
if (message === 'locked') {
|
|
382
|
+
try {
|
|
383
|
+
duplicate = request('action-contended');
|
|
384
|
+
} catch (error) {
|
|
385
|
+
reject(error);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (message === 'committed') resolve();
|
|
389
|
+
});
|
|
390
|
+
worker.on('error', reject);
|
|
391
|
+
worker.on('exit', code => {
|
|
392
|
+
if (code !== 0) reject(new Error(`Host-action lock worker exited with code ${code}`));
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
expect(duplicate).toMatchObject({
|
|
397
|
+
actionId: 'action-contended',
|
|
398
|
+
state: 'waiting',
|
|
399
|
+
});
|
|
400
|
+
expect(eventCount('action-contended')).toBe(1);
|
|
401
|
+
} finally {
|
|
402
|
+
await worker.terminate();
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
});
|
package/dist/database.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { EnqueueWorkInput, LeaseWorkInput, RequestSessionClosureInput, SessionClosureRecord, WorkError, WorkItemRecord, WorkQueueHealth } from './work-queue';
|
|
2
2
|
import { type SessionObligationRecord, type SessionObligationState } from './obligation-ledger';
|
|
3
|
+
import { type FailHostActionInput, type HostActionRecord, type ReportHostActionInput, type RequestHostActionInput, type StartHostActionInput, type VerifyHostActionInput } from './host-actions';
|
|
3
4
|
export interface BenchmarkReportRecord {
|
|
4
5
|
runId: string;
|
|
5
6
|
suiteId: string;
|
|
@@ -400,6 +401,19 @@ export declare class DevFlowDatabase {
|
|
|
400
401
|
private mapMemoryTurn;
|
|
401
402
|
recordMemoryDistillCheckpoint(checkpoint: MemoryDistillCheckpointRecord): void;
|
|
402
403
|
listMemoryDistillCheckpoints(projectRoot: string, limit?: number): MemoryDistillCheckpointRecord[];
|
|
404
|
+
requestHostAction(input: RequestHostActionInput): HostActionRecord;
|
|
405
|
+
startHostAction(input: StartHostActionInput): HostActionRecord;
|
|
406
|
+
reportHostAction(input: ReportHostActionInput): HostActionRecord;
|
|
407
|
+
verifyHostAction(input: VerifyHostActionInput): HostActionRecord;
|
|
408
|
+
failHostAction(input: FailHostActionInput): HostActionRecord;
|
|
409
|
+
getHostAction(projectRoot: string, actionId: string): HostActionRecord | null;
|
|
410
|
+
listHostActionsForRun(projectRoot: string, runId: string): HostActionRecord[];
|
|
411
|
+
private transitionHostAction;
|
|
412
|
+
private appendHostActionEvent;
|
|
413
|
+
private validateHostActionIdentity;
|
|
414
|
+
private withImmediateTransaction;
|
|
415
|
+
private isTerminalHostActionState;
|
|
416
|
+
private isLegalHostActionTransition;
|
|
403
417
|
all(sql: string, ...params: unknown[]): unknown[];
|
|
404
418
|
get(sql: string, ...params: unknown[]): unknown;
|
|
405
419
|
close(): void;
|