deadbyte-mcp 0.15.13 → 0.15.15
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/MANIFEST.SHA256 +20 -14
- package/PROVENANCE.json +1 -1
- package/README.txt +5 -5
- package/RELEASE.json +53 -53
- package/package.json +1 -1
- package/scripts/npm-release-r47.mjs +21 -0
- package/src/coding-task-runtime.mjs +6 -1
- package/src/observation-output-contracts.mjs +9 -8
- package/src/observation-runtime.mjs +47 -17
- package/src/operation-history.mjs +95 -0
- package/src/operation-lifecycle.mjs +63 -0
- package/src/process-output-contracts.mjs +24 -1
- package/src/process-readiness.mjs +120 -0
- package/src/process-runtime.mjs +178 -6
- package/src/release-slot.mjs +294 -293
- package/src/version.mjs +1 -1
- package/test/observation-runtime.test.mjs +28 -0
- package/test/operation-history.test.mjs +61 -0
- package/test/operation-lifecycle.test.mjs +72 -0
- package/test/process-readiness.test.mjs +32 -0
- package/test/process-runtime.test.mjs +89 -0
package/src/version.mjs
CHANGED
|
@@ -148,3 +148,31 @@ test('R46 observation search hard cap fails closed while all retained sessions a
|
|
|
148
148
|
await waitTerminal(f.runtime,a.search_id).catch(()=>{});
|
|
149
149
|
await waitTerminal(f.runtime,b.search_id).catch(()=>{});
|
|
150
150
|
});
|
|
151
|
+
test('R46 search cursor rejects tampering, wrong search, rewind, and expired snapshot', async () => {
|
|
152
|
+
let now=1000;
|
|
153
|
+
const f=await fixture({searchSessionTtlMs:50,clockMs:()=>now});
|
|
154
|
+
await writeFile(path.join(f.project,'a.txt'),'needle a\nneedle aa\n');
|
|
155
|
+
await writeFile(path.join(f.project,'b.txt'),'needle b\n');
|
|
156
|
+
const first=await f.runtime.searchStart({rootId:'project',relativePath:'.',pattern:'needle',maxResults:10,maxFiles:10,maxBytes:65536});
|
|
157
|
+
await waitTerminal(f.runtime,first.search_id);
|
|
158
|
+
const p1=await f.runtime.searchPage({searchId:first.search_id,limit:1});
|
|
159
|
+
assert.match(p1.results[0].result_id,/^[0-9a-f]{64}$/);
|
|
160
|
+
assert.match(p1.results[0].file_sha256,/^[0-9a-f]{64}$/);
|
|
161
|
+
assert.equal(p1.snapshot_id,first.snapshot_id);
|
|
162
|
+
const p2=await f.runtime.searchPage({searchId:first.search_id,limit:1,cursor:p1.next_cursor});
|
|
163
|
+
assert.equal(p2.results.length,1);
|
|
164
|
+
const p2Retry=await f.runtime.searchPage({searchId:first.search_id,limit:1,cursor:p1.next_cursor});
|
|
165
|
+
assert.deepEqual(p2Retry.results.map(x=>x.result_id),p2.results.map(x=>x.result_id));
|
|
166
|
+
const p3=await f.runtime.searchPage({searchId:first.search_id,limit:1,cursor:p2.next_cursor});
|
|
167
|
+
assert.equal(p3.results.length,1);
|
|
168
|
+
await assert.rejects(f.runtime.searchPage({searchId:first.search_id,limit:1,cursor:p1.next_cursor}),/rewound search cursor rejected/);
|
|
169
|
+
const second=await f.runtime.searchStart({rootId:'project',relativePath:'.',pattern:'needle',maxResults:10,maxFiles:10,maxBytes:65536});
|
|
170
|
+
await waitTerminal(f.runtime,second.search_id);
|
|
171
|
+
await assert.rejects(f.runtime.searchPage({searchId:second.search_id,limit:1,cursor:p1.next_cursor}),/invalid observation cursor|invalid, tampered, rewound, or mismatched search cursor/);
|
|
172
|
+
const raw=Buffer.from(p1.next_cursor,'base64url').toString('utf8');
|
|
173
|
+
const tampered=Buffer.from(raw.slice(0,-1)+(raw.endsWith('a')?'b':'a'),'utf8').toString('base64url');
|
|
174
|
+
await assert.rejects(f.runtime.searchPage({searchId:first.search_id,limit:1,cursor:tampered}),/invalid observation cursor|invalid, tampered, rewound, or mismatched search cursor/);
|
|
175
|
+
now+=60;
|
|
176
|
+
await assert.rejects(f.runtime.searchPage({searchId:first.search_id,limit:1,cursor:p2.next_cursor}),/unknown or expired search|search snapshot expired/);
|
|
177
|
+
});
|
|
178
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createOperationHistory, operationHistoryReceipt } from '../src/operation-history.mjs';
|
|
4
|
+
|
|
5
|
+
test('hash-chain preserves operation continuity', () => {
|
|
6
|
+
const h=createOperationHistory({maxEvents:8,maxBytes:65536,operationId:'op-1'});
|
|
7
|
+
const a=h.append('started',{root_id:'r1'});
|
|
8
|
+
const b=h.append('progress',{n:1});
|
|
9
|
+
assert.equal(b.previous_event_hash,a.event_hash);
|
|
10
|
+
assert.equal(h.verify().ok,true);
|
|
11
|
+
const page=h.read({afterSequence:0,limit:8});
|
|
12
|
+
assert.deepEqual(page.events.map(x=>x.sequence),[1,2]);
|
|
13
|
+
assert.equal(page.next_sequence,2);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('payload mutation after append cannot alter recorded digest', () => {
|
|
17
|
+
const h=createOperationHistory({operationId:'op-2'});
|
|
18
|
+
const payload={value:'before'};
|
|
19
|
+
const e=h.append('input',payload);
|
|
20
|
+
payload.value='after';
|
|
21
|
+
assert.equal(h.verify().ok,true);
|
|
22
|
+
assert.equal(e.payload_digest.length,64);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('event cap creates explicit bounded-retention overflow', () => {
|
|
26
|
+
const h=createOperationHistory({maxEvents:2,maxBytes:65536,operationId:'op-3'});
|
|
27
|
+
h.append('a'); h.append('b'); h.append('c');
|
|
28
|
+
const v=h.verify();
|
|
29
|
+
assert.equal(v.ok,true);
|
|
30
|
+
assert.equal(v.overflow.occurred,true);
|
|
31
|
+
assert.equal(v.overflow.reason,'event_cap');
|
|
32
|
+
assert.equal(v.overflow.dropped,1);
|
|
33
|
+
assert.throws(()=>h.read({afterSequence:0}),/cursor expired/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('byte cap creates explicit overflow boundary', () => {
|
|
37
|
+
const h=createOperationHistory({maxEvents:100,maxBytes:1200,operationId:'op-4'});
|
|
38
|
+
for(let i=0;i<20;i++) h.append('payload',{i,text:'x'.repeat(80)});
|
|
39
|
+
const v=h.verify();
|
|
40
|
+
assert.equal(v.ok,true);
|
|
41
|
+
assert.equal(v.overflow.occurred,true);
|
|
42
|
+
assert.equal(v.overflow.reason,'byte_cap');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('same operation cursor resumes after acknowledged boundary without duplicates', () => {
|
|
46
|
+
const h=createOperationHistory({operationId:'op-5'});
|
|
47
|
+
h.append('a'); h.append('b'); h.append('c');
|
|
48
|
+
const first=h.read({afterSequence:0,limit:2});
|
|
49
|
+
const second=h.read({afterSequence:first.next_sequence,limit:2});
|
|
50
|
+
assert.deepEqual(first.events.map(e=>e.sequence),[1,2]);
|
|
51
|
+
assert.deepEqual(second.events.map(e=>e.sequence),[3]);
|
|
52
|
+
assert.equal(new Set([...first.events,...second.events].map(e=>e.sequence)).size,3);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('receipt exposes evidence head without becoming authority', () => {
|
|
56
|
+
const h=createOperationHistory({operationId:'op-6'});
|
|
57
|
+
h.append('started');
|
|
58
|
+
const receipt=operationHistoryReceipt(h);
|
|
59
|
+
assert.equal(receipt.operation_id,'op-6');
|
|
60
|
+
assert.match(receipt.head_event_hash,/^[0-9a-f]{64}$/);
|
|
61
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createOperationLifecycle } from '../src/operation-lifecycle.mjs';
|
|
4
|
+
|
|
5
|
+
test('lifecycle is monotonic and terminal exactly once', () => {
|
|
6
|
+
const op=createOperationLifecycle({operationId:'a'.repeat(64),clockMs:()=>1234});
|
|
7
|
+
op.emit('start',{kind:'process'});
|
|
8
|
+
op.emit('running',{pid:123});
|
|
9
|
+
op.emit('output',{channel:'stdout',bytes:4,payload_digest:'b'.repeat(64)});
|
|
10
|
+
const terminal=op.terminal('success',{exit_code:0});
|
|
11
|
+
assert.equal(terminal.event.sequence,4);
|
|
12
|
+
assert.equal(op.snapshot().state,'success');
|
|
13
|
+
assert.equal(op.snapshot().terminal.sequence,4);
|
|
14
|
+
assert.throws(()=>op.terminal('failure'),/terminal transition already committed/);
|
|
15
|
+
assert.throws(()=>op.emit('output'),/late operation event rejected/);
|
|
16
|
+
const receipt=op.receipt();
|
|
17
|
+
assert.equal(receipt.terminal_sequence,4);
|
|
18
|
+
assert.equal(receipt.terminal_event_hash,terminal.event.event_hash);
|
|
19
|
+
assert.equal(receipt.head_event_hash,terminal.event.event_hash);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('cancel and timeout terminal outcomes are mutually exclusive', () => {
|
|
23
|
+
const cancelled=createOperationLifecycle({operationId:'c'.repeat(64)});
|
|
24
|
+
cancelled.emit('start'); cancelled.emit('running'); cancelled.emit('cancel_requested',{reason:'operator'});
|
|
25
|
+
cancelled.emit('canceling',{reason:'cancel'});
|
|
26
|
+
const c=cancelled.terminal('cancelled');
|
|
27
|
+
assert.equal(cancelled.snapshot().terminal.type,'cancelled');
|
|
28
|
+
assert.equal(cancelled.receipt().terminal_event_hash,c.event.event_hash);
|
|
29
|
+
const timed=createOperationLifecycle({operationId:'d'.repeat(64)});
|
|
30
|
+
timed.emit('start'); timed.emit('running'); timed.emit('canceling',{reason:'deadline_exceeded'});
|
|
31
|
+
const t=timed.terminal('timed_out');
|
|
32
|
+
assert.equal(timed.snapshot().terminal.type,'timed_out');
|
|
33
|
+
assert.equal(timed.receipt().terminal_event_hash,t.event.event_hash);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('invalid transition and duplicate start fail closed', () => {
|
|
37
|
+
const op=createOperationLifecycle({operationId:'e'.repeat(64)});
|
|
38
|
+
assert.throws(()=>op.emit('running'),/invalid operation transition/);
|
|
39
|
+
op.emit('start');
|
|
40
|
+
assert.throws(()=>op.emit('start'),/duplicate operation start/);
|
|
41
|
+
assert.throws(()=>op.emit('canceling'),/invalid operation transition/);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('producer failure terminal is distinct from cancellation', () => {
|
|
45
|
+
const op=createOperationLifecycle({operationId:'f'.repeat(64)});
|
|
46
|
+
op.emit('start'); op.emit('running'); op.emit('error',{channel:'stderr',bytes:1});
|
|
47
|
+
const terminal=op.terminal('failure',{exit_code:7});
|
|
48
|
+
assert.equal(op.snapshot().terminal.type,'failure');
|
|
49
|
+
assert.equal(terminal.event.sequence,4);
|
|
50
|
+
});
|
|
51
|
+
test('history eviction remains evidence-only and terminal receipt binds the retained head', () => {
|
|
52
|
+
const op=createOperationLifecycle({operationId:'9'.repeat(64),maxEvents:4,maxBytes:65536});
|
|
53
|
+
op.emit('start'); op.emit('running');
|
|
54
|
+
op.emit('output',{bytes:1});
|
|
55
|
+
op.emit('output',{bytes:1});
|
|
56
|
+
op.emit('output',{bytes:1});
|
|
57
|
+
const terminal=op.terminal('success',{exit_code:0});
|
|
58
|
+
const snapshot=op.snapshot();
|
|
59
|
+
assert.equal(snapshot.overflow.occurred,true);
|
|
60
|
+
assert.equal(snapshot.overflow.reason,'event_cap');
|
|
61
|
+
assert.equal(op.receipt().terminal_event_hash,terminal.event.event_hash);
|
|
62
|
+
assert.equal(op.receipt().head_event_hash,terminal.event.event_hash);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('late producer event after terminal is rejected and cannot mutate terminal proof', () => {
|
|
66
|
+
const op=createOperationLifecycle({operationId:'a'.repeat(64)});
|
|
67
|
+
op.emit('start'); op.emit('running');
|
|
68
|
+
const terminal=op.terminal('success',{exit_code:0});
|
|
69
|
+
assert.throws(()=>op.emit('output',{bytes:1}),/late operation event rejected/);
|
|
70
|
+
assert.equal(op.receipt().terminal_event_hash,terminal.event.event_hash);
|
|
71
|
+
});
|
|
72
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {
|
|
4
|
+
analyzeReadiness,
|
|
5
|
+
detectPrompt,
|
|
6
|
+
lineEndingProfile,
|
|
7
|
+
normalizeLineEndings,
|
|
8
|
+
createReadinessTracker
|
|
9
|
+
} from '../src/process-readiness.mjs';
|
|
10
|
+
|
|
11
|
+
const LF=String.fromCharCode(10);
|
|
12
|
+
const CR=String.fromCharCode(13);
|
|
13
|
+
|
|
14
|
+
test('detects prompts without treating arbitrary output as ready', () => {
|
|
15
|
+
assert.deepEqual(detectPrompt('hello'+LF+'>>> '), { profile: 'python', prompt: '>>> ' });
|
|
16
|
+
assert.equal(detectPrompt('hello'+LF+'still running'), null);
|
|
17
|
+
assert.equal(analyzeReadiness({stdout:'hello'+LF+'still running'}).state, 'running');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('distinguishes terminal state from prompt state', () => {
|
|
21
|
+
assert.equal(analyzeReadiness({stdout:'PS C:'+String.fromCharCode(92)+'work> ', state:'running'}).state, 'ready_for_input');
|
|
22
|
+
assert.equal(analyzeReadiness({stdout:'Exit code: 7', state:'running', exitCode:7}).state, 'finished');
|
|
23
|
+
assert.equal(analyzeReadiness({stdout:'anything', state:'exited', exitCode:0}).evidence, 'process_terminal');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('handles CRLF/LF/CR and normalization', () => {
|
|
27
|
+
const value='a'+CR+LF+'b'+CR+LF+'c'+LF;
|
|
28
|
+
assert.deepEqual(lineEndingProfile(value), { style:CR+LF, crlf:2, lf:1, cr:0, total:3, mixed:true });
|
|
29
|
+
assert.equal(normalizeLineEndings(value, LF), 'a'+LF+'b'+LF+'c'+LF);
|
|
30
|
+
assert.equal(normalizeLineEndings('a'+LF+'b', CR+LF), 'a'+CR+LF+'b');
|
|
31
|
+
});
|
|
32
|
+
|
|
@@ -165,6 +165,11 @@ test('R46 process output retention evicts oldest bytes without losing tail ident
|
|
|
165
165
|
assert.equal(recovered.stream_next_seq, null);
|
|
166
166
|
assert.equal(recovered.stream_coverage, null);
|
|
167
167
|
assert.ok(recovered.stream_head > 2 * 1024 * 1024);
|
|
168
|
+
assert.ok(recovered.operation_id);
|
|
169
|
+
assert.ok(recovered.operation_receipt);
|
|
170
|
+
assert.ok(recovered.operation.terminal.event_hash);
|
|
171
|
+
const recoveredEvidence = await recoveredRuntime.verifyOperationEvidence({ sessionId: started.session_id });
|
|
172
|
+
assert.equal(recoveredEvidence.ok, true);
|
|
168
173
|
});
|
|
169
174
|
|
|
170
175
|
test('R46 process raw input keeps persistent PowerShell state with bounded wait and tail reads', { skip: process.platform !== 'win32' }, async () => {
|
|
@@ -309,3 +314,87 @@ test('R32 named actions reject sessions after child exit', { skip: process.platf
|
|
|
309
314
|
});
|
|
310
315
|
|
|
311
316
|
|
|
317
|
+
test('R47 operation history integrates with real process lifecycle and signed terminal receipt', { skip: process.platform !== 'win32' }, async () => {
|
|
318
|
+
const f = await fixture();
|
|
319
|
+
const started = await f.runtime.start({ profileId: 'hold', rootId: 'core', cwd: '.', args: [] });
|
|
320
|
+
assert.match(started.operation_id, /^[0-9a-f]{64}$/);
|
|
321
|
+
assert.ok(['running','output'].includes(started.operation.state));
|
|
322
|
+
const stopped = await f.runtime.terminate({ sessionId: started.session_id });
|
|
323
|
+
assert.equal(stopped.operation.terminal.type, 'cancelled');
|
|
324
|
+
assert.ok(stopped.operation.terminal.sequence > 0);
|
|
325
|
+
assert.equal(stopped.operation.history_head_sha256, stopped.operation.terminal.event_hash);
|
|
326
|
+
assert.ok(stopped.operation_receipt);
|
|
327
|
+
assert.equal(stopped.operation_receipt.operation_id, started.operation_id);
|
|
328
|
+
assert.equal(stopped.operation_receipt.terminal.terminal_sequence, stopped.operation.terminal.sequence);
|
|
329
|
+
assert.equal(stopped.operation_receipt.terminal.terminal_event_hash, stopped.operation.terminal.event_hash);
|
|
330
|
+
const evidence = await f.runtime.verifyOperationEvidence({ sessionId: started.session_id });
|
|
331
|
+
assert.equal(evidence.ok, true);
|
|
332
|
+
assert.equal(evidence.operation_id, started.operation_id);
|
|
333
|
+
const history = await readFile(path.join(f.stateDir, started.session_id + '.operation.jsonl'), 'utf8');
|
|
334
|
+
const events = history.trim().split(/\r?\n/).map(line => JSON.parse(line));
|
|
335
|
+
assert.equal(events[0].event_type, 'start');
|
|
336
|
+
assert.equal(events.at(-1).event_type, 'cancelled');
|
|
337
|
+
assert.deepEqual(events.map(event => event.sequence), events.map((_, i) => i + 1));
|
|
338
|
+
assert.equal(events.at(-1).event_hash, stopped.operation_receipt.terminal.terminal_event_hash);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('R47 concurrent cancel/terminate has one terminal operation event', { skip: process.platform !== 'win32' }, async () => {
|
|
342
|
+
const f = await fixture();
|
|
343
|
+
const started = await f.runtime.start({ profileId: 'hold', rootId: 'core', cwd: '.', args: [] });
|
|
344
|
+
const results = await Promise.allSettled([
|
|
345
|
+
f.runtime.cancel({ sessionId: started.session_id, reason: 'race_cancel' }),
|
|
346
|
+
f.runtime.terminate({ sessionId: started.session_id })
|
|
347
|
+
]);
|
|
348
|
+
assert.ok(results.some(result => result.status === 'fulfilled'));
|
|
349
|
+
const status = await f.runtime.status({ sessionId: started.session_id });
|
|
350
|
+
assert.ok(['cancelled','timed_out'].includes(status.operation.terminal.type));
|
|
351
|
+
const history = await readFile(path.join(f.stateDir, started.session_id + '.operation.jsonl'), 'utf8');
|
|
352
|
+
const events = history.trim().split(/\r?\n/).map(line => JSON.parse(line));
|
|
353
|
+
const terminals = events.filter(event => ['success','failure','cancelled','timed_out'].includes(event.event_type));
|
|
354
|
+
assert.equal(terminals.length, 1);
|
|
355
|
+
assert.equal(events.at(-1).event_hash, status.operation_receipt.terminal.terminal_event_hash);
|
|
356
|
+
const evidence = await f.runtime.verifyOperationEvidence({ sessionId: started.session_id });
|
|
357
|
+
assert.equal(evidence.ok, true);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test('R47 timeout and producer failure map to distinct terminal evidence', { skip: process.platform !== 'win32' }, async () => {
|
|
361
|
+
const timed = await fixture();
|
|
362
|
+
const timedSession = await timed.runtime.start({
|
|
363
|
+
profileId:'powershell', rootId:'core', cwd:'.',
|
|
364
|
+
timeoutMs:250, args:['Start-Sleep -Seconds 5']
|
|
365
|
+
});
|
|
366
|
+
const deadline=Date.now()+5000;
|
|
367
|
+
let timedStatus=await timed.runtime.status({sessionId:timedSession.session_id});
|
|
368
|
+
while(timedStatus.state==='running' && Date.now()<deadline){ await sleep(25); timedStatus=await timed.runtime.status({sessionId:timedSession.session_id}); }
|
|
369
|
+
assert.equal(timedStatus.operation.terminal.type,'timed_out');
|
|
370
|
+
assert.equal((await timed.runtime.verifyOperationEvidence({sessionId:timedSession.session_id})).ok,true);
|
|
371
|
+
|
|
372
|
+
const failed = await fixture();
|
|
373
|
+
const failedSession = await failed.runtime.start({
|
|
374
|
+
profileId:'powershell', rootId:'core', cwd:'.',
|
|
375
|
+
args:['[Console]::Error.WriteLine(\'DB_FAIL_MARK\'); exit 7']
|
|
376
|
+
});
|
|
377
|
+
const failedDeadline=Date.now()+5000;
|
|
378
|
+
let failedStatus=await failed.runtime.status({sessionId:failedSession.session_id});
|
|
379
|
+
while(failedStatus.state==='running' && Date.now()<failedDeadline){ await sleep(25); failedStatus=await failed.runtime.status({sessionId:failedSession.session_id}); }
|
|
380
|
+
assert.equal(failedStatus.state,'exited');
|
|
381
|
+
assert.equal(failedStatus.exit_code,7);
|
|
382
|
+
assert.equal(failedStatus.operation.terminal.type,'failure');
|
|
383
|
+
assert.equal((await failed.runtime.verifyOperationEvidence({sessionId:failedSession.session_id})).ok,true);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test('R47 concurrent cancel and page keep terminal authority singular', { skip: process.platform !== 'win32' }, async () => {
|
|
387
|
+
const f=await fixture();
|
|
388
|
+
const started=await f.runtime.start({profileId:'hold',rootId:'core',cwd:'.',args:[]});
|
|
389
|
+
const operations=await Promise.allSettled([
|
|
390
|
+
f.runtime.cancel({sessionId:started.session_id,reason:'page_race_cancel'}),
|
|
391
|
+
f.runtime.read({sessionId:started.session_id,offset:0,maxBytes:1024,waitMs:100}),
|
|
392
|
+
f.runtime.status({sessionId:started.session_id})
|
|
393
|
+
]);
|
|
394
|
+
assert.ok(operations.some(x=>x.status==='fulfilled'));
|
|
395
|
+
const status=await f.runtime.status({sessionId:started.session_id});
|
|
396
|
+
assert.ok(['cancelled','timed_out'].includes(status.operation.terminal.type));
|
|
397
|
+
const evidence=await f.runtime.verifyOperationEvidence({sessionId:started.session_id});
|
|
398
|
+
assert.equal(evidence.ok,true);
|
|
399
|
+
});
|
|
400
|
+
|