deadbyte-mcp 0.15.51 → 0.15.53
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 +47 -18
- package/PROVENANCE.json +27 -9
- package/README.txt +18 -18
- package/RELEASE.json +33 -6
- package/SBOM.cdx.json +2 -2
- package/TEST-MATRIX.json +32 -2
- package/bridge/src/remote-call-claims-r60.mjs +5 -0
- package/controller/deadbyte-process-policy.json +4 -4
- package/docs/R135-AUTONOMOUS-SCOPE.md +21 -0
- package/docs/R136-SCOPED-OBSERVER.md +21 -0
- package/docs/R137-DESKTOP-COMMANDER-FOUNDATION.md +31 -0
- package/docs/R138-COMMANDER-WORKFLOW.md +21 -0
- package/docs/R139-COMMANDER-ERGONOMICS.md +17 -0
- package/docs/THIRD-PARTY-DESKTOP-COMMANDER-MIT.md +27 -0
- package/package.json +1 -1
- package/src/autonomous-scope-r135.mjs +91 -0
- package/src/commander-config-recovery-r137.mjs +67 -0
- package/src/commander-extended-runtime.mjs +22 -5
- package/src/commander-external-process-r48.mjs +25 -3
- package/src/commander-large-text-r139.mjs +66 -0
- package/src/commander-wire-adapter-r47.mjs +63 -12
- package/src/deadbyte-cli.mjs +5 -1
- package/src/scoped-observer-launcher-r136.mjs +77 -0
- package/src/scoped-observer-worker-r136.mjs +65 -0
- package/src/version.mjs +1 -1
- package/test/commander-wire-contract-r47.test.mjs +8 -5
- package/test/r130-release-security-contract.test.mjs +8 -8
- package/test/r132-release-identity.test.mjs +9 -9
- package/test/r135-autonomous-scope.test.mjs +103 -0
- package/test/r136-scoped-command-rights.test.mjs +39 -0
- package/test/r136-scoped-observer.test.mjs +89 -0
- package/test/r137-commander-config-recovery.test.mjs +111 -0
- package/test/r137-commander-three-version-contract.test.mjs +56 -0
- package/test/r138-commander-process-pagination.test.mjs +42 -0
- package/test/r138-commander-session-list.test.mjs +24 -0
- package/test/r138-commander-stream-resume.test.mjs +50 -0
- package/test/r139-large-text-edit.test.mjs +51 -0
- package/test/r139-large-text-read.test.mjs +46 -0
- package/test/r139-process-start-preview.test.mjs +50 -0
- package/test/r139-search-partial.test.mjs +42 -0
- package/test/r140-large-text-integrity.test.mjs +68 -0
- package/test/r141-commander-tail-utf8.test.mjs +55 -0
- package/test/r141-real-windows-disk.test.mjs +71 -0
- package/test/r142-native-machinefs-authority.test.mjs +127 -0
- package/test/r144-claim-terminal-history.test.mjs +55 -0
- package/test/r144-controller-broad-roots-cleanup.test.mjs +54 -0
- package/test/r59-tool-surface-compat.test.mjs +1 -1
- package/test/r74-prepublish-contract.test.mjs +3 -3
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {createCommanderWireAdapter} from '../src/commander-wire-adapter-r47.mjs';
|
|
4
|
+
const saved={profile:process.env.DEADBYTE_COMMANDER_COMPAT_PROFILE,root:process.env.DEADBYTE_COMMANDER_COMPAT_ROOT};
|
|
5
|
+
function fakeProcess({retained='one\ntwo\n',earliest=5,head=13,state='exited'}={}){
|
|
6
|
+
const bytes=Buffer.from(retained);
|
|
7
|
+
return {
|
|
8
|
+
policy:{roots:{core:{real_path:'C:\\\\r138'}}},
|
|
9
|
+
async start(){return {session_id:'s'.repeat(64),child_pid:18138}},
|
|
10
|
+
async read({offset,maxBytes}){
|
|
11
|
+
const start=Math.max(offset,earliest);
|
|
12
|
+
const data=bytes.subarray(start-earliest,start-earliest+maxBytes);
|
|
13
|
+
return {offset:start,next_offset:start+data.length,earliest_offset:earliest,stream_head:head,
|
|
14
|
+
state,exit_code:0,data_base64:data.toString('base64')};
|
|
15
|
+
},
|
|
16
|
+
async list(){return {sessions:[]}},
|
|
17
|
+
async status(){return {state}}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
async function adapter(proc){
|
|
21
|
+
process.env.DEADBYTE_COMMANDER_COMPAT_PROFILE='test';
|
|
22
|
+
process.env.DEADBYTE_COMMANDER_COMPAT_ROOT='core';
|
|
23
|
+
const rt=createCommanderWireAdapter({processRuntime:proc});
|
|
24
|
+
await rt.handlers.start_process({command:'echo hi',timeout_ms:1000});
|
|
25
|
+
return rt;
|
|
26
|
+
}
|
|
27
|
+
test.after(()=>{if(saved.profile===undefined)delete process.env.DEADBYTE_COMMANDER_COMPAT_PROFILE;else process.env.DEADBYTE_COMMANDER_COMPAT_PROFILE=saved.profile;if(saved.root===undefined)delete process.env.DEADBYTE_COMMANDER_COMPAT_ROOT;else process.env.DEADBYTE_COMMANDER_COMPAT_ROOT=saved.root;});
|
|
28
|
+
test('R138 incremental output reports exact byte resume location and dropped prefix explicitly',async()=>{
|
|
29
|
+
const a=await adapter(fakeProcess());
|
|
30
|
+
const first=await a.handlers.read_process_output({pid:18138,offset:0,length:1,timeout_ms:0});
|
|
31
|
+
assert.equal(first.isError,undefined,JSON.stringify(first));
|
|
32
|
+
assert.equal(first.structuredContent.truncated,true);
|
|
33
|
+
assert.equal(first.structuredContent.earliest_offset,5);
|
|
34
|
+
assert.equal(first.structuredContent.next_byte_offset,9);
|
|
35
|
+
assert.equal(first.structuredContent.more_unread,true);
|
|
36
|
+
const second=await a.handlers.read_process_output({pid:18138,offset:0,length:1,timeout_ms:0});
|
|
37
|
+
assert.equal(second.structuredContent.next_byte_offset,13);
|
|
38
|
+
assert.equal(second.structuredContent.more_unread,false);
|
|
39
|
+
const third=await a.handlers.read_process_output({pid:18138,offset:0,length:1,timeout_ms:0});
|
|
40
|
+
assert.equal(third.structuredContent.next_byte_offset,13);
|
|
41
|
+
assert.equal(third.structuredContent.read_count,0);
|
|
42
|
+
});
|
|
43
|
+
test('R138 historical line offset reads are nonconsuming and declare no byte resume cursor',async()=>{
|
|
44
|
+
const a=await adapter(fakeProcess());
|
|
45
|
+
const history=await a.handlers.read_process_output({pid:18138,offset:-1,length:1,timeout_ms:0});
|
|
46
|
+
assert.equal(history.structuredContent.read_count,1);
|
|
47
|
+
assert.equal(history.structuredContent.next_byte_offset,null);
|
|
48
|
+
const initial=await a.handlers.read_process_output({pid:18138,offset:0,length:1,timeout_ms:0});
|
|
49
|
+
assert.equal(initial.structuredContent.next_byte_offset,9);
|
|
50
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {createHash} from 'node:crypto';
|
|
4
|
+
import {createCommanderWireAdapter} from '../src/commander-wire-adapter-r47.mjs';
|
|
5
|
+
const sha=data=>createHash('sha256').update(data).digest('hex');
|
|
6
|
+
function fake(initial){
|
|
7
|
+
let current=Buffer.from(initial);let writes=0;
|
|
8
|
+
const runtime={
|
|
9
|
+
policy:{roots:{core:{real_path:'D:\\\\R139-EDIT'}},max_read_bytes:64*1024*1024},
|
|
10
|
+
async read({maxBytes}){
|
|
11
|
+
const view=current.subarray(0,maxBytes);
|
|
12
|
+
return {data_base64:view.toString('base64'),file_sha256:sha(current),eof:view.length===current.length,file_size:current.length};
|
|
13
|
+
},
|
|
14
|
+
async write({content,encoding,expectedSha256}){
|
|
15
|
+
assert.equal(expectedSha256,sha(current));
|
|
16
|
+
const next=encoding==='utf8'?Buffer.from(content):Buffer.from(content,'base64');
|
|
17
|
+
current=next;writes++;
|
|
18
|
+
return {sha256:sha(next),receipt_id:'r139-test'};
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
return {runtime,writes:()=>writes,current:()=>current};
|
|
22
|
+
}
|
|
23
|
+
test('R139 edit_block edits a unique target in a 5MB UTF-8 file with exact source hash',async()=>{
|
|
24
|
+
const content=Buffer.from('a'.repeat(5*1024*1024)+'END-MARKER\n');
|
|
25
|
+
const f=fake(content),a=createCommanderWireAdapter({machineFsRuntime:f.runtime});
|
|
26
|
+
const r=await a.handlers.edit_block({file_path:'D:\\\\R139-EDIT\\\\large.log',old_string:'END-MARKER',new_string:'REPLACED',expected_replacements:1});
|
|
27
|
+
assert.equal(r.isError,undefined);
|
|
28
|
+
assert.equal(r.structuredContent.replacements,1);
|
|
29
|
+
assert.equal(f.writes(),1);
|
|
30
|
+
assert.match(f.current().subarray(-16).toString('utf8'),/REPLACED/);
|
|
31
|
+
});
|
|
32
|
+
test('R139 edit_block refuses invalid UTF-8 content even when the marker is ASCII',async()=>{
|
|
33
|
+
const f=fake(Buffer.concat([Buffer.from('abc TARGET'),Buffer.from([0xff,0xfe])]));
|
|
34
|
+
const a=createCommanderWireAdapter({machineFsRuntime:f.runtime});
|
|
35
|
+
await assert.rejects(a.handlers.edit_block({file_path:'D:\\\\R139-EDIT\\\\invalid.txt',old_string:'TARGET',new_string:'CHANGED',expected_replacements:1}),/invalid UTF-8|binary/);
|
|
36
|
+
assert.equal(f.writes(),0);
|
|
37
|
+
});
|
|
38
|
+
test('R139 edit_block preserves UTF-8 BOM when modifying existing text',async()=>{
|
|
39
|
+
const original=Buffer.concat([Buffer.from([0xef,0xbb,0xbf]),Buffer.from('header OLD tail','utf8')]);
|
|
40
|
+
const f=fake(original),a=createCommanderWireAdapter({machineFsRuntime:f.runtime});
|
|
41
|
+
await a.handlers.edit_block({file_path:'D:\\\\R139-EDIT\\\\bom.txt',old_string:'OLD',new_string:'NEW',expected_replacements:1});
|
|
42
|
+
assert.deepEqual(f.current().subarray(0,3),Buffer.from([0xef,0xbb,0xbf]));
|
|
43
|
+
assert.equal(f.current().subarray(3).toString('utf8'),'header NEW tail');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('R139 text edits refuse a file above the 16MiB safety bound',async()=>{
|
|
47
|
+
const f=fake(Buffer.from('x'.repeat(17*1024*1024)+'OLD'));
|
|
48
|
+
const a=createCommanderWireAdapter({machineFsRuntime:f.runtime});
|
|
49
|
+
await assert.rejects(a.handlers.edit_block({file_path:'D:\\\\R139-EDIT\\\\too-big.log',old_string:'OLD',new_string:'NEW',expected_replacements:1}),/exceeds bounded compatibility edit limit/);
|
|
50
|
+
assert.equal(f.writes(),0);
|
|
51
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {createHash} from 'node:crypto';
|
|
4
|
+
import {createCommanderWireAdapter} from '../src/commander-wire-adapter-r47.mjs';
|
|
5
|
+
|
|
6
|
+
function simulatedFile(bytes,{tamperAfter=Infinity}={}){
|
|
7
|
+
const sha=createHash('sha256').update(bytes).digest('hex');
|
|
8
|
+
let reads=0;
|
|
9
|
+
const fs={
|
|
10
|
+
policy:{roots:{core:{real_path:'D:\\\\R139-LARGE'}},max_read_bytes:1024*1024},
|
|
11
|
+
async read({offset=0,maxBytes}){
|
|
12
|
+
reads++;
|
|
13
|
+
const data=bytes.subarray(offset,offset+maxBytes);
|
|
14
|
+
return {offset,next_offset:offset+data.length,data_base64:data.toString('base64'),
|
|
15
|
+
eof:offset+data.length===bytes.length,file_size:bytes.length,
|
|
16
|
+
file_sha256:reads>=tamperAfter?'0'.repeat(64):sha};
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
return {fs,reads:()=>reads};
|
|
20
|
+
}
|
|
21
|
+
test('R139 reads near end of a 5MB text file in bounded native chunks without full-file buffering',async()=>{
|
|
22
|
+
const rows=Array.from({length:55000},(_,i)=>String(i).padStart(5,'0')+':'+'.'.repeat(90));
|
|
23
|
+
const source=Buffer.from(rows.join('\r\n')+'\r\n','utf8');
|
|
24
|
+
assert.ok(source.length>4*1024*1024);
|
|
25
|
+
const f=simulatedFile(source);
|
|
26
|
+
const a=createCommanderWireAdapter({machineFsRuntime:f.fs});
|
|
27
|
+
const result=await a.handlers.read_file({path:'D:\\\\R139-LARGE\\\\sample.txt',offset:54997,length:3});
|
|
28
|
+
assert.equal(result.isError,undefined,JSON.stringify(result));
|
|
29
|
+
assert.equal(result.structuredContent.lineOffset,54997);
|
|
30
|
+
assert.equal(result.structuredContent.lineCount,3);
|
|
31
|
+
assert.equal(result.structuredContent.totalLines,55001);
|
|
32
|
+
assert.equal(result.content[0].text,rows.slice(54997,55000).join('\n')+'\n');
|
|
33
|
+
assert.ok(f.reads()>4);
|
|
34
|
+
});
|
|
35
|
+
test('R139 detects a changing file identity during large streaming read',async()=>{
|
|
36
|
+
const source=Buffer.from(('ok-line\n').repeat(700000));
|
|
37
|
+
const f=simulatedFile(source,{tamperAfter:3});
|
|
38
|
+
const a=createCommanderWireAdapter({machineFsRuntime:f.fs});
|
|
39
|
+
await assert.rejects(a.handlers.read_file({path:'D:\\\\R139-LARGE\\\\x.log',offset:400000,length:1}),/changed during paged read|identity mismatch/);
|
|
40
|
+
});
|
|
41
|
+
test('R139 leaves binary compatibility handling unchanged on small files',async()=>{
|
|
42
|
+
const bytes=Buffer.from([0,2,3,4]);const f=simulatedFile(bytes);
|
|
43
|
+
const a=createCommanderWireAdapter({machineFsRuntime:f.fs});
|
|
44
|
+
const r=await a.handlers.read_file({path:'D:\\\\R139-LARGE\\\\x.bin',offset:0,length:5});
|
|
45
|
+
assert.equal(r.structuredContent.isBinary,true);
|
|
46
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {createCommanderWireAdapter} from '../src/commander-wire-adapter-r47.mjs';
|
|
4
|
+
|
|
5
|
+
function mock({output='hello from child\n',readThrows=false}={}){
|
|
6
|
+
const calls={reads:[],started:0};
|
|
7
|
+
const raw=Buffer.from(output,'utf8');
|
|
8
|
+
const runtime={
|
|
9
|
+
policy:{roots:{core:{real_path:'C:\\\\r139'}},profiles:{repl:{allow_raw_input:false}}},
|
|
10
|
+
async start(){calls.started++;return {child_pid:29139,session_id:'a'.repeat(64),state:'running'}},
|
|
11
|
+
async read(args){calls.reads.push(args);if(readThrows)throw new Error('read unavailable');
|
|
12
|
+
const at=args.offset??0;const chunk=raw.subarray(at,at+args.maxBytes);
|
|
13
|
+
return {offset:at,next_offset:at+chunk.length,earliest_offset:0,stream_head:raw.length,
|
|
14
|
+
state:'exited',exit_code:0,data_base64:chunk.toString('base64')};},
|
|
15
|
+
};
|
|
16
|
+
return {runtime,calls};
|
|
17
|
+
}
|
|
18
|
+
async function invoke(f){
|
|
19
|
+
const oldProfile=process.env.DEADBYTE_COMMANDER_COMPAT_PROFILE;
|
|
20
|
+
const oldRoot=process.env.DEADBYTE_COMMANDER_COMPAT_ROOT;
|
|
21
|
+
process.env.DEADBYTE_COMMANDER_COMPAT_PROFILE='repl';
|
|
22
|
+
process.env.DEADBYTE_COMMANDER_COMPAT_ROOT='core';
|
|
23
|
+
try{
|
|
24
|
+
const a=createCommanderWireAdapter({processRuntime:f.runtime});
|
|
25
|
+
return await a.handlers.start_process({command:'echo sample',timeout_ms:1000});
|
|
26
|
+
}finally{
|
|
27
|
+
if(oldProfile===undefined)delete process.env.DEADBYTE_COMMANDER_COMPAT_PROFILE;
|
|
28
|
+
else process.env.DEADBYTE_COMMANDER_COMPAT_PROFILE=oldProfile;
|
|
29
|
+
if(oldRoot===undefined)delete process.env.DEADBYTE_COMMANDER_COMPAT_ROOT;
|
|
30
|
+
else process.env.DEADBYTE_COMMANDER_COMPAT_ROOT=oldRoot;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
test('R139 start_process returns already-ready output preview without waiting or consuming output cursor',async()=>{
|
|
34
|
+
const f=mock();const res=await invoke(f);
|
|
35
|
+
assert.equal(res.structuredContent.pid,29139);
|
|
36
|
+
assert.equal(res.structuredContent.initial_output,'hello from child\n');
|
|
37
|
+
assert.equal(res.structuredContent.initial_output_truncated,false);
|
|
38
|
+
assert.match(res.content[0].text,/hello from child/);
|
|
39
|
+
assert.equal(f.calls.reads.length,1);
|
|
40
|
+
assert.equal(f.calls.reads[0].waitMs,0);
|
|
41
|
+
assert.equal(f.calls.reads[0].offset,0);
|
|
42
|
+
assert.ok(f.calls.reads[0].maxBytes<=16384);
|
|
43
|
+
});
|
|
44
|
+
test('R139 start_process preserves started session if quick output probe fails',async()=>{
|
|
45
|
+
const f=mock({readThrows:true});const res=await invoke(f);
|
|
46
|
+
assert.equal(f.calls.started,1);
|
|
47
|
+
assert.equal(res.structuredContent.pid,29139);
|
|
48
|
+
assert.equal(res.structuredContent.initial_output,'');
|
|
49
|
+
assert.equal(res.structuredContent.output_preview_unavailable,true);
|
|
50
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {createCommanderWireAdapter} from '../src/commander-wire-adapter-r47.mjs';
|
|
4
|
+
|
|
5
|
+
const root='D:\\R139-SEARCH';
|
|
6
|
+
const mockFs={policy:{roots:{core:{real_path:root}}}};
|
|
7
|
+
const pause=()=>new Promise(resolve=>setImmediate(resolve));
|
|
8
|
+
|
|
9
|
+
test('R139 search returns the completed first page while later pages are still running',async()=>{
|
|
10
|
+
let resume,entered=false,calls=0;
|
|
11
|
+
const block=new Promise(r=>resume=r);
|
|
12
|
+
const adapter=createCommanderWireAdapter({machineFsRuntime:mockFs,searchRuntime:async args=>{
|
|
13
|
+
calls++;
|
|
14
|
+
if(calls===1)return {matches:[{path:'one.txt',line:1,preview:'alpha'}],next_cursor:'page-2'};
|
|
15
|
+
entered=true;await block;
|
|
16
|
+
return {matches:[{path:'two.txt',line:2,preview:'alpha'}],next_cursor:null};
|
|
17
|
+
}});
|
|
18
|
+
const started=await adapter.handlers.start_search({path:root,pattern:'alpha',searchType:'content',ignoreCase:true});
|
|
19
|
+
try{
|
|
20
|
+
for(let i=0;i<20&&!entered;i++)await pause();
|
|
21
|
+
assert.equal(entered,true);
|
|
22
|
+
const partial=await adapter.handlers.get_more_search_results({sessionId:started.sessionId,offset:0,length:10});
|
|
23
|
+
assert.deepEqual(partial.results.map(x=>x.file),['one.txt']);
|
|
24
|
+
assert.equal(partial.isComplete,false);
|
|
25
|
+
assert.equal(partial.totalResults,1);
|
|
26
|
+
assert.equal(partial.hasMoreResults,true);
|
|
27
|
+
resume();
|
|
28
|
+
for(let i=0;i<20;i++){await pause();const done=await adapter.handlers.get_more_search_results({sessionId:started.sessionId,offset:0,length:10});if(done.isComplete){assert.deepEqual(done.results.map(x=>x.file),['one.txt','two.txt']);return;}}
|
|
29
|
+
assert.fail('search did not complete after second page released');
|
|
30
|
+
}finally{resume();}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('R139 completed search returns cached page immediately without another runtime search',async()=>{
|
|
34
|
+
let calls=0;
|
|
35
|
+
const adapter=createCommanderWireAdapter({machineFsRuntime:mockFs,searchRuntime:async()=>{calls++;return {matches:[{path:'matched.txt',line:1,preview:'ok'}],next_cursor:null};}});
|
|
36
|
+
const start=await adapter.handlers.start_search({path:root,pattern:'matched',searchType:'files'});
|
|
37
|
+
let done;
|
|
38
|
+
for(let i=0;i<20;i++){await pause();done=await adapter.handlers.get_more_search_results({sessionId:start.sessionId,offset:0,length:10});if(done.isComplete)break;}
|
|
39
|
+
assert.equal(done.isComplete,true);
|
|
40
|
+
assert.equal(done.results[0].file,'matched.txt');
|
|
41
|
+
assert.equal(calls,1);
|
|
42
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { readCommanderLargeText } from '../src/commander-large-text-r139.mjs';
|
|
5
|
+
|
|
6
|
+
const sha = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
7
|
+
const source = Buffer.from(('line-000000: '+'.'.repeat(85)+'\r\n').repeat(60000), 'utf8');
|
|
8
|
+
|
|
9
|
+
function native(initial, mutate = null) {
|
|
10
|
+
let bytes = Buffer.from(initial), reads = 0, fullHashes = 0;
|
|
11
|
+
return {
|
|
12
|
+
policy: {max_read_bytes: 256*1024},
|
|
13
|
+
get counts() { return {reads, fullHashes}; },
|
|
14
|
+
async read({offset, maxBytes, hashFile}) {
|
|
15
|
+
reads++;
|
|
16
|
+
if(mutate) bytes = mutate(bytes, reads) ?? bytes;
|
|
17
|
+
const part = bytes.subarray(offset, offset + maxBytes);
|
|
18
|
+
if(hashFile) fullHashes++;
|
|
19
|
+
return {offset, next_offset: offset + part.length,
|
|
20
|
+
data_base64:part.toString('base64'), eof:offset + part.length >= bytes.length,
|
|
21
|
+
file_size:bytes.length, file_sha256:hashFile ? sha(bytes) : null};
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test('R140 large file hashes once, checks whole delivered stream, and preserves page semantics', async () => {
|
|
27
|
+
assert.ok(source.length > 4*1024*1024);
|
|
28
|
+
const runtime = native(source);
|
|
29
|
+
const result = await readCommanderLargeText({runtime, rootId:'core',
|
|
30
|
+
relativePath:'large.txt', offset:59997, length:3});
|
|
31
|
+
assert.equal(result.fileSha256, sha(source));
|
|
32
|
+
assert.equal(result.totalLines, 60001);
|
|
33
|
+
assert.equal(result.lineOffset, 59997);
|
|
34
|
+
assert.equal(result.lineCount, 3);
|
|
35
|
+
assert.equal(result.content, ('line-000000: '+'.'.repeat(85)+'\n').repeat(3));
|
|
36
|
+
assert.equal(runtime.counts.fullHashes, 1, 'must not rehash whole file per chunk');
|
|
37
|
+
assert.ok(runtime.counts.reads > 2);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('R140 detects same-length mutation of bytes already delivered across page boundary', async () => {
|
|
41
|
+
const runtime = native(source, (bytes, readNumber) => {
|
|
42
|
+
if(readNumber === 3) { const changed=Buffer.from(bytes); changed[40]^=1; return changed; }
|
|
43
|
+
});
|
|
44
|
+
await assert.rejects(readCommanderLargeText({runtime,rootId:'core',
|
|
45
|
+
relativePath:'large.txt',offset:59000,length:2}), /changed during paged read|identity mismatch/);
|
|
46
|
+
assert.equal(runtime.counts.fullHashes,1);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('R140 detects same-length mutation immediately before closing hash', async () => {
|
|
50
|
+
const expectedChunks = Math.ceil(source.length / (256*1024));
|
|
51
|
+
const runtime = native(source, (bytes, readNumber) => {
|
|
52
|
+
if(readNumber === expectedChunks+1) { const changed=Buffer.from(bytes); changed[2]^=1; return changed; }
|
|
53
|
+
});
|
|
54
|
+
await assert.rejects(readCommanderLargeText({runtime,rootId:'core',
|
|
55
|
+
relativePath:'large.txt',offset:59999,length:1}), /changed during paged read|identity mismatch/);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('R140 rejects untrusted or absent closing SHA-256 identity', async () => {
|
|
59
|
+
const runtime = native(source);
|
|
60
|
+
const original=runtime.read.bind(runtime);
|
|
61
|
+
runtime.read = async args => {
|
|
62
|
+
const result=await original(args);
|
|
63
|
+
if(args.hashFile) result.file_sha256=null;
|
|
64
|
+
return result;
|
|
65
|
+
};
|
|
66
|
+
await assert.rejects(readCommanderLargeText({runtime,rootId:'core',
|
|
67
|
+
relativePath:'large.txt',offset:1,length:1}), /identity missing/);
|
|
68
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {createCommanderWireAdapter} from '../src/commander-wire-adapter-r47.mjs';
|
|
4
|
+
|
|
5
|
+
const PATH='D:\\R141-TAIL\\log.txt';
|
|
6
|
+
function fixture(data,{maxRead=64,tailFileOffset=null}={}){
|
|
7
|
+
const bytes=Buffer.from(data); let reads=0;
|
|
8
|
+
const fs={
|
|
9
|
+
policy:{roots:{core:{real_path:'D:\\R141-TAIL'}},max_read_bytes:maxRead},
|
|
10
|
+
async read({offset=0,maxBytes,hashFile=false}){
|
|
11
|
+
reads++;
|
|
12
|
+
const at=offset<0?Math.max(0,bytes.length+offset):offset;
|
|
13
|
+
const part=bytes.subarray(at,at+maxBytes);
|
|
14
|
+
return {status:'ok',offset:at,next_offset:at+part.length,
|
|
15
|
+
file_size:bytes.length,eof:at+part.length>=bytes.length,
|
|
16
|
+
file_sha256:null,data_base64:part.toString('base64')};
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
return {fs,reads:()=>reads};
|
|
20
|
+
}
|
|
21
|
+
test('R141 negative tail rejects invalid UTF-8 instead of silently introducing replacement characters', async()=>{
|
|
22
|
+
const runtime=fixture(Buffer.concat([Buffer.from('ok\ninvalid '),Buffer.from([0xc3,0x28]),Buffer.from('\n')]));
|
|
23
|
+
const api=createCommanderWireAdapter({machineFsRuntime:runtime.fs});
|
|
24
|
+
await assert.rejects(api.handlers.read_file({path:PATH,offset:-1}),/valid UTF-8|UTF-8/);
|
|
25
|
+
});
|
|
26
|
+
test('R141 negative tail rejects embedded NUL just like positive text detection',async()=>{
|
|
27
|
+
const runtime=fixture(Buffer.from('ok\ntail\0broken\n'));
|
|
28
|
+
const api=createCommanderWireAdapter({machineFsRuntime:runtime.fs});
|
|
29
|
+
await assert.rejects(api.handlers.read_file({path:PATH,offset:-1}),/NUL|binary/);
|
|
30
|
+
});
|
|
31
|
+
test('R141 negative tail does not decode the leading partial UTF-8 line',async()=>{
|
|
32
|
+
const prefix=Buffer.from('prefix '+('x'.repeat(18))+'é-not-selected\n');
|
|
33
|
+
const bytes=Buffer.concat([prefix,Buffer.from('valid-一\nlast-€\n')]);
|
|
34
|
+
const tailLen=bytes.length-(prefix.indexOf(Buffer.from('é'))+1);
|
|
35
|
+
const runtime=fixture(bytes,{maxRead:tailLen});
|
|
36
|
+
const api=createCommanderWireAdapter({machineFsRuntime:runtime.fs});
|
|
37
|
+
const result=await api.handlers.read_file({path:PATH,offset:-2});
|
|
38
|
+
assert.equal(result.content[0].text,'valid-一\nlast-€');
|
|
39
|
+
assert.equal(result.structuredContent.tailByteOffset,bytes.length-tailLen);
|
|
40
|
+
});
|
|
41
|
+
test('R141 tail decodes intact multibyte UTF-8 CRLF lines',async()=>{
|
|
42
|
+
const runtime=fixture(Buffer.from('old\r\n漢字\r\nterminus €\r\n'));
|
|
43
|
+
const api=createCommanderWireAdapter({machineFsRuntime:runtime.fs});
|
|
44
|
+
const result=await api.handlers.read_file({path:PATH,offset:-2});
|
|
45
|
+
assert.equal(result.content[0].text,'漢字\nterminus €');
|
|
46
|
+
assert.equal(result.structuredContent.lineCount,2);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('R141 keeps a complete first line when the bounded tail begins exactly at a line boundary',async()=>{
|
|
50
|
+
const source=Buffer.from('header!\nlast-1\nlast-2\n');
|
|
51
|
+
const runtime=fixture(source,{maxRead:14});
|
|
52
|
+
const api=createCommanderWireAdapter({machineFsRuntime:runtime.fs});
|
|
53
|
+
const result=await api.handlers.read_file({path:PATH,offset:-2});
|
|
54
|
+
assert.equal(result.content[0].text,'last-1\nlast-2');
|
|
55
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {createHash} from 'node:crypto';
|
|
4
|
+
import {mkdtemp,readFile,writeFile,open,rm} from 'node:fs/promises';
|
|
5
|
+
import {tmpdir} from 'node:os';
|
|
6
|
+
import {join} from 'node:path';
|
|
7
|
+
import {spawnSync} from 'node:child_process';
|
|
8
|
+
import {readCommanderLargeText} from '../src/commander-large-text-r139.mjs';
|
|
9
|
+
|
|
10
|
+
const sha = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
11
|
+
const source=Buffer.from(('deadbyte-0123456789-'+'.'.repeat(87)+'\r\n').repeat(52000));
|
|
12
|
+
assert.ok(source.length > 5*1024*1024);
|
|
13
|
+
|
|
14
|
+
async function withFixture(fn){
|
|
15
|
+
const dir=await mkdtemp(join(tmpdir(),'deadbyte-r141-real-io-'));
|
|
16
|
+
const file=join(dir,'test.log');
|
|
17
|
+
await writeFile(file,source);
|
|
18
|
+
try { await fn(file); } finally { await rm(dir,{recursive:true,force:true}); }
|
|
19
|
+
}
|
|
20
|
+
function diskRuntime(file,{mutateAt=Infinity}={}){
|
|
21
|
+
let reads=0,hashed=0;
|
|
22
|
+
return {
|
|
23
|
+
policy:{max_read_bytes:256*1024},
|
|
24
|
+
get calls(){return {reads,hashed};},
|
|
25
|
+
async read({offset,maxBytes,hashFile=false}){
|
|
26
|
+
reads++;
|
|
27
|
+
if(reads===mutateAt){
|
|
28
|
+
const command='const fs=require("fs");const f=process.argv[1];const fd=fs.openSync(f,"r+");const b=Buffer.from([0x58]);fs.writeSync(fd,b,0,1,3);fs.fsyncSync(fd);fs.closeSync(fd)';
|
|
29
|
+
const child=spawnSync(process.execPath,['-e',command,file],{windowsHide:true});
|
|
30
|
+
assert.equal(child.status,0,child.stderr?.toString());
|
|
31
|
+
}
|
|
32
|
+
const h=await open(file,'r');
|
|
33
|
+
let n=0,part=Buffer.alloc(maxBytes);
|
|
34
|
+
try { ({bytesRead:n}=await h.read(part,0,maxBytes,offset)); } finally {await h.close();}
|
|
35
|
+
part=part.subarray(0,n);
|
|
36
|
+
const current=await readFile(file);
|
|
37
|
+
let fileSha256=null;
|
|
38
|
+
if(hashFile){hashed++;fileSha256=sha(current);}
|
|
39
|
+
return {offset,next_offset:offset+n,file_size:current.length,
|
|
40
|
+
eof:offset+n>=current.length,data_base64:part.toString('base64'),file_sha256:fileSha256};
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
test('R141 5MiB+ physical Windows filesystem read matches independent file digest',async()=>{
|
|
45
|
+
await withFixture(async file=>{
|
|
46
|
+
const runtime=diskRuntime(file);
|
|
47
|
+
const result=await readCommanderLargeText({runtime,rootId:'core',relativePath:file,offset:51998,length:2});
|
|
48
|
+
assert.equal(result.fileSha256,sha(await readFile(file)));
|
|
49
|
+
assert.equal(result.totalLines,52001);
|
|
50
|
+
assert.equal(result.lineCount,2);
|
|
51
|
+
assert.equal(runtime.calls.hashed,1);
|
|
52
|
+
assert.ok(runtime.calls.reads>20);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
test('R141 cross-process same-size on-disk mutation between pages fails closed',async()=>{
|
|
56
|
+
await withFixture(async file=>{
|
|
57
|
+
const before=sha(await readFile(file));
|
|
58
|
+
const runtime=diskRuntime(file,{mutateAt:4});
|
|
59
|
+
await assert.rejects(readCommanderLargeText({runtime,rootId:'core',relativePath:file,offset:51998,length:2}),/identity mismatch/);
|
|
60
|
+
assert.notEqual(sha(await readFile(file)),before);
|
|
61
|
+
assert.equal(runtime.calls.hashed,1);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
test('R141 cross-process late mutation before native closing hash fails closed',async()=>{
|
|
65
|
+
await withFixture(async file=>{
|
|
66
|
+
const pages=Math.ceil(source.length/(256*1024));
|
|
67
|
+
const runtime=diskRuntime(file,{mutateAt:pages+1});
|
|
68
|
+
await assert.rejects(readCommanderLargeText({runtime,rootId:'core',relativePath:file,offset:51998,length:2}),/identity mismatch/);
|
|
69
|
+
assert.equal(runtime.calls.hashed,1);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { generateTrustKeypair } from '../src/trust.mjs';
|
|
9
|
+
import { loadCodingPolicy } from '../src/coding-plane.mjs';
|
|
10
|
+
import { createMachineFsRuntime } from '../src/machine-fs.mjs';
|
|
11
|
+
import { writeBoundStandingGrant } from './r65-authority-fixture.mjs';
|
|
12
|
+
import { readCommanderLargeText } from '../src/commander-large-text-r139.mjs';
|
|
13
|
+
|
|
14
|
+
const digest = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
15
|
+
const line = 'R142-EVIDENCE:' + '.'.repeat(95) + '\r\n';
|
|
16
|
+
const bytes = Buffer.from(line.repeat(50000));
|
|
17
|
+
assert.ok(bytes.length > 5 * 1024 * 1024);
|
|
18
|
+
|
|
19
|
+
async function isolatedFixture({grant=true,read=true}={}) {
|
|
20
|
+
const state = await mkdtemp(path.join(os.tmpdir(), 'deadbyte-r142-authority-'));
|
|
21
|
+
const project = path.join(state,'project');
|
|
22
|
+
await mkdir(project);
|
|
23
|
+
const file = path.join(project,'large.log');
|
|
24
|
+
await writeFile(file,bytes);
|
|
25
|
+
const policyFile = path.join(state,'coding-policy.json');
|
|
26
|
+
const leaseFile = path.join(state,'runtime','coding-grant.json');
|
|
27
|
+
await writeFile(policyFile, JSON.stringify({
|
|
28
|
+
schema:'deadbyte.coding-policy.v1',enabled:true,
|
|
29
|
+
lease_file:leaseFile,evidence_dir:path.join(state,'evidence'),
|
|
30
|
+
roots:{project:{path:project,read,write:false,exec:false,deny_paths:[]}},
|
|
31
|
+
commands:{},max_read_bytes:262144,max_write_bytes:65536
|
|
32
|
+
}));
|
|
33
|
+
const policy = await loadCodingPolicy(policyFile);
|
|
34
|
+
if(grant) await writeBoundStandingGrant('coding', leaseFile, {
|
|
35
|
+
schema:'deadbyte.coding-grant.v2',policy_sha256:policy.policy_sha256,
|
|
36
|
+
instance_nonce:'c'.repeat(64),root_ids:['project'],granted_at_utc:new Date().toISOString()
|
|
37
|
+
});
|
|
38
|
+
const keys = await generateTrustKeypair({
|
|
39
|
+
privateKeyPath:path.join(state,'private.pem'),publicKeyPath:path.join(state,'public.pem')
|
|
40
|
+
});
|
|
41
|
+
return {state,file,policy,keys,runtime:createMachineFsRuntime({
|
|
42
|
+
policy,signingKeyPath:keys.privateKeyPath,verifyKeyPath:keys.publicKeyPath
|
|
43
|
+
})};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function modifyViaOtherProcess(file) {
|
|
47
|
+
const script = 'const fs=require("node:fs");const fd=fs.openSync(process.argv[1],"r+");' +
|
|
48
|
+
'const n=Buffer.from("X");fs.writeSync(fd,n,0,1,14);fs.fsyncSync(fd);fs.closeSync(fd)';
|
|
49
|
+
const child = spawnSync(process.execPath,['-e',script,file],{
|
|
50
|
+
windowsHide:true,encoding:'utf8',timeout:15000
|
|
51
|
+
});
|
|
52
|
+
assert.equal(child.status,0,child.stderr || String(child.error));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function instrument(real,file,{mutateCall=Infinity}={}) {
|
|
56
|
+
let reads=0,hashRequests=0;
|
|
57
|
+
return {
|
|
58
|
+
policy:real.policy,
|
|
59
|
+
counters:()=>({reads,hashRequests}),
|
|
60
|
+
async read(args){
|
|
61
|
+
reads++;
|
|
62
|
+
if(reads===mutateCall) modifyViaOtherProcess(file);
|
|
63
|
+
if(args.hashFile) hashRequests++;
|
|
64
|
+
return real.read(args);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
test('R142 real MachineFS signed isolated read authority, native pages and final SHA',async()=>{
|
|
70
|
+
const f=await isolatedFixture();
|
|
71
|
+
try {
|
|
72
|
+
const runtime=instrument(f.runtime,f.file);
|
|
73
|
+
const result=await readCommanderLargeText({
|
|
74
|
+
runtime,rootId:'project',relativePath:'large.log',offset:49998,length:2
|
|
75
|
+
});
|
|
76
|
+
assert.equal(result.fileSha256,digest(await readFile(f.file)));
|
|
77
|
+
assert.equal(result.bytesScanned,bytes.length);
|
|
78
|
+
assert.equal(result.totalLines,50001);
|
|
79
|
+
assert.equal(result.lineCount,2);
|
|
80
|
+
assert.equal(runtime.counters().hashRequests,1);
|
|
81
|
+
assert.ok(runtime.counters().reads>20);
|
|
82
|
+
} finally {await rm(f.state,{recursive:true,force:true});}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('R142 real MachineFS detects same-size concurrent child writer before an intermediate page',async()=>{
|
|
86
|
+
const f=await isolatedFixture();
|
|
87
|
+
try {
|
|
88
|
+
const runtime=instrument(f.runtime,f.file,{mutateCall:4});
|
|
89
|
+
await assert.rejects(readCommanderLargeText({
|
|
90
|
+
runtime,rootId:'project',relativePath:'large.log',offset:49998,length:2
|
|
91
|
+
}),/large text file changed during paged read: identity mismatch/);
|
|
92
|
+
assert.notEqual(digest(await readFile(f.file)),digest(bytes));
|
|
93
|
+
assert.equal(runtime.counters().hashRequests,1);
|
|
94
|
+
} finally {await rm(f.state,{recursive:true,force:true});}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('R142 real MachineFS detects same-size child writer just before closing hash',async()=>{
|
|
98
|
+
const f=await isolatedFixture();
|
|
99
|
+
try {
|
|
100
|
+
const chunks=Math.ceil(bytes.length/262144);
|
|
101
|
+
const runtime=instrument(f.runtime,f.file,{mutateCall:chunks+1});
|
|
102
|
+
await assert.rejects(readCommanderLargeText({
|
|
103
|
+
runtime,rootId:'project',relativePath:'large.log',offset:49998,length:2
|
|
104
|
+
}),/large text file changed during paged read: identity mismatch/);
|
|
105
|
+
assert.equal(runtime.counters().hashRequests,1);
|
|
106
|
+
} finally {await rm(f.state,{recursive:true,force:true});}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('R142 real MachineFS rejects reads denied by isolated root policy even when disarmed',async()=>{
|
|
110
|
+
const f=await isolatedFixture({grant:false,read:false});
|
|
111
|
+
try {
|
|
112
|
+
await assert.rejects(readCommanderLargeText({
|
|
113
|
+
runtime:f.runtime,rootId:'project',relativePath:'large.log',offset:0,length:1
|
|
114
|
+
}),/does not allow read/i);
|
|
115
|
+
} finally {await rm(f.state,{recursive:true,force:true});}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('R142 disarmed MachineFS permits explicitly policy-authorized read-only access',async()=>{
|
|
119
|
+
const f=await isolatedFixture({grant:false,read:true});
|
|
120
|
+
try {
|
|
121
|
+
const one=await f.runtime.read({rootId:'project',relativePath:'large.log',offset:0,maxBytes:64,hashFile:false});
|
|
122
|
+
assert.equal(Buffer.from(one.data_base64,'base64').toString('utf8'),bytes.subarray(0,64).toString('utf8'));
|
|
123
|
+
await assert.rejects(f.runtime.write({
|
|
124
|
+
rootId:'project',relativePath:'no-write.txt',content:'x',expectedSha256:'absent'
|
|
125
|
+
}),/disarmed|does not allow write|authority/i);
|
|
126
|
+
} finally {await rm(f.state,{recursive:true,force:true});}
|
|
127
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {mkdtemp,rm,readFile,writeFile} from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import {performance} from 'node:perf_hooks';
|
|
7
|
+
import {openRemoteCallClaims} from '../bridge/src/remote-call-claims-r60.mjs';
|
|
8
|
+
|
|
9
|
+
const now=Date.parse('2029-01-01T00:00:00Z');
|
|
10
|
+
const base={device_id:'device-1',session_id:'session-1',request_id:'init',
|
|
11
|
+
tool:'coding_file_write',arguments:{root_id:'fixture',path:'a.txt',content:'A'},
|
|
12
|
+
expires_at_utc:'2030-01-01T00:00:00.000Z',release_id:'v-test',
|
|
13
|
+
generation_manifest_sha256:'a'.repeat(64),machine_id:'b'.repeat(64),
|
|
14
|
+
online_epoch:'c'.repeat(64),classification:'mutating'};
|
|
15
|
+
|
|
16
|
+
test('R144 terminal-heavy recovery ignores closed history and keeps live interruption fail-closed',async()=>{
|
|
17
|
+
const dir=await mkdtemp(path.join(os.tmpdir(),'r144-terminal-'));
|
|
18
|
+
try{
|
|
19
|
+
const store=openRemoteCallClaims({root:dir,clock:()=>now});
|
|
20
|
+
for(let i=0;i<200;i++){
|
|
21
|
+
const rec=await store.claim({...base,request_id:'terminal-'+i});
|
|
22
|
+
await store.dispatch(rec.claim_id);
|
|
23
|
+
await store.terminal(rec.claim_id,{status:'ok',value:i});
|
|
24
|
+
}
|
|
25
|
+
const pending=await store.claim({...base,request_id:'pending'});
|
|
26
|
+
const inflight=await store.claim({...base,request_id:'inflight'});
|
|
27
|
+
await store.dispatch(inflight.claim_id);
|
|
28
|
+
const start=performance.now(), recovered=await store.recover({limit:100});
|
|
29
|
+
const elapsed_ms=Math.round(performance.now()-start);
|
|
30
|
+
assert.deepEqual(new Set(recovered.records.map(x=>x.claim_id)),new Set([pending.claim_id,inflight.claim_id]));
|
|
31
|
+
assert.equal(recovered.next_cursor,null);
|
|
32
|
+
assert.equal((await store.state(inflight.claim_id)).state,'INTERRUPTED');
|
|
33
|
+
assert.equal((await store.state(inflight.claim_id)).side_effect_state,'unknown');
|
|
34
|
+
const replay=await store.claim({...base,request_id:'terminal-77'});
|
|
35
|
+
assert.equal(replay.owner,false);
|
|
36
|
+
assert.equal(replay.state,'TERMINAL');
|
|
37
|
+
console.log('R144_TERMINAL_HISTORY_BENCH',{terminal_count:200,elapsed_ms});
|
|
38
|
+
}finally{await rm(dir,{recursive:true,force:true});}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('R144 terminal marker cannot enable replay when its bytes are forged',async()=>{
|
|
42
|
+
const dir=await mkdtemp(path.join(os.tmpdir(),'r144-forged-'));
|
|
43
|
+
try{
|
|
44
|
+
const store=openRemoteCallClaims({root:dir,clock:()=>now});
|
|
45
|
+
const claim=await store.claim(base);
|
|
46
|
+
await store.dispatch(claim.claim_id);
|
|
47
|
+
await store.terminal(claim.claim_id,{status:'ok'});
|
|
48
|
+
const f=path.join(dir,claim.claim_id.slice(0,2),claim.claim_id+'.terminal.json');
|
|
49
|
+
const bytes=await readFile(f,'utf8');
|
|
50
|
+
await writeFile(f,bytes.replace('"status":"ok"','"status":"forged"'));
|
|
51
|
+
assert.equal((await store.recover({limit:100})).records.length,0);
|
|
52
|
+
await assert.rejects(store.claim(base),/terminal hash corrupt|remote claim state unreadable/);
|
|
53
|
+
await assert.rejects(store.dispatch(claim.claim_id),/already dispatched/);
|
|
54
|
+
}finally{await rm(dir,{recursive:true,force:true});}
|
|
55
|
+
});
|