start-command 0.42.0 → 0.44.0
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/.changeset/README.md +1 -1
- package/CHANGELOG.md +26 -0
- package/README.md +2 -0
- package/package.json +6 -6
- package/src/lib/args-parser-queries.js +64 -3
- package/src/lib/docker-inspect-batch.js +3 -0
- package/src/lib/execution-attach.js +10 -1
- package/src/lib/execution-cleanup.js +8 -0
- package/src/lib/execution-control.js +14 -3
- package/src/lib/execution-pause.js +701 -0
- package/src/lib/execution-recovery.js +6 -0
- package/src/lib/execution-resume-all.js +14 -0
- package/src/lib/execution-resume.js +16 -1
- package/src/lib/execution-store.js +5 -1
- package/src/lib/formal-ai-client.js +8 -5
- package/src/lib/formal-ai-files.js +474 -61
- package/src/lib/formal-ai.js +170 -7
- package/src/lib/log-sanitizer.js +5 -0
- package/src/lib/query-commands.js +33 -1
- package/src/lib/session-probe.js +27 -1
- package/src/lib/status-formatter.js +13 -3
- package/src/lib/status-probe.js +19 -5
- package/src/lib/usage.js +8 -1
- package/test/args-parser-attach-resume.js +1 -1
- package/test/args-parser-control.js +3 -3
- package/test/args-parser.js +1 -1
- package/test/fixtures/formal-ai/receipts-258.json +27 -9
- package/test/fixtures/formal-ai/receipts-261.json +589 -0
- package/test/fixtures/formal-ai/replay.cjs +4 -1
- package/test/fixtures/pause-golden.cjs +142 -0
- package/test/regression-243.js +4 -1
- package/test/regression-258-files.js +27 -5
- package/test/regression-258.js +22 -9
- package/test/regression-261.js +856 -0
- package/test/regression-263.js +867 -0
- package/test/shared-golden.js +28 -3
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #263: runs one `pause-control` shared golden case
|
|
3
|
+
* (parity/fixtures/contracts.json) through the JavaScript `--pause` /
|
|
4
|
+
* `--unpause` control path. rust/tests/shared_golden.rs runs the same cases
|
|
5
|
+
* through the Rust path, so both runtimes are held to identical reports,
|
|
6
|
+
* errors, docker argv and persisted records.
|
|
7
|
+
*
|
|
8
|
+
* The in-memory docker follows the rules spelled out in
|
|
9
|
+
* `pause-control.docker` of the fixture file; keep it in step with
|
|
10
|
+
* `GoldenDocker` in rust/tests/shared_golden.rs.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const TIMESTAMP =
|
|
14
|
+
/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})/g;
|
|
15
|
+
|
|
16
|
+
/** Replace every ISO-8601 timestamp, which differs per run, with a marker. */
|
|
17
|
+
function normalize(value) {
|
|
18
|
+
if (typeof value === 'string') {
|
|
19
|
+
return value.replace(TIMESTAMP, '<timestamp>');
|
|
20
|
+
}
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
return value.map(normalize);
|
|
23
|
+
}
|
|
24
|
+
if (value && typeof value === 'object') {
|
|
25
|
+
return Object.fromEntries(
|
|
26
|
+
Object.entries(value).map(([key, entry]) => [key, normalize(entry)])
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function goldenDocker(initial, name) {
|
|
33
|
+
const docker = { state: initial, calls: [] };
|
|
34
|
+
const ok = (stdout = '') => ({
|
|
35
|
+
success: true,
|
|
36
|
+
stdout,
|
|
37
|
+
stderr: '',
|
|
38
|
+
status: 0,
|
|
39
|
+
error: null,
|
|
40
|
+
});
|
|
41
|
+
const fail = (stderr) => ({
|
|
42
|
+
success: false,
|
|
43
|
+
stdout: '',
|
|
44
|
+
stderr,
|
|
45
|
+
status: 1,
|
|
46
|
+
error: null,
|
|
47
|
+
});
|
|
48
|
+
docker.runner = (command, args, extra = {}) => {
|
|
49
|
+
docker.calls.push({ args: [...args], bounded: Boolean(extra.timeout) });
|
|
50
|
+
const verb = args[0];
|
|
51
|
+
if (docker.state === 'removed' && verb !== 'rm') {
|
|
52
|
+
return fail(`Error response from daemon: No such container: ${name}`);
|
|
53
|
+
}
|
|
54
|
+
const alive = docker.state === 'running' || docker.state === 'paused';
|
|
55
|
+
switch (verb) {
|
|
56
|
+
case 'inspect': {
|
|
57
|
+
const format = args[args.indexOf('-f') + 1];
|
|
58
|
+
if (format === '{{.State.Status}} {{.State.Paused}}') {
|
|
59
|
+
return ok(`${docker.state} ${docker.state === 'paused'}\n`);
|
|
60
|
+
}
|
|
61
|
+
if (format === '{{.State.Status}}') {
|
|
62
|
+
return ok(`${docker.state}\n`);
|
|
63
|
+
}
|
|
64
|
+
if (format === '{{.Id}} {{.State.Pid}}') {
|
|
65
|
+
return ok(`abc263def ${alive ? 4242 : 0}\n`);
|
|
66
|
+
}
|
|
67
|
+
return ok('');
|
|
68
|
+
}
|
|
69
|
+
case 'pause':
|
|
70
|
+
if (docker.state !== 'running') {
|
|
71
|
+
return fail(`container ${name} is not running`);
|
|
72
|
+
}
|
|
73
|
+
docker.state = 'paused';
|
|
74
|
+
return ok(`${name}\n`);
|
|
75
|
+
case 'unpause':
|
|
76
|
+
if (docker.state !== 'paused') {
|
|
77
|
+
return fail(`container ${name} is not paused`);
|
|
78
|
+
}
|
|
79
|
+
docker.state = 'running';
|
|
80
|
+
return ok(`${name}\n`);
|
|
81
|
+
default:
|
|
82
|
+
return ok('');
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
return docker;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Run one case and return its normalized observable result.
|
|
90
|
+
* @param {{ExecutionRecord: Function, pause: object}} modules - Code under test
|
|
91
|
+
* @param {object} store - An empty ExecutionStore
|
|
92
|
+
* @param {object} section - `pause-control` from the contracts file
|
|
93
|
+
* @param {object} fixture - One of `section.cases`
|
|
94
|
+
* @returns {object} The value `fixture.expected` must equal
|
|
95
|
+
*/
|
|
96
|
+
function runPauseCase({ ExecutionRecord, pause }, store, section, fixture) {
|
|
97
|
+
const base = section.record;
|
|
98
|
+
store.save(
|
|
99
|
+
ExecutionRecord.fromObject({
|
|
100
|
+
...base,
|
|
101
|
+
status: fixture.status || base.status,
|
|
102
|
+
exitCode:
|
|
103
|
+
fixture.exitCode === undefined ? base.exitCode : fixture.exitCode,
|
|
104
|
+
options: { ...base.options, ...(fixture.options || {}) },
|
|
105
|
+
})
|
|
106
|
+
);
|
|
107
|
+
const docker = goldenDocker(fixture.docker, base.options.sessionName);
|
|
108
|
+
const deps = {
|
|
109
|
+
runner: docker.runner,
|
|
110
|
+
outputFormat: fixture.outputFormat,
|
|
111
|
+
reason: fixture.reason,
|
|
112
|
+
by: fixture.by,
|
|
113
|
+
};
|
|
114
|
+
const result =
|
|
115
|
+
fixture.action === 'pause'
|
|
116
|
+
? pause.pauseExecution(store, fixture.identifier, deps)
|
|
117
|
+
: pause.unpauseExecution(store, fixture.identifier, deps);
|
|
118
|
+
const saved = store.get(base.uuid).toObject();
|
|
119
|
+
let output = result.output === undefined ? null : result.output;
|
|
120
|
+
if (output !== null && fixture.outputFormat === 'json') {
|
|
121
|
+
output = JSON.parse(output);
|
|
122
|
+
}
|
|
123
|
+
return normalize({
|
|
124
|
+
success: result.success,
|
|
125
|
+
output,
|
|
126
|
+
error: result.error === undefined ? null : result.error,
|
|
127
|
+
dockerCalls: docker.calls.map((call) => call.args),
|
|
128
|
+
unboundedDockerCalls: docker.calls
|
|
129
|
+
.filter((call) => !call.bounded)
|
|
130
|
+
.map((call) => call.args),
|
|
131
|
+
dockerState: docker.state,
|
|
132
|
+
record: {
|
|
133
|
+
status: saved.status,
|
|
134
|
+
exitCode: saved.exitCode === undefined ? null : saved.exitCode,
|
|
135
|
+
containerState:
|
|
136
|
+
saved.containerState === undefined ? null : saved.containerState,
|
|
137
|
+
options: saved.options,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = { normalize, goldenDocker, runPauseCase };
|
package/test/regression-243.js
CHANGED
|
@@ -82,6 +82,9 @@ const CONTAINERS = [
|
|
|
82
82
|
'.State.OOMKilled': String(oom),
|
|
83
83
|
'.State.StartedAt': startedAt || '0001-01-01T00:00:00Z',
|
|
84
84
|
'.State.FinishedAt': finishedAt || '0001-01-01T00:00:00Z',
|
|
85
|
+
// Paused state for --status/--list (issue #263).
|
|
86
|
+
'.State.Status': running ? 'running' : 'exited',
|
|
87
|
+
'.State.Paused': 'false',
|
|
85
88
|
}));
|
|
86
89
|
|
|
87
90
|
/**
|
|
@@ -290,7 +293,7 @@ posix('issue #243: one docker inspect per query, same output', () => {
|
|
|
290
293
|
});
|
|
291
294
|
|
|
292
295
|
describe('issue #243: batched inspect answers', () => {
|
|
293
|
-
const row = (name, id) => `/${name} ${id} 7 true 0 false s f`;
|
|
296
|
+
const row = (name, id) => `/${name} ${id} 7 true 0 false s f running false`;
|
|
294
297
|
|
|
295
298
|
it('matches lines by name or ID prefix, not by order', () => {
|
|
296
299
|
const answers = parseBatchInspect(
|
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
EXIT,
|
|
12
12
|
} = require('../src/lib/formal-ai-policy');
|
|
13
13
|
const files = require('../src/lib/formal-ai-files');
|
|
14
|
+
const { readFrame, readFrameText } = files;
|
|
14
15
|
const { runFormalAiTask } = require('../src/lib/formal-ai');
|
|
15
16
|
const {
|
|
16
17
|
cleanup,
|
|
@@ -144,11 +145,14 @@ describe('read (#258)', () => {
|
|
|
144
145
|
const cwd = workspace();
|
|
145
146
|
const ctx = context(cwd, 'read-only');
|
|
146
147
|
const receipt = call('read', { path: './a.js' }, ctx);
|
|
148
|
+
// Formal AI binds a read as complete only in its numbered frame (#261).
|
|
147
149
|
expect(receipt).toMatchObject({
|
|
148
150
|
exitCode: 0,
|
|
149
|
-
|
|
151
|
+
text: TEXT,
|
|
152
|
+
result: readFrame(TEXT),
|
|
150
153
|
bytes: Buffer.byteLength(TEXT),
|
|
151
154
|
});
|
|
155
|
+
expect(readFrameText(receipt.result)).toBe(TEXT);
|
|
152
156
|
expect(ctx.observed.has('a.js')).toBe(true);
|
|
153
157
|
expect(ctx.contents.get('a.js')).toBe(receipt.sha256);
|
|
154
158
|
});
|
|
@@ -198,10 +202,16 @@ describe('edit (#258)', () => {
|
|
|
198
202
|
);
|
|
199
203
|
expect(receipt).toMatchObject({
|
|
200
204
|
exitCode: 0,
|
|
201
|
-
result: '',
|
|
202
205
|
replacements: 1,
|
|
203
206
|
verification: { verified: true },
|
|
204
207
|
});
|
|
208
|
+
expect(JSON.parse(receipt.result)).toMatchObject({
|
|
209
|
+
ok: true,
|
|
210
|
+
path: 'a.js',
|
|
211
|
+
tool: 'edit',
|
|
212
|
+
replacements: 1,
|
|
213
|
+
verified: true,
|
|
214
|
+
});
|
|
205
215
|
expect(fs.readFileSync(file, 'utf8')).toBe(
|
|
206
216
|
TEXT.replace('const b = 2;', () => 'const b = $&3;')
|
|
207
217
|
);
|
|
@@ -215,7 +225,7 @@ describe('edit (#258)', () => {
|
|
|
215
225
|
it('keeps a byte order mark', () => {
|
|
216
226
|
const cwd = workspace({ 'bom.txt': '\ufefffirst\nsecond\n' });
|
|
217
227
|
const ctx = context(cwd);
|
|
218
|
-
expect(call('read', { path: 'bom.txt' }, ctx).
|
|
228
|
+
expect(call('read', { path: 'bom.txt' }, ctx).text).toBe(
|
|
219
229
|
'\ufefffirst\nsecond\n'
|
|
220
230
|
);
|
|
221
231
|
const receipt = call(
|
|
@@ -420,10 +430,22 @@ describe('file tools in the task loop (#258)', () => {
|
|
|
420
430
|
},
|
|
421
431
|
{ content: 'The edit failed.' },
|
|
422
432
|
]);
|
|
433
|
+
// The failure stays unresolved with its original error (#261).
|
|
423
434
|
expect(result.evidence.outcome).toMatchObject({
|
|
424
|
-
status: '
|
|
435
|
+
status: 'incomplete',
|
|
425
436
|
exitCode: 1,
|
|
426
|
-
remainingObligations: [
|
|
437
|
+
remainingObligations: [
|
|
438
|
+
'`edit a.js` failed: a.js: old_string was not found; nothing changed',
|
|
439
|
+
],
|
|
440
|
+
failures: [
|
|
441
|
+
{
|
|
442
|
+
command: 'edit a.js',
|
|
443
|
+
tool: 'edit',
|
|
444
|
+
target: 'a.js',
|
|
445
|
+
exitCode: 1,
|
|
446
|
+
error: 'a.js: old_string was not found; nothing changed',
|
|
447
|
+
},
|
|
448
|
+
],
|
|
427
449
|
});
|
|
428
450
|
expect(result.after).toEqual(result.before);
|
|
429
451
|
});
|
package/test/regression-258.js
CHANGED
|
@@ -30,6 +30,7 @@ const {
|
|
|
30
30
|
recognizeCapabilityGap,
|
|
31
31
|
} = require('../src/lib/formal-ai-client');
|
|
32
32
|
const { runFormalAiTask } = require('../src/lib/formal-ai');
|
|
33
|
+
const { readFrame } = require('../src/lib/formal-ai-files');
|
|
33
34
|
const { createFakeServer, edits } = require('./fixtures/formal-ai/replay.cjs');
|
|
34
35
|
const {
|
|
35
36
|
CLI,
|
|
@@ -273,11 +274,22 @@ describe('structured file tools against captured receipts (#258)', () => {
|
|
|
273
274
|
'reads, edits and reads back, preserving all other bytes',
|
|
274
275
|
async () => {
|
|
275
276
|
const result = await run(LITERAL_EDIT, { effects: 'workspace-write' });
|
|
277
|
+
// start verified the edit byte for byte, but Formal AI's final answer
|
|
278
|
+
// is a gap for the clause "Preserve all other content.": that is never
|
|
279
|
+
// reported as done (#261).
|
|
276
280
|
expect(result.outcome).toMatchObject({
|
|
277
|
-
status: '
|
|
278
|
-
exitCode:
|
|
279
|
-
message:
|
|
280
|
-
|
|
281
|
+
status: 'incomplete',
|
|
282
|
+
exitCode: EXIT.UNSUPPORTED,
|
|
283
|
+
message:
|
|
284
|
+
'Formal AI reported obligation_gap (no_artifact_in_clause) for the clause "Preserve all other content."; nothing was verified as done by Formal AI; changed snippet.mjs',
|
|
285
|
+
remainingObligations: [
|
|
286
|
+
'Formal AI did not confirm the requested change',
|
|
287
|
+
],
|
|
288
|
+
gapRecord: {
|
|
289
|
+
schema: 'obligation_gap',
|
|
290
|
+
reason: 'no_artifact_in_clause',
|
|
291
|
+
clause: 'Preserve all other content.',
|
|
292
|
+
},
|
|
281
293
|
});
|
|
282
294
|
expect(result.text).toBe(EDITED);
|
|
283
295
|
const [read, edit, check] = result.tools;
|
|
@@ -304,9 +316,9 @@ describe('structured file tools against captured receipts (#258)', () => {
|
|
|
304
316
|
command: 'sha256sum -- snippet.mjs',
|
|
305
317
|
exitCode: 0,
|
|
306
318
|
});
|
|
307
|
-
// Formal AI received the exact file text and the readback hash.
|
|
319
|
+
// Formal AI received the exact file text (framed) and the readback hash.
|
|
308
320
|
const toolMessages = result.sent[3].filter((m) => m.role === 'tool');
|
|
309
|
-
expect(toolMessages[0].content).toBe(ORIGINAL);
|
|
321
|
+
expect(toolMessages[0].content).toBe(readFrame(ORIGINAL));
|
|
310
322
|
expect(toolMessages[2].content).toContain(edit.sha256);
|
|
311
323
|
expect(result.evidence.effects).toMatchObject({
|
|
312
324
|
changed: ['snippet.mjs'],
|
|
@@ -398,7 +410,7 @@ describe('edit requests through the CLI (#258)', () => {
|
|
|
398
410
|
const result = cli(LITERAL_EDIT, {
|
|
399
411
|
START_FORMAL_AI_EFFECTS: 'workspace-write',
|
|
400
412
|
});
|
|
401
|
-
expect(result.status).toBe(
|
|
413
|
+
expect(result.status).toBe(EXIT.UNSUPPORTED);
|
|
402
414
|
expect(result.text).toBe(EDITED);
|
|
403
415
|
expect(result.stderr).toContain('[formal-ai] edit snippet.mjs');
|
|
404
416
|
expect(result.stderr).toContain(
|
|
@@ -411,8 +423,9 @@ describe('edit requests through the CLI (#258)', () => {
|
|
|
411
423
|
'write',
|
|
412
424
|
]);
|
|
413
425
|
expect(result.evidence.outcome).toMatchObject({
|
|
414
|
-
status: '
|
|
415
|
-
exitCode:
|
|
426
|
+
status: 'incomplete',
|
|
427
|
+
exitCode: EXIT.UNSUPPORTED,
|
|
428
|
+
gapRecord: { schema: 'obligation_gap' },
|
|
416
429
|
});
|
|
417
430
|
});
|
|
418
431
|
|