deadbyte-mcp 0.15.14 → 0.15.16
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 +65 -27
- package/PROVENANCE.json +6 -6
- package/README.txt +6 -6
- package/RELEASE.json +58 -53
- package/docs/COMMANDER_EXHAUSTIVE_SURFACE.json +49 -0
- package/docs/COMMANDER_REFERENCE_INVENTORY.json +70 -0
- package/docs/R47-FULL-TOOL-CATALOG.json +123 -0
- package/package.json +5 -2
- package/scripts/commander-hardening-gate-r47.mjs +35 -0
- package/scripts/commander-parity-benchmark.mjs +6 -0
- package/scripts/commander-real-performance-benchmark-r47.mjs +41 -0
- package/scripts/deploy-r47.mjs +67 -8
- package/scripts/full-regression-r47.mjs +28 -0
- package/scripts/inventory-reference-dist.mjs +17 -0
- package/scripts/npm-release-r47.mjs +25 -1
- package/scripts/r47-syntax-check.mjs +6 -0
- package/scripts/r47-wire-remote-plane.mjs +23 -0
- package/src/atomic-mutation.mjs +59 -29
- package/src/commander-compat-contracts.mjs +90 -0
- package/src/commander-compat-runtime.mjs +91 -0
- package/src/commander-core-bridge.mjs +30 -0
- package/src/commander-extended-runtime.mjs +74 -0
- package/src/commander-observability.mjs +33 -0
- package/src/commander-remote-bridge-r47.mjs +41 -0
- package/src/commander-remote-relay-r47.mjs +44 -0
- package/src/machine-fs.mjs +108 -22
- package/src/mcp-server.mjs +94 -0
- 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 +296 -293
- package/src/runtime-update.mjs +61 -7
- package/src/tool-audit.mjs +15 -2
- package/src/version.mjs +1 -2
- package/test/OBSERVED-COMMANDER-TOOLS.json +1 -0
- package/test/agent-runtime.test.mjs +1 -1
- package/test/autonomous-runtime.test.mjs +1 -1
- package/test/coding-plane.test.mjs +1 -1
- package/test/coding-search-r36.test.mjs +14 -0
- package/test/commander-compat-runtime.test.mjs +34 -0
- package/test/commander-core-bridge-r47.test.mjs +31 -0
- package/test/commander-exhaustive-surface-r47.test.mjs +32 -0
- package/test/commander-extended-runtime-r47.test.mjs +46 -0
- package/test/commander-observability-r47.test.mjs +34 -0
- package/test/commander-parity-gate-r47.test.mjs +49 -0
- package/test/commander-protocol-r47.test.mjs +30 -0
- package/test/commander-real-parity-r47.test.mjs +121 -0
- package/test/commander-remote-bridge-r47.test.mjs +39 -0
- package/test/commander-remote-plane-r47.test.mjs +25 -0
- package/test/commander-torture-r47.test.mjs +146 -0
- package/test/full-max-catalog-r46.test.mjs +1 -1
- package/test/full-max-catalog-r47.test.mjs +78 -0
- package/test/machine-fs-runtime.test.mjs +97 -0
- 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/output-schema.test.mjs +1 -1
- package/test/process-readiness.test.mjs +32 -0
- package/test/process-runtime.test.mjs +147 -0
- package/test/r47-release-contract.test.mjs +37 -0
- package/test/release-activation-gate.test.mjs +30 -0
- package/test/release-version.test.mjs +1 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { registerCommanderRemoteBridge } from '../src/commander-remote-bridge-r47.mjs';
|
|
4
|
+
|
|
5
|
+
function fakeServer(){
|
|
6
|
+
const tools=new Map();
|
|
7
|
+
return {tools,registerTool(name,meta,handler){tools.set(name,{meta,handler});}};
|
|
8
|
+
}
|
|
9
|
+
function fakePlane(){
|
|
10
|
+
const calls=[];
|
|
11
|
+
const result=(op,value)=>{calls.push({op,value});return {status:'ok',op,value};};
|
|
12
|
+
return {calls,pair:async value=>result('pair',value),status:async value=>result('status',value),reportOnline:async value=>result('report_online',value),probe:async value=>result('probe',value),dispatch:async value=>result('dispatch',value),revoke:async value=>result('revoke',value),rotateCredential:async value=>result('rotate_credential',value),list:async()=>result('list',{})};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test('R47 remote Commander bridge routes remote_device directly to remote-device-r44 plane',async()=>{
|
|
16
|
+
const server=fakeServer(),plane=fakePlane();
|
|
17
|
+
registerCommanderRemoteBridge(server,{remoteDevicePlane:plane});
|
|
18
|
+
assert.ok(server.tools.has('remote_device'));assert.ok(server.tools.has('remote_mcp'));
|
|
19
|
+
const tool=server.tools.get('remote_device').handler;const id='a'.repeat(64);
|
|
20
|
+
await tool({op:'pair',public_key_spki_base64:'ZmFrZQ==',display_name:'lab'});
|
|
21
|
+
await tool({op:'status',device_id:id});await tool({op:'report_online',device_id:id,epoch:1,connection_id:'c'});
|
|
22
|
+
await tool({op:'probe',device_id:id,epoch:1});await tool({op:'dispatch',device_id:id,epoch:1,capability_id:'machine.status',payload:{},request_id:'b'.repeat(64)});
|
|
23
|
+
await tool({op:'revoke',device_id:id,epoch:1});await tool({op:'rotate_credential',device_id:id,epoch:1,encrypted_bundle_base64:'dG9rZW4=',expected_previous_sha256:null});await tool({op:'list'});
|
|
24
|
+
assert.deepEqual(plane.calls.map(x=>x.op),['pair','status','report_online','probe','dispatch','revoke','rotate_credential','list']);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('R47 remote_mcp is a thin dispatch surface and never shells out to Commander',async()=>{
|
|
28
|
+
const server=fakeServer(),plane=fakePlane();registerCommanderRemoteBridge(server,{remoteDevicePlane:plane});
|
|
29
|
+
const id='c'.repeat(64),request='d'.repeat(64);
|
|
30
|
+
const result=await server.tools.get('remote_mcp').handler({device_id:id,epoch:7,capability_id:'coding.file_read',payload:{path:'README.md'},request_id:request});
|
|
31
|
+
assert.equal(result.status,'ok');
|
|
32
|
+
assert.deepEqual(plane.calls,[{op:'dispatch',value:{deviceId:id,epoch:7,capabilityId:'coding.file_read',payload:{path:'README.md'},requestId:request}}]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('R47 remote surfaces fail closed as unavailable when no remote-device-r44 plane is wired',async()=>{
|
|
36
|
+
const server=fakeServer();registerCommanderRemoteBridge(server,{});
|
|
37
|
+
const result=await server.tools.get('remote_device').handler({op:'list'});
|
|
38
|
+
assert.equal(result.status,'unavailable');assert.match(result.error,/remote-device-r44 plane is not configured/);
|
|
39
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
const sha=value=>createHash('sha256').update(value).digest('hex');
|
|
7
|
+
|
|
8
|
+
test('R47 production server wires native remote device plane',async()=>{
|
|
9
|
+
const server=await readFile(new URL('../src/mcp-server.mjs',import.meta.url),'utf8');
|
|
10
|
+
assert.match(server,/createRemoteDevicePlaneR44/);
|
|
11
|
+
assert.match(server,/createCommanderRemoteRelayR47/);
|
|
12
|
+
assert.match(server,/registerCommanderRemoteBridge/);
|
|
13
|
+
assert.match(server,/DEADBYTE_REMOTE_RELAY_ENDPOINT\|\|process\.env\.DEADBYTE_REMOTE_ENDPOINT/);
|
|
14
|
+
assert.match(server,/DEADBYTE_REMOTE_CAPABILITIES/);
|
|
15
|
+
assert.match(server,/remote-device-r44/);
|
|
16
|
+
assert.equal((server.match(/commander-remote-relay-r47\.mjs/g)??[]).length,1);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('R47 relay is native MCP client and contains no Desktop Commander dependency',async()=>{
|
|
20
|
+
const relay=await readFile(new URL('../src/commander-remote-relay-r47.mjs',import.meta.url),'utf8');
|
|
21
|
+
assert.match(relay,/StreamableHTTPClientTransport/);
|
|
22
|
+
assert.match(relay,/client\.callTool/);
|
|
23
|
+
assert.doesNotMatch(relay,/@wonderwhy-er\/desktop-commander/);
|
|
24
|
+
assert.equal(sha(relay).length,64);
|
|
25
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { generateTrustKeypair } from '../src/trust.mjs';
|
|
7
|
+
import { loadCodingPolicy } from '../src/coding-plane.mjs';
|
|
8
|
+
import { createMachineFsRuntime } from '../src/machine-fs.mjs';
|
|
9
|
+
import { PROCESS_GRANT_SCHEMA, PROCESS_POLICY_SCHEMA, loadProcessPolicy } from '../src/process-policy.mjs';
|
|
10
|
+
import { createProcessRuntime } from '../src/process-runtime.mjs';
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
|
|
13
|
+
const sha=b=>createHash('sha256').update(b).digest('hex');
|
|
14
|
+
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
|
|
15
|
+
|
|
16
|
+
async function fsFixture(){
|
|
17
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-torture-fs-')), project=path.join(root,'project');
|
|
18
|
+
await mkdir(project);
|
|
19
|
+
const policyPath=path.join(root,'policy.json'), grantPath=path.join(root,'grant.json');
|
|
20
|
+
await writeFile(policyPath,JSON.stringify({schema:'deadbyte.coding-policy.v1',enabled:true,lease_file:grantPath,evidence_dir:path.join(root,'evidence'),roots:{project:{path:project,read:true,write:true,exec:false,deny_paths:['blocked']}},commands:{},max_read_bytes:65536,max_write_bytes:65536}));
|
|
21
|
+
const policy=await loadCodingPolicy(policyPath);
|
|
22
|
+
await writeFile(grantPath,JSON.stringify({schema:'deadbyte.coding-grant.v2',policy_sha256:policy.policy_sha256,instance_nonce:'c'.repeat(64),root_ids:['project'],granted_at_utc:new Date().toISOString()}));
|
|
23
|
+
const keys=await generateTrustKeypair({privateKeyPath:path.join(root,'private.pem'),publicKeyPath:path.join(root,'public.pem')});
|
|
24
|
+
return {root,project,runtime:createMachineFsRuntime({policy,signingKeyPath:keys.privateKeyPath,verifyKeyPath:keys.publicKeyPath})};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function processFixture(){
|
|
28
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-torture-proc-')), project=path.join(root,'project'),trust=path.join(root,'trust');
|
|
29
|
+
await mkdir(project); await mkdir(trust);
|
|
30
|
+
const host=path.resolve('bin/deadbyte-process-host.exe'), exe='C:\\Windows\\System32\\cmd.exe';
|
|
31
|
+
const grantFile=path.join(root,'grant.json'), stateDir=path.join(root,'state'), policyPath=path.join(root,'policy.json');
|
|
32
|
+
await writeFile(policyPath,JSON.stringify({schema:PROCESS_POLICY_SCHEMA,enabled:true,grant_file:grantFile,state_dir:stateDir,native_host:{path:host,sha256:sha(await readFile(host))},roots:{core:{path:project,deny_paths:[]}},profiles:{
|
|
33
|
+
echo:{executable:exe,executable_sha256:sha(await readFile(exe)),root_ids:['core'],prefix_args:['/d','/c'],max_args:4,max_arg_bytes:256,max_sessions:8,max_input_bytes:4096,max_retained_output_bytes:65536,env_allow:['PATH','TEMP','TMP'],input_actions:{}},
|
|
34
|
+
hold:{executable:exe,executable_sha256:sha(await readFile(exe)),root_ids:['core'],transport:'pipe',prefix_args:['/d','/q'],max_args:0,max_arg_bytes:256,max_sessions:8,max_input_bytes:4096,max_retained_output_bytes:65536,env_allow:['PATH','TEMP','TMP'],input_actions:{}}
|
|
35
|
+
}}));
|
|
36
|
+
const policy=await loadProcessPolicy(policyPath);
|
|
37
|
+
await writeFile(grantFile,JSON.stringify({schema:PROCESS_GRANT_SCHEMA,policy_sha256:policy.policy_sha256,instance_nonce:'d'.repeat(64),profile_ids:['echo','hold'],root_ids:['core'],granted_at_utc:new Date().toISOString()}));
|
|
38
|
+
const keys=await generateTrustKeypair({privateKeyPath:path.join(trust,'private.pem'),publicKeyPath:path.join(trust,'public.pem')});
|
|
39
|
+
return {root,runtime:createProcessRuntime({policy,signingKeyPath:keys.privateKeyPath,verifyKeyPath:keys.publicKeyPath})};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test('torture: concurrent stale writers permit exactly one winner per version',async()=>{
|
|
43
|
+
const f=await fsFixture();
|
|
44
|
+
try{
|
|
45
|
+
let expected='absent', winners=0, conflicts=0;
|
|
46
|
+
for(let round=0;round<12;round++){
|
|
47
|
+
const target='race.txt';
|
|
48
|
+
const before=await f.runtime.state({rootId:'project',relativePath:target});
|
|
49
|
+
expected=before.exists?before.sha256:'absent';
|
|
50
|
+
const results=await Promise.allSettled(Array.from({length:8},(_,i)=>f.runtime.write({rootId:'project',relativePath:target,content:`round=${round};writer=${i}`,expectedSha256:expected})));
|
|
51
|
+
const ok=results.filter(x=>x.status==='fulfilled'&&x.value.status==='committed');
|
|
52
|
+
winners+=ok.length; conflicts+=results.filter(x=>x.status==='rejected').length;
|
|
53
|
+
assert.equal(ok.length,1);
|
|
54
|
+
assert.equal(conflicts>=round+7,true);
|
|
55
|
+
const current=await f.runtime.stat({rootId:'project',relativePath:target,hash:true});
|
|
56
|
+
assert.equal(current.sha256,ok[0].value.sha256);
|
|
57
|
+
}
|
|
58
|
+
assert.equal(winners,12);
|
|
59
|
+
} finally { await rm(f.root,{recursive:true,force:true}); }
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('torture: move/delete races cannot duplicate or silently lose an object',async()=>{
|
|
63
|
+
const f=await fsFixture();
|
|
64
|
+
try{
|
|
65
|
+
await f.runtime.write({rootId:'project',relativePath:'source.txt',content:'immutable',expectedSha256:'absent'});
|
|
66
|
+
const s=await f.runtime.stat({rootId:'project',relativePath:'source.txt',hash:true});
|
|
67
|
+
const [a,b]=await Promise.allSettled([
|
|
68
|
+
f.runtime.move({rootId:'project',sourcePath:'source.txt',destinationPath:'a.txt',expectedSourceIdentity:s.identity_sha256}),
|
|
69
|
+
f.runtime.move({rootId:'project',sourcePath:'source.txt',destinationPath:'b.txt',expectedSourceIdentity:s.identity_sha256})
|
|
70
|
+
]);
|
|
71
|
+
assert.equal([a,b].filter(x=>x.status==='fulfilled').length,1);
|
|
72
|
+
const stateA=await f.runtime.state({rootId:'project',relativePath:'a.txt'});
|
|
73
|
+
const stateB=await f.runtime.state({rootId:'project',relativePath:'b.txt'});
|
|
74
|
+
assert.equal(Number(stateA.exists)+Number(stateB.exists),1);
|
|
75
|
+
const winner=stateA.exists?stateA:stateB;
|
|
76
|
+
assert.equal(winner.sha256,sha(Buffer.from('immutable')));
|
|
77
|
+
} finally { await rm(f.root,{recursive:true,force:true}); }
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('torture: pagination remains deterministic under concurrent directory churn',async()=>{
|
|
81
|
+
const f=await fsFixture();
|
|
82
|
+
try{
|
|
83
|
+
for(let i=0;i<40;i++) await writeFile(path.join(f.project,`f${String(i).padStart(2,'0')}.txt`),String(i));
|
|
84
|
+
let cursor=null;
|
|
85
|
+
const first=await f.runtime.list({rootId:'project',relativePath:'.',limit:7,cursor});
|
|
86
|
+
assert.equal(first.entries.length,7);
|
|
87
|
+
cursor=first.next_cursor;
|
|
88
|
+
const churn=(async()=>{for(let i=0;i<20;i++){
|
|
89
|
+
const p=path.join(f.project,"z"+i+".tmp");
|
|
90
|
+
await writeFile(p,'z');
|
|
91
|
+
await rm(p,{force:true});
|
|
92
|
+
}
|
|
93
|
+
await writeFile(path.join(f.project,'persistent-churn.txt'),'changed');})();
|
|
94
|
+
await churn;
|
|
95
|
+
await assert.rejects(
|
|
96
|
+
()=>f.runtime.list({rootId:'project',relativePath:'.',limit:7,cursor}),
|
|
97
|
+
/cursor snapshot changed/i
|
|
98
|
+
);
|
|
99
|
+
} finally {
|
|
100
|
+
for (let i=0;i<20;i++) {
|
|
101
|
+
try { await rm(f.root,{recursive:true,force:true}); break; }
|
|
102
|
+
catch (error) { if (error?.code!=='EBUSY' && error?.code!=='ENOTEMPTY') throw error; await sleep(50); }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('torture: process cancellation race converges to one verified terminal state', {skip:process.platform!=='win32'}, async()=>{
|
|
108
|
+
const f=await processFixture();
|
|
109
|
+
try{
|
|
110
|
+
const s=await f.runtime.start({profileId:'hold',rootId:'core',cwd:'.',args:[]});
|
|
111
|
+
const results=await Promise.allSettled([
|
|
112
|
+
f.runtime.cancel({sessionId:s.session_id,reason:'race-a'}),
|
|
113
|
+
f.runtime.cancel({sessionId:s.session_id,reason:'race-b'}),
|
|
114
|
+
f.runtime.status({sessionId:s.session_id})
|
|
115
|
+
]);
|
|
116
|
+
const cancels=results.filter(x=>x.status==='fulfilled'&&x.value.termination);
|
|
117
|
+
assert.ok(cancels.length>=1);
|
|
118
|
+
const deadline=Date.now()+5000; let status=await f.runtime.status({sessionId:s.session_id});
|
|
119
|
+
while(status.state==='running'&&Date.now()<deadline){await sleep(20);status=await f.runtime.status({sessionId:s.session_id});}
|
|
120
|
+
assert.notEqual(status.state,'running');
|
|
121
|
+
assert.ok(status.operation_receipt); assert.ok(status.operation_id);
|
|
122
|
+
assert.equal((await f.runtime.verifyEventJournal({sessionId:s.session_id})).ok,true);
|
|
123
|
+
assert.equal((await f.runtime.verifyOperationEvidence({sessionId:s.session_id})).ok,true);
|
|
124
|
+
} finally { await rm(f.root,{recursive:true,force:true}); }
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('torture: process session identity cannot be replaced by a foreign session id', {skip:process.platform!=='win32'}, async()=>{
|
|
128
|
+
const f=await processFixture();
|
|
129
|
+
try{
|
|
130
|
+
const a=await f.runtime.start({profileId:'echo',rootId:'core',cwd:'.',args:['echo','A']});
|
|
131
|
+
const b=await f.runtime.start({profileId:'echo',rootId:'core',cwd:'.',args:['echo','B']});
|
|
132
|
+
await assert.rejects(()=>f.runtime.read({sessionId:'f'.repeat(64),offset:0,maxBytes:64}),/session|not found|unknown/i);
|
|
133
|
+
await f.runtime.cancel({sessionId:a.session_id,reason:'cleanup'});
|
|
134
|
+
await f.runtime.cancel({sessionId:b.session_id,reason:'cleanup'});
|
|
135
|
+
for (const id of [a.session_id,b.session_id]) {
|
|
136
|
+
let s=await f.runtime.status({sessionId:id});
|
|
137
|
+
for(let i=0;i<100&&s.state==='running';i++){await sleep(20);s=await f.runtime.status({sessionId:id});}
|
|
138
|
+
assert.notEqual(s.state,'running');
|
|
139
|
+
}
|
|
140
|
+
for(let i=0;i<20;i++){
|
|
141
|
+
try { await rm(f.root,{recursive:true,force:true}); return; }
|
|
142
|
+
catch(error){ if(error?.code!=='EBUSY') throw error; await sleep(50); }
|
|
143
|
+
}
|
|
144
|
+
await rm(f.root,{recursive:true,force:true});
|
|
145
|
+
} finally { await rm(f.root,{recursive:true,force:true}); }
|
|
146
|
+
});
|
|
@@ -15,7 +15,7 @@ const desktopPolicyPath=path.join(repoRoot,'controller','deadbyte-desktop-policy
|
|
|
15
15
|
const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
16
16
|
const absent=async p=>{try{await access(p);return false;}catch(e){if(e?.code==='ENOENT')return true;throw e;}};
|
|
17
17
|
|
|
18
|
-
test('R46
|
|
18
|
+
test.skip('R46 historical max-runtime full stdio catalog contract is frozen at 84; current R47 runtime intentionally supersedes it',async()=>{
|
|
19
19
|
const f=await createR34Fixture({authority:'disarmed'});
|
|
20
20
|
const processPolicyPath=path.join(f.root,'process-policy.json');
|
|
21
21
|
const processGrant=path.join(f.root,'process-grant.json');
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { Client } from '@modelcontextprotocol/client';
|
|
8
|
+
import { StdioClientTransport, getDefaultEnvironment } from '@modelcontextprotocol/client/stdio';
|
|
9
|
+
import { createR34Fixture } from './helpers/autonomous-r34-fixture.mjs';
|
|
10
|
+
|
|
11
|
+
const here=path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const repoRoot=path.resolve(here,'..');
|
|
13
|
+
const serverPath=path.join(repoRoot,'src','mcp-server.mjs');
|
|
14
|
+
const desktopPolicyPath=path.join(repoRoot,'controller','deadbyte-desktop-policy.json');
|
|
15
|
+
const catalogPath=path.join(repoRoot,'docs','R47-FULL-TOOL-CATALOG.json');
|
|
16
|
+
const absent=async p=>{try{await access(p);return false;}catch(e){if(e?.code==='ENOENT')return true;throw e;}};
|
|
17
|
+
|
|
18
|
+
test('R47 runtime full stdio catalog exactly matches canonical registration surface',async()=>{
|
|
19
|
+
const canonical=JSON.parse(await readFile(catalogPath,'utf8'));
|
|
20
|
+
assert.equal(canonical.schema,'deadbyte.r47-full-catalog.v1');
|
|
21
|
+
assert.equal(canonical.train,'R47');
|
|
22
|
+
assert.ok(Array.isArray(canonical.tools));
|
|
23
|
+
assert.equal(canonical.tool_count,canonical.tools.length);
|
|
24
|
+
assert.equal(new Set(canonical.tools).size,canonical.tools.length);
|
|
25
|
+
assert.deepEqual([...canonical.tools].sort(),canonical.tools);
|
|
26
|
+
|
|
27
|
+
const f=await createR34Fixture({authority:'disarmed'});
|
|
28
|
+
const processPolicyPath=path.join(f.root,'process-policy.json');
|
|
29
|
+
const processGrant=path.join(f.root,'process-grant.json');
|
|
30
|
+
const processState=path.join(f.root,'process-state');
|
|
31
|
+
const hostPolicyPath=path.join(f.root,'host-policy.json');
|
|
32
|
+
const hostArm=path.join(f.root,'host-arm.json');
|
|
33
|
+
const desktopRaw=JSON.parse(await readFile(desktopPolicyPath,'utf8'));
|
|
34
|
+
const desktopGrant=desktopRaw.grant_file;
|
|
35
|
+
await mkdir(processState,{recursive:true});
|
|
36
|
+
const nodeSha=createHash('sha256').update(await readFile(process.execPath)).digest('hex');
|
|
37
|
+
await writeFile(processPolicyPath,JSON.stringify({
|
|
38
|
+
schema:'deadbyte.process-policy.v1',enabled:true,grant_file:processGrant,state_dir:processState,
|
|
39
|
+
native_host:{path:process.execPath,sha256:nodeSha},
|
|
40
|
+
roots:{core:{path:f.project,deny_paths:[]}},
|
|
41
|
+
profiles:{node:{executable:process.execPath,executable_sha256:nodeSha,root_ids:['core'],prefix_args:[],transport:'pipe',max_args:8,max_arg_bytes:1024,max_sessions:2,max_input_bytes:65536,max_retained_output_bytes:262144,env_allow:['PATH','TEMP','TMP'],input_actions:{ping:'PING\\r\\n'}}}
|
|
42
|
+
},null,2));
|
|
43
|
+
await writeFile(hostPolicyPath,JSON.stringify({
|
|
44
|
+
schema:'deadbyte.host-policy.v1',enabled:true,arm_file:hostArm,max_read_bytes:262144,max_write_bytes:1048576,
|
|
45
|
+
roots:{core:{path:f.project,read:true,write:true,exec:true}},
|
|
46
|
+
commands:{node_script:{kind:'script',executable:process.execPath,executable_sha256:nodeSha,root_ids:['core'],prefix_args:[],extensions:['.mjs'],timeout_ms:5000,max_output_bytes:65536}}
|
|
47
|
+
},null,2));
|
|
48
|
+
const codingLease=f.codingPolicy.lease_file;
|
|
49
|
+
const autonomousLease=f.policy.lease_file;
|
|
50
|
+
for(const q of [processGrant,hostArm,desktopGrant,codingLease,autonomousLease]) assert.equal(await absent(q),true,'authority unexpectedly armed before start: '+q);
|
|
51
|
+
|
|
52
|
+
const client=new Client({name:'deadbyte-r47-full-catalog',version:'0.15.16'},{versionNegotiation:{mode:{pin:'2026-07-28'}}});
|
|
53
|
+
const env={
|
|
54
|
+
...getDefaultEnvironment(),DEADBYTE_CAPABILITY_PROFILE:'full',DEADBYTE_AUDIT_RUNTIME:'0',
|
|
55
|
+
DEADBYTE_STATE_ROOT:path.join(f.root,'state'),DEADBYTE_WORKSPACE_ROOT:f.root,
|
|
56
|
+
DEADBYTE_SIGNING_KEY:f.keys.privateKeyPath,DEADBYTE_VERIFY_KEY:f.keys.publicKeyPath,
|
|
57
|
+
DEADBYTE_PRIVILEGED_POLICY:hostPolicyPath,DEADBYTE_MACHINE_RUNTIME:'1',DEADBYTE_AGENT_RUNTIME:'1',
|
|
58
|
+
DEADBYTE_PROCESS_RUNTIME:'1',DEADBYTE_PROCESS_POLICY:processPolicyPath,DEADBYTE_CODING_RUNTIME:'1',
|
|
59
|
+
DEADBYTE_CODING_POLICY:path.join(f.root,'coding-policy.json'),DEADBYTE_MACHINE_FS_RUNTIME:'1',
|
|
60
|
+
DEADBYTE_OBSERVATION_RUNTIME:'1',DEADBYTE_DESKTOP_RUNTIME:'1',DEADBYTE_DESKTOP_POLICY:desktopPolicyPath,
|
|
61
|
+
DEADBYTE_AUTONOMOUS_RUNTIME:'1',DEADBYTE_AUTONOMOUS_POLICY:path.join(f.root,'autonomous-policy.json'),
|
|
62
|
+
DEADBYTE_AUTONOMOUS_SUPERVISOR_MODE:'external'
|
|
63
|
+
};
|
|
64
|
+
const transport=new StdioClientTransport({command:process.execPath,args:[serverPath],cwd:repoRoot,env,stderr:'pipe'});
|
|
65
|
+
try{
|
|
66
|
+
await client.connect(transport);
|
|
67
|
+
const listed=await client.listTools();
|
|
68
|
+
const names=listed.tools.map(t=>t.name).sort();
|
|
69
|
+
assert.deepEqual(names,canonical.tools);
|
|
70
|
+
assert.equal(new Set(names).size,names.length,'runtime duplicate tool names');
|
|
71
|
+
for(const tool of listed.tools) if(tool.outputSchema!==undefined) assert.equal(tool.outputSchema?.additionalProperties,false,'non-strict output '+tool.name);
|
|
72
|
+
for(const name of ['agent_task_submit','autonomous_goal_submit','coding_task_submit','process_session_start','machine_fs_write','machine_search_start','desktop_action','host_exec','native_diff_apply','skill_catalog']) assert.ok(names.includes(name),'missing R47 surface '+name);
|
|
73
|
+
} finally {
|
|
74
|
+
await client.close().catch(()=>{});
|
|
75
|
+
for(const q of [processGrant,hostArm,desktopGrant,codingLease,autonomousLease]) assert.equal(await absent(q),true,'authority file appeared during catalog probe: '+q);
|
|
76
|
+
await f.cleanup();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
@@ -125,3 +125,100 @@ test('R46 machine fs read timeout kills a hung read worker instead of abandoning
|
|
|
125
125
|
);
|
|
126
126
|
assert.ok(Date.now() - started < 3000, 'hung worker was not terminated promptly');
|
|
127
127
|
});
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
test('R47 machine fs list cursor is signed, canonical, and snapshot-bound', async () => {
|
|
131
|
+
const f = await fixture();
|
|
132
|
+
for (const name of ['c.txt', 'a.txt', 'b.txt']) await writeFile(path.join(f.project, name), name);
|
|
133
|
+
const first = await f.runtime.list({ rootId:'project', relativePath:'.', limit:2 });
|
|
134
|
+
assert.match(first.next_cursor, /^[A-Za-z0-9_-]+$/);
|
|
135
|
+
const raw = JSON.parse(Buffer.from(first.next_cursor, 'base64url').toString('utf8'));
|
|
136
|
+
assert.equal(raw.schema, 'deadbyte.machine-fs-list-cursor.v3');
|
|
137
|
+
assert.equal(raw.attestation.algorithm, 'ed25519');
|
|
138
|
+
const tamperedBody = { ...raw, after_name: 'c.txt' };
|
|
139
|
+
const tampered = Buffer.from(JSON.stringify(tamperedBody), 'utf8').toString('base64url');
|
|
140
|
+
await assert.rejects(
|
|
141
|
+
() => f.runtime.list({ rootId:'project', relativePath:'.', limit:2, cursor:tampered }),
|
|
142
|
+
/cursor (invalid|signature)/i
|
|
143
|
+
);
|
|
144
|
+
await writeFile(path.join(f.project, 'd.txt'), 'd');
|
|
145
|
+
await assert.rejects(
|
|
146
|
+
() => f.runtime.list({ rootId:'project', relativePath:'.', limit:2, cursor:first.next_cursor }),
|
|
147
|
+
/cursor snapshot changed/i
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
test('R47 machine fs signed cursor rejects reordered canonical envelope', async () => {
|
|
153
|
+
const f = await fixture();
|
|
154
|
+
for (const name of ['a.txt', 'b.txt', 'c.txt']) await writeFile(path.join(f.project, name), name);
|
|
155
|
+
const first = await f.runtime.list({ rootId:'project', relativePath:'.', limit:2 });
|
|
156
|
+
const raw = JSON.parse(Buffer.from(first.next_cursor, 'base64url').toString('utf8'));
|
|
157
|
+
const reordered = {
|
|
158
|
+
attestation: raw.attestation,
|
|
159
|
+
after_name: raw.after_name,
|
|
160
|
+
snapshot_sha256: raw.snapshot_sha256,
|
|
161
|
+
path: raw.path,
|
|
162
|
+
schema: raw.schema
|
|
163
|
+
};
|
|
164
|
+
const cursor = Buffer.from(JSON.stringify(reordered), 'utf8').toString('base64url');
|
|
165
|
+
await assert.rejects(
|
|
166
|
+
() => f.runtime.list({ rootId:'project', relativePath:'.', limit:2, cursor }),
|
|
167
|
+
/cursor invalid/i
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('R47 machine fs move rejects destination race without losing source identity', async () => {
|
|
172
|
+
const f = await fixture();
|
|
173
|
+
const source = path.join(f.project, 'source.txt');
|
|
174
|
+
const destination = path.join(f.project, 'destination.txt');
|
|
175
|
+
await writeFile(source, 'source-data');
|
|
176
|
+
const before = await f.runtime.stat({ rootId:'project', relativePath:'source.txt', hash:true });
|
|
177
|
+
await writeFile(destination, 'occupied');
|
|
178
|
+
await assert.rejects(
|
|
179
|
+
() => f.runtime.move({
|
|
180
|
+
rootId:'project', sourcePath:'source.txt', destinationPath:'destination.txt',
|
|
181
|
+
expectedSourceIdentity: before.identity_sha256
|
|
182
|
+
}),
|
|
183
|
+
/destination precondition/i
|
|
184
|
+
);
|
|
185
|
+
assert.equal(await readFile(source, 'utf8'), 'source-data');
|
|
186
|
+
assert.equal(await readFile(destination, 'utf8'), 'occupied');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('R47 machine fs delete recovery preserves identity when receipt persistence fails', async () => {
|
|
190
|
+
const f = await fixture();
|
|
191
|
+
await writeFile(path.join(f.project, 'recover.txt'), 'recover-data');
|
|
192
|
+
const before = await f.runtime.stat({ rootId:'project', relativePath:'recover.txt', hash:true });
|
|
193
|
+
const blockedEvidence = path.join(f.project, 'evidence-blocked');
|
|
194
|
+
await writeFile(blockedEvidence, 'not-a-directory');
|
|
195
|
+
const originalEvidence = f.policy.evidence_dir;
|
|
196
|
+
f.policy.evidence_dir = blockedEvidence;
|
|
197
|
+
await assert.rejects(
|
|
198
|
+
() => f.runtime.delete({ rootId:'project', relativePath:'recover.txt', expectedSourceIdentity:before.identity_sha256 }),
|
|
199
|
+
/ENOTDIR|not a directory|evidence/i
|
|
200
|
+
);
|
|
201
|
+
f.policy.evidence_dir = originalEvidence;
|
|
202
|
+
assert.equal(await readFile(path.join(f.project, 'recover.txt'), 'utf8'), 'recover-data');
|
|
203
|
+
const after = await f.runtime.stat({ rootId:'project', relativePath:'recover.txt', hash:true });
|
|
204
|
+
assert.equal(after.identity_sha256, before.identity_sha256);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('R47 machine fs read_many is bounded, deterministic, and rejects duplicate paths', async () => {
|
|
208
|
+
const f = await fixture();
|
|
209
|
+
await writeFile(path.join(f.project, 'a.txt'), 'alpha');
|
|
210
|
+
await writeFile(path.join(f.project, 'b.txt'), 'bravo');
|
|
211
|
+
const batch = await f.runtime.readMany({ rootId:'project', paths:['a.txt','b.txt'], maxBytes:3 });
|
|
212
|
+
assert.equal(batch.status, 'ok');
|
|
213
|
+
assert.equal(batch.count, 2);
|
|
214
|
+
assert.deepEqual(batch.results.map(x => x.path), ['a.txt','b.txt']);
|
|
215
|
+
assert.deepEqual(batch.results.map(x => Buffer.from(x.data_base64, 'base64').toString('utf8')), ['alp','bra']);
|
|
216
|
+
await assert.rejects(
|
|
217
|
+
() => f.runtime.readMany({ rootId:'project', paths:['a.txt','a.txt'] }),
|
|
218
|
+
/duplicate path/i
|
|
219
|
+
);
|
|
220
|
+
await assert.rejects(
|
|
221
|
+
() => f.runtime.readMany({ rootId:'project', paths:Array.from({length:65}, (_,i)=>`x-${i}.txt`) }),
|
|
222
|
+
/1-64/i
|
|
223
|
+
);
|
|
224
|
+
});
|
|
@@ -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
|
+
|
|
@@ -48,7 +48,7 @@ async function fixture() {
|
|
|
48
48
|
return { trust, client, transport };
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
test('R44
|
|
51
|
+
test.skip('R44 historical base-plus-agent catalog is frozen at 13; current R47 runtime supersedes it', async () => {
|
|
52
52
|
const f = await fixture();
|
|
53
53
|
try {
|
|
54
54
|
await f.client.connect(f.transport);
|
|
@@ -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
|
+
|