deadbyte-mcp 0.15.50 → 0.15.52
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 -17
- package/PROVENANCE.json +29 -11
- package/README.txt +18 -18
- package/RELEASE.json +392 -368
- package/SBOM.cdx.json +9 -4
- package/TEST-MATRIX.json +32 -2
- package/docs/R132-MANIFEST-PORTABILITY.md +23 -0
- 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/scripts/npm-prepublish-contract-r74.mjs +8 -2
- package/scripts/release-manifest.mjs +15 -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/release-slot.mjs +13 -3
- 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 +7 -7
- package/test/r131-installed-manifest-boundary.test.mjs +65 -0
- package/test/r132-release-identity.test.mjs +44 -0
- 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/r59-tool-surface-compat.test.mjs +1 -1
- package/test/r74-prepublish-contract.test.mjs +6 -4
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {mkdtemp,mkdir,rm,readFile,writeFile} from 'node:fs/promises';
|
|
4
|
+
import {createHash} from 'node:crypto';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import {deriveIsolatedAutonomousScope} from '../src/autonomous-scope-r135.mjs';
|
|
8
|
+
import {assertAutonomousBootstrapRoots} from '../src/runtime-update.mjs';
|
|
9
|
+
const hash=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
10
|
+
async function fixture(run){
|
|
11
|
+
const base=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r135-scope-'));
|
|
12
|
+
try{
|
|
13
|
+
const core=path.join(base,'safe-project'),scratch=path.join(base,'child-state');
|
|
14
|
+
await mkdir(core);await mkdir(scratch);
|
|
15
|
+
const original={schema:'deadbyte.coding-policy.v1',enabled:true,
|
|
16
|
+
lease_file:path.join(base,'parent-coding-grant.json'),
|
|
17
|
+
evidence_dir:path.join(base,'parent-evidence'),
|
|
18
|
+
roots:{core:{path:core,read:true,write:true,exec:true,deny_paths:[]},
|
|
19
|
+
local_c:{path:path.parse(base).root,read:true,write:true,exec:true,deny_paths:[]},
|
|
20
|
+
downloads:{path:path.join(base,'downloads'),read:true,write:true,exec:true,deny_paths:[]}},
|
|
21
|
+
commands:{node_script:{executable:'node',args:[],root_ids:['core','local_c']},unused_command:{executable:'cmd',args:[],root_ids:['local_c']}}};
|
|
22
|
+
const autonomous={schema:'deadbyte.autonomous-policy.v1',enabled:true,
|
|
23
|
+
lease_file:path.join(base,'parent-autonomous-grant.json'),evidence_dir:path.join(base,'old-auto-evidence'),
|
|
24
|
+
coding_policy_sha256:'a'.repeat(64),root_ids:['core'],
|
|
25
|
+
planner:{backend:'client_sampling',max_tokens:2048},
|
|
26
|
+
profiles:{quick:{kind:'command',root_id:'core',command_id:'node_script',
|
|
27
|
+
script_path:'test.mjs',script_sha256:'b'.repeat(64)}}};
|
|
28
|
+
await run({base,core,scratch,original,autonomous});
|
|
29
|
+
}finally{await rm(base,{recursive:true,force:true})}
|
|
30
|
+
}
|
|
31
|
+
test('R135 projects only the chosen project root and its pinned commands without modifying manual Coding',async()=>{
|
|
32
|
+
await fixture(async({scratch,original,autonomous})=>{
|
|
33
|
+
const before=JSON.stringify(original),beforeAutonomous=JSON.stringify(autonomous);
|
|
34
|
+
const result=deriveIsolatedAutonomousScope({manualCodingPolicy:original,autonomousPolicy:autonomous,
|
|
35
|
+
scopeRootId:'core',isolatedStateRoot:scratch});
|
|
36
|
+
assert.deepEqual(Object.keys(result.codingPolicy.roots),['core']);
|
|
37
|
+
assert.deepEqual(Object.keys(result.codingPolicy.commands),['node_script']);
|
|
38
|
+
assert.equal(result.autonomousPolicy.coding_policy_sha256,hash(result.codingBytes));
|
|
39
|
+
assert.deepEqual(result.autonomousPolicy.root_ids,['core']);
|
|
40
|
+
assert.deepEqual(assertAutonomousBootstrapRoots(result.autonomousPolicy,result.codingPolicy,hash(result.codingBytes)),['core']);
|
|
41
|
+
assert.equal(JSON.stringify(original),before);
|
|
42
|
+
assert.equal(JSON.stringify(autonomous),beforeAutonomous);
|
|
43
|
+
assert.notEqual(result.codingPolicy.lease_file,original.lease_file);
|
|
44
|
+
assert.notEqual(result.autonomousPolicy.lease_file,autonomous.lease_file);
|
|
45
|
+
await assert.rejects(()=>readFile(result.codingPolicy.lease_file),{code:'ENOENT'});
|
|
46
|
+
await assert.rejects(()=>readFile(result.autonomousPolicy.lease_file),{code:'ENOENT'});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
test('R135 scope projector refuses direct whole-drive root and does not weaken R70 security gate',async()=>{
|
|
50
|
+
await fixture(async({scratch,original,autonomous})=>{
|
|
51
|
+
const wide={...autonomous,root_ids:['local_c'],
|
|
52
|
+
profiles:{quick:{...autonomous.profiles.quick,root_id:'local_c'}}};
|
|
53
|
+
assert.throws(()=>deriveIsolatedAutonomousScope({manualCodingPolicy:original,autonomousPolicy:wide,
|
|
54
|
+
scopeRootId:'local_c',isolatedStateRoot:scratch}),/project-scoped root/);
|
|
55
|
+
assert.throws(()=>assertAutonomousBootstrapRoots(autonomous,original,'a'.repeat(64)),
|
|
56
|
+
/coding policy contains a broad filesystem root/);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
test('R135 rejects Autonomous profiles targeting roots other than the approved scope',async()=>{
|
|
60
|
+
await fixture(async({scratch,original,autonomous})=>{
|
|
61
|
+
const forged={...autonomous,profiles:{out:{...autonomous.profiles.quick,root_id:'downloads'}}};
|
|
62
|
+
assert.throws(()=>deriveIsolatedAutonomousScope({manualCodingPolicy:original,autonomousPolicy:forged,
|
|
63
|
+
scopeRootId:'core',isolatedStateRoot:scratch}),/profile.*outside/);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
test('R135 rejects missing command referenced by approved profile',async()=>{
|
|
67
|
+
await fixture(async({scratch,original,autonomous})=>{
|
|
68
|
+
const forged={...autonomous,profiles:{quick:{...autonomous.profiles.quick,command_id:'not_allowlisted'}}};
|
|
69
|
+
assert.throws(()=>deriveIsolatedAutonomousScope({manualCodingPolicy:original,autonomousPolicy:forged,
|
|
70
|
+
scopeRootId:'core',isolatedStateRoot:scratch}),/command.*missing/);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
test('R135 refuses missing, redirected, and unsafe project root',async()=>{
|
|
74
|
+
await fixture(async({scratch,original,autonomous,base})=>{
|
|
75
|
+
original.roots.core.path=path.join(base,'not-present');
|
|
76
|
+
assert.throws(()=>deriveIsolatedAutonomousScope({manualCodingPolicy:original,autonomousPolicy:autonomous,
|
|
77
|
+
scopeRootId:'core',isolatedStateRoot:scratch}),/real project directory/);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
test('R135 does not accept an isolated state ancestor of an existing manual lease',async()=>{
|
|
81
|
+
await fixture(async({scratch,original,autonomous,base})=>{
|
|
82
|
+
original.lease_file=path.join(base,'runtime','existing-coding-grant.json');
|
|
83
|
+
autonomous.lease_file=path.join(path.dirname(base),'other-autonomous-grant.json');
|
|
84
|
+
assert.throws(()=>deriveIsolatedAutonomousScope({manualCodingPolicy:original,autonomousPolicy:autonomous,
|
|
85
|
+
scopeRootId:'core',isolatedStateRoot:base}),/isolated state root overlaps manual Coding lease/);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
test('R135 does not inherit any existing grant file at the isolated destination',async()=>{
|
|
89
|
+
await fixture(async({scratch,original,autonomous})=>{
|
|
90
|
+
const runtime=path.join(scratch,'runtime');
|
|
91
|
+
await mkdir(runtime);
|
|
92
|
+
await writeFile(path.join(runtime,'coding-grant.json'),'{"legacy":true}\n');
|
|
93
|
+
assert.throws(()=>deriveIsolatedAutonomousScope({manualCodingPolicy:original,autonomousPolicy:autonomous,
|
|
94
|
+
scopeRootId:'core',isolatedStateRoot:scratch}),/isolated grant file already present/);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('R135 refuses using the manual lease root as the isolated runtime root',async()=>{
|
|
99
|
+
await fixture(async({original,autonomous,base})=>{
|
|
100
|
+
assert.throws(()=>deriveIsolatedAutonomousScope({manualCodingPolicy:original,autonomousPolicy:autonomous,
|
|
101
|
+
scopeRootId:'core',isolatedStateRoot:base}),/isolated state root overlaps manual Coding lease/);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {mkdtemp,mkdir,rm} from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {deriveIsolatedAutonomousScope} from '../src/autonomous-scope-r135.mjs';
|
|
7
|
+
const fixture=async fn=>{
|
|
8
|
+
const dir=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r136-command-'));
|
|
9
|
+
try{
|
|
10
|
+
const core=path.join(dir,'core'),state=path.join(dir,'isolated');
|
|
11
|
+
await mkdir(core);await mkdir(state);
|
|
12
|
+
const coding={schema:'deadbyte.coding-policy.v1',enabled:true,lease_file:path.join(dir,'manual','coding-grant.json'),
|
|
13
|
+
evidence_dir:path.join(dir,'manual','evidence'),roots:{core:{path:core,read:true,write:true,exec:true,deny_paths:[]},
|
|
14
|
+
local_c:{path:path.parse(dir).root,read:true,write:true,exec:true,deny_paths:[]}},
|
|
15
|
+
commands:{node_script:{kind:'script',executable:process.execPath,executable_sha256:'a'.repeat(64),
|
|
16
|
+
root_ids:['core','local_c'],prefix_args:[],extensions:['.mjs']}}};
|
|
17
|
+
const autonomous={schema:'deadbyte.autonomous-policy.v1',enabled:true,lease_file:path.join(dir,'manual','autonomous-grant.json'),
|
|
18
|
+
evidence_dir:path.join(dir,'manual','evidence-auto'),coding_policy_sha256:'b'.repeat(64),
|
|
19
|
+
root_ids:['core'],profiles:{quick:{root_id:'core',command_id:'node_script'}},
|
|
20
|
+
planner:{backend:'client_sampling'}};
|
|
21
|
+
await fn({coding,autonomous,state});
|
|
22
|
+
} finally {await rm(dir,{recursive:true,force:true})}
|
|
23
|
+
};
|
|
24
|
+
test('R136 projected command does not retain broad manual root_ids',async()=>{
|
|
25
|
+
await fixture(({coding,autonomous,state})=>{
|
|
26
|
+
const before=structuredClone(coding);
|
|
27
|
+
const result=deriveIsolatedAutonomousScope({manualCodingPolicy:coding,autonomousPolicy:autonomous,
|
|
28
|
+
scopeRootId:'core',isolatedStateRoot:state});
|
|
29
|
+
assert.deepEqual(result.codingPolicy.commands.node_script.root_ids,['core']);
|
|
30
|
+
assert.deepEqual(coding,before);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
test('R136 command with no access to approved scope fails closed',async()=>{
|
|
34
|
+
await fixture(({coding,autonomous,state})=>{
|
|
35
|
+
coding.commands.node_script.root_ids=['local_c'];
|
|
36
|
+
assert.throws(()=>deriveIsolatedAutonomousScope({manualCodingPolicy:coding,autonomousPolicy:autonomous,
|
|
37
|
+
scopeRootId:'core',isolatedStateRoot:state}),/command.*outside approved scope/);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {mkdtemp,mkdir,writeFile,readFile,rm} from 'node:fs/promises';
|
|
4
|
+
import {createHash} from 'node:crypto';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import {startScopedObserver} from '../src/scoped-observer-launcher-r136.mjs';
|
|
8
|
+
import {deriveIsolatedAutonomousScope} from '../src/autonomous-scope-r135.mjs';
|
|
9
|
+
const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
10
|
+
|
|
11
|
+
async function prepared(run){
|
|
12
|
+
const temp=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r136-observer-'));
|
|
13
|
+
let observer=null;
|
|
14
|
+
try{
|
|
15
|
+
const project=path.join(temp,'project'),isolated=path.join(temp,'isolated');
|
|
16
|
+
await mkdir(project);await mkdir(isolated);
|
|
17
|
+
const manual={schema:'deadbyte.coding-policy.v1',enabled:true,lease_file:path.join(temp,'manual','coding-grant.json'),
|
|
18
|
+
evidence_dir:path.join(temp,'manual','evidence'),
|
|
19
|
+
roots:{core:{path:project,read:true,write:true,exec:true,deny_paths:[]},
|
|
20
|
+
local_d:{path:path.parse(temp).root,read:true,write:true,exec:true,deny_paths:[]}},
|
|
21
|
+
commands:{node_script:{kind:'script',root_ids:['core','local_d'],executable:process.execPath,
|
|
22
|
+
executable_sha256:'a'.repeat(64),prefix_args:[],extensions:['.mjs']}}};
|
|
23
|
+
const autonomous={schema:'deadbyte.autonomous-policy.v1',enabled:true,
|
|
24
|
+
lease_file:path.join(temp,'manual','autonomous-grant.json'),evidence_dir:path.join(temp,'manual','aevidence'),
|
|
25
|
+
coding_policy_sha256:'f'.repeat(64),root_ids:['core'],
|
|
26
|
+
planner:{backend:'client_sampling'},profiles:{quick:{kind:'command',root_id:'core',command_id:'node_script'}}};
|
|
27
|
+
const projected=deriveIsolatedAutonomousScope({manualCodingPolicy:manual,autonomousPolicy:autonomous,scopeRootId:'core',isolatedStateRoot:isolated});
|
|
28
|
+
const codingFile=path.join(isolated,'coding-policy.json'),autonomousFile=path.join(isolated,'autonomous-policy.json');
|
|
29
|
+
await writeFile(codingFile,projected.codingBytes);
|
|
30
|
+
await writeFile(autonomousFile,projected.autonomousBytes);
|
|
31
|
+
const options={stateRoot:isolated,codingPolicyFile:codingFile,autonomousPolicyFile:autonomousFile,
|
|
32
|
+
codingSha256:projected.coding_sha256,autonomousSha256:projected.autonomous_sha256};
|
|
33
|
+
await run({temp,isolated,options,projected,manual,autonomous,setObserver:v=>observer=v});
|
|
34
|
+
} finally {
|
|
35
|
+
if(observer)await observer.close();
|
|
36
|
+
await rm(temp,{recursive:true,force:true});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
test('R136 observer starts in its own PID disarmed, with no inherited broad Coding policy or ambient secrets',async()=>{
|
|
40
|
+
await prepared(async({options,projected,setObserver})=>{
|
|
41
|
+
const obs=await startScopedObserver({...options, ambientEnv:{DEADBYTE_CODING_POLICY:'C:\\\\evil.json',
|
|
42
|
+
DEADBYTE_STATE_ROOT:'C:\\\\Users\\\\DEADBYTE\\\\.deadbyte-mcp',NODE_OPTIONS:'--inspect',
|
|
43
|
+
OPENAI_API_KEY:'DO_NOT_LEAK'}});
|
|
44
|
+
setObserver(obs);
|
|
45
|
+
const result=obs.ready;
|
|
46
|
+
assert.ok(Number.isInteger(result.pid)&&result.pid>0);
|
|
47
|
+
assert.notEqual(result.pid,process.pid);
|
|
48
|
+
assert.equal(result.status,'DISARMED');
|
|
49
|
+
assert.deepEqual(result.root_ids,['core']);
|
|
50
|
+
assert.deepEqual(result.command_ids,['node_script']);
|
|
51
|
+
assert.equal(result.coding_sha256,projected.coding_sha256);
|
|
52
|
+
assert.equal(result.autonomous_sha256,projected.autonomous_sha256);
|
|
53
|
+
assert.equal(result.ambient_deadbyte_keys,0);
|
|
54
|
+
assert.equal(result.node_options_present,false);
|
|
55
|
+
assert.equal(result.user_token_present,false);
|
|
56
|
+
assert.equal(result.grant_files_present,false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
test('R136 observer refuses execute, arm and privileged requests instead of invoking any host tool',async()=>{
|
|
60
|
+
await prepared(async({options,setObserver})=>{
|
|
61
|
+
const obs=await startScopedObserver(options);setObserver(obs);
|
|
62
|
+
for(const action of ['execute','arm','write','host_exec','unknown']){
|
|
63
|
+
const reply=await obs.request({op:action,root_id:'core',command_id:'node_script'});
|
|
64
|
+
assert.deepEqual(reply,{status:'DENIED_UNAUTHORIZED',op:action});
|
|
65
|
+
}
|
|
66
|
+
assert.deepEqual(await obs.request({op:'status'}),{status:'DISARMED',root_ids:['core']});
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
test('R136 client-controlled IPC envelope fields cannot override request identity',async()=>{
|
|
70
|
+
await prepared(async({options,setObserver})=>{
|
|
71
|
+
const obs=await startScopedObserver(options);setObserver(obs);
|
|
72
|
+
const result=await obs.request({kind:'reply',id:9999,op:'execute'});
|
|
73
|
+
assert.deepEqual(result,{status:'DENIED_UNAUTHORIZED',op:'execute'});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('R136 observer rejects policy byte tampering against SHA-pinned startup',async()=>{
|
|
78
|
+
await prepared(async({options})=>{
|
|
79
|
+
await writeFile(options.codingPolicyFile,Buffer.concat([await readFile(options.codingPolicyFile),Buffer.from(' ')]));
|
|
80
|
+
await assert.rejects(startScopedObserver(options),/policy digest mismatch|observer boot failed/);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
test('R136 observer refuses inherited grant bytes even when policy hashes match',async()=>{
|
|
84
|
+
await prepared(async({options,isolated})=>{
|
|
85
|
+
const dir=path.join(isolated,'runtime');await mkdir(dir,{recursive:true});
|
|
86
|
+
await writeFile(path.join(dir,'coding-grant.json'),Buffer.from('{"armed":true}\n'));
|
|
87
|
+
await assert.rejects(startScopedObserver(options),/grant present|observer boot failed/);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {mkdtemp,writeFile,readFile,readdir,rm} from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import {createHash} from 'node:crypto';
|
|
7
|
+
import {createCommanderExtendedRuntime} from '../src/commander-extended-runtime.mjs';
|
|
8
|
+
const sha=b=>createHash('sha256').update(b).digest('hex');
|
|
9
|
+
async function fixture(run){
|
|
10
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r137-config-'));
|
|
11
|
+
try{await run({root,file:path.join(root,'commander-config.json')});}
|
|
12
|
+
finally{await rm(root,{recursive:true,force:true});}
|
|
13
|
+
}
|
|
14
|
+
test('R137 recovers bounded display limits from complete JSON prefix, not privileges or telemetry',async()=>{
|
|
15
|
+
await fixture(async({root,file})=>{
|
|
16
|
+
const corrupt=Buffer.from('{"fileReadLineLimit":200,"fileWriteLineLimit":20,"allowedDirectories":["C:\\\\","D:\\\\"],"telemetryEnabled":true,"defaultShell":"cmd.exe","theme":"light","blockedCommands":[],"damaged": [\n','utf8');
|
|
17
|
+
await writeFile(file,corrupt);
|
|
18
|
+
const runtime=createCommanderExtendedRuntime({stateRoot:root});
|
|
19
|
+
const cfg=await runtime.getConfig();
|
|
20
|
+
assert.equal(cfg.config.fileReadLineLimit,200);
|
|
21
|
+
assert.equal(cfg.config.fileWriteLineLimit,20);
|
|
22
|
+
assert.equal(cfg.config.telemetryEnabled,false);
|
|
23
|
+
assert.deepEqual(cfg.config.allowedDirectories,[]);
|
|
24
|
+
assert.equal(cfg.config.theme,undefined);
|
|
25
|
+
assert.equal(cfg.config.defaultShell,process.platform==='win32'?'powershell.exe':(process.env.SHELL||'/bin/sh'));
|
|
26
|
+
assert.equal(cfg.recovery?.status,'recovered');
|
|
27
|
+
assert.deepEqual(cfg.recovery?.salvaged_keys,['fileReadLineLimit','fileWriteLineLimit']);
|
|
28
|
+
const backups=(await readdir(root)).filter(f=>f.startsWith('commander-config.json.corrupt.'));
|
|
29
|
+
assert.equal(backups.length,1);
|
|
30
|
+
assert.equal(sha(await readFile(path.join(root,backups[0]))),sha(corrupt));
|
|
31
|
+
const stored=JSON.parse(await readFile(file,'utf8'));
|
|
32
|
+
assert.equal(stored.fileReadLineLimit,200);
|
|
33
|
+
assert.equal(stored.telemetryEnabled,false);
|
|
34
|
+
const next=await runtime.getConfig();
|
|
35
|
+
assert.equal(next.config_sha256,cfg.config_sha256);
|
|
36
|
+
assert.equal((await readdir(root)).filter(f=>f.startsWith('commander-config.json.corrupt.')).length,1);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
test('R137 totally corrupt or oversized config recovers minimal defaults without inheriting blockedCommands/paths',async()=>{
|
|
40
|
+
await fixture(async({root,file})=>{
|
|
41
|
+
await writeFile(file,'garbled not JSON');
|
|
42
|
+
const config=await createCommanderExtendedRuntime({stateRoot:root}).getConfig();
|
|
43
|
+
assert.equal(config.recovery?.status,'recovered');
|
|
44
|
+
assert.deepEqual(config.recovery?.salvaged_keys,[]);
|
|
45
|
+
assert.deepEqual(config.config.allowedDirectories,[]);
|
|
46
|
+
assert.equal(config.config.telemetryEnabled,false);
|
|
47
|
+
assert.ok(!Object.hasOwn(config.config,'arbitraryAuthority'));
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
test('R137 ignores out-of-bounds limits in parseable prefix',async()=>{
|
|
51
|
+
await fixture(async({root,file})=>{
|
|
52
|
+
await writeFile(file,'{"fileReadLineLimit":999999,"fileWriteLineLimit":0,"allowedDirectories":["C:\\\\"],"broken":');
|
|
53
|
+
const config=await createCommanderExtendedRuntime({stateRoot:root}).getConfig();
|
|
54
|
+
assert.equal(config.config.fileReadLineLimit,1000);
|
|
55
|
+
assert.equal(config.config.fileWriteLineLimit,50);
|
|
56
|
+
assert.deepEqual(config.recovery?.salvaged_keys,[]);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
test('R137 parallel configuration reads share one recovery and preserve original backup',async()=>{
|
|
60
|
+
await fixture(async({root,file})=>{
|
|
61
|
+
const raw=Buffer.from('{"fileReadLineLimit":210,"allowedDirectories":["C:\\\\"],"unfinished": [\n');
|
|
62
|
+
await writeFile(file,raw);
|
|
63
|
+
const rt=createCommanderExtendedRuntime({stateRoot:root});
|
|
64
|
+
const [a,b,c]=await Promise.all([rt.getConfig(),rt.getConfig(),rt.getConfig()]);
|
|
65
|
+
assert.deepEqual([a.config_sha256,b.config_sha256,c.config_sha256],[a.config_sha256,a.config_sha256,a.config_sha256]);
|
|
66
|
+
const backups=(await readdir(root)).filter(n=>n.startsWith('commander-config.json.corrupt.'));
|
|
67
|
+
assert.equal(backups.length,1);
|
|
68
|
+
assert.equal(sha(await readFile(path.join(root,backups[0]))),sha(raw));
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
test('R137 quoted commas and nested JSON do not spoof a recoverable top-level setting',async()=>{
|
|
72
|
+
await fixture(async({root,file})=>{
|
|
73
|
+
await writeFile(file,'{"description":"x, \\\"fileReadLineLimit\\\": 8","nested":{"fileReadLineLimit":1},"fileReadLineLimit":225,"bad": [');
|
|
74
|
+
const r=await createCommanderExtendedRuntime({stateRoot:root}).getConfig();
|
|
75
|
+
assert.equal(r.config.fileReadLineLimit,225);
|
|
76
|
+
assert.deepEqual(r.recovery?.salvaged_keys,['fileReadLineLimit']);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
test('R137 valid non-object JSON is not trusted as configuration',async()=>{
|
|
80
|
+
await fixture(async({root,file})=>{
|
|
81
|
+
await writeFile(file,'["allowedDirectories","C:\\\\"]');
|
|
82
|
+
const r=await createCommanderExtendedRuntime({stateRoot:root}).getConfig();
|
|
83
|
+
assert.equal(r.recovery?.status,'recovered');
|
|
84
|
+
assert.deepEqual(r.config.allowedDirectories,[]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('R137 preserves the existing valid config without making recovery backup',async()=>{
|
|
89
|
+
await fixture(async({root,file})=>{
|
|
90
|
+
await writeFile(file,JSON.stringify({theme:'classic',telemetryEnabled:false}));
|
|
91
|
+
const result=await createCommanderExtendedRuntime({stateRoot:root}).getConfig();
|
|
92
|
+
assert.equal(result.config.theme,'classic');
|
|
93
|
+
assert.equal(result.recovery,undefined);
|
|
94
|
+
assert.deepEqual(await readdir(root),['commander-config.json']);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
test('R137 recovered config remains hash-preconditioned for subsequent edits',async()=>{
|
|
98
|
+
await fixture(async({root,file})=>{
|
|
99
|
+
await writeFile(file,'{"fileReadLineLimit":333,"bad":[');
|
|
100
|
+
const runtime=createCommanderExtendedRuntime({stateRoot:root});
|
|
101
|
+
const first=await runtime.getConfig();
|
|
102
|
+
await assert.rejects(runtime.setConfigValue({key:'theme',value:'classic',expected_sha256:'0'.repeat(64)}),/precondition/);
|
|
103
|
+
const done=await runtime.setConfigValue({key:'theme',value:'classic',expected_sha256:first.config_sha256});
|
|
104
|
+
assert.equal(done.status,'committed');
|
|
105
|
+
const fresh=createCommanderExtendedRuntime({stateRoot:root});
|
|
106
|
+
const now=await fresh.getConfig();
|
|
107
|
+
assert.equal(now.config.theme,'classic');
|
|
108
|
+
assert.equal(now.config.fileReadLineLimit,333);
|
|
109
|
+
assert.equal(now.recovery,undefined);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
import {pathToFileURL} from 'node:url';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import {createCommanderWireAdapter} from '../src/commander-wire-adapter-r47.mjs';
|
|
8
|
+
import {registerCommanderExtendedBridge} from '../src/commander-extended-bridge-r47.mjs';
|
|
9
|
+
|
|
10
|
+
const base='D:\\Projects\\deadbyte-mcp\\references';
|
|
11
|
+
const fixtures=[
|
|
12
|
+
{version:'0.2.52',relative:'desktop-commander v0.2.52/dist/tools/schemas.js',sha256:'f342345e34e48769dabaefe1b932f09d41e2e207b422f7794a15ee50ffa8f140',tools:27},
|
|
13
|
+
{version:'0.3.0',relative:'desktop-commander v0.3.0/dist/tools/schemas.js',sha256:'7d4c62e2099d757d8fba68011cfe25d3088426f56c0b77d5b8fa3edb4ec49c2c',tools:26},
|
|
14
|
+
{version:'0.3.1-alpha.2',relative:'desktop-commander-0.3.1-alpha.2/static/dist/tools/schemas.js',sha256:'6a290497b29f9dc5f43d66008ea54fcf0c302edea5612af519ef52ab3816d29e',tools:27}
|
|
15
|
+
];
|
|
16
|
+
const known={
|
|
17
|
+
get_config:{},set_config_value:{key:'theme',value:'classic'},read_file:{path:'x'},read_multiple_files:{paths:['x']},
|
|
18
|
+
write_file:{path:'x',content:'v'},write_pdf:{path:'x',content:'# x'},create_directory:{path:'x'},
|
|
19
|
+
list_directory:{path:'x'},move_file:{source:'a',destination:'b'},start_search:{path:'.',pattern:'x'},
|
|
20
|
+
get_more_search_results:{sessionId:'s'},stop_search:{sessionId:'s'},list_searches:{},get_file_info:{path:'x'},
|
|
21
|
+
edit_block:{file_path:'x',old_string:'a',new_string:'b'},start_process:{command:'echo x',timeout_ms:10},
|
|
22
|
+
read_process_output:{pid:1},interact_with_process:{pid:1,input:'x'},force_terminate:{pid:1},list_sessions:{},
|
|
23
|
+
list_processes:{},kill_process:{pid:1},get_usage_stats:{},get_recent_tool_calls:{},
|
|
24
|
+
give_feedback_to_desktop_commander:{},get_prompts:{action:'get_prompt',promptId:'x'}
|
|
25
|
+
};
|
|
26
|
+
const sha=s=>createHash('sha256').update(s).digest('hex');
|
|
27
|
+
const unwrap=o=>{while(o?._def?.schema)o=o._def.schema;return o};
|
|
28
|
+
function candidate(){
|
|
29
|
+
const v={...createCommanderWireAdapter({}).schemas};
|
|
30
|
+
registerCommanderExtendedBridge({registerTool(name,def){v[name]=def.inputSchema;}},{extendedRuntime:{}});
|
|
31
|
+
return v;
|
|
32
|
+
}
|
|
33
|
+
async function reference(f){
|
|
34
|
+
const bytes=await readFile(path.join(base,f.relative));
|
|
35
|
+
assert.equal(sha(bytes),f.sha256,'reference source SHA drift: '+f.version);
|
|
36
|
+
const zod=pathToFileURL(path.resolve('node_modules/zod/v3/index.js')).href;
|
|
37
|
+
const source=bytes.toString('utf8').replace('from "zod"','from '+JSON.stringify(zod));
|
|
38
|
+
return (await import('data:text/javascript;base64,'+Buffer.from(source).toString('base64'))).toolArgSchemas;
|
|
39
|
+
}
|
|
40
|
+
for(const fixture of fixtures){
|
|
41
|
+
test('R137 pinned Desktop Commander '+fixture.version+' public tool schemas remain compatible',async()=>{
|
|
42
|
+
const r=await reference(fixture),c=candidate();
|
|
43
|
+
assert.equal(Object.keys(r).length,fixture.tools);
|
|
44
|
+
if(fixture.version==='0.3.0')assert.equal(Object.hasOwn(r,'get_prompts'),false);
|
|
45
|
+
for(const [name,refSchema] of Object.entries(r)){
|
|
46
|
+
// track_ui_event is a deliberately hidden UI dispatch, not a public MCP tool.
|
|
47
|
+
if(name==='track_ui_event')continue;
|
|
48
|
+
assert.ok(c[name],'DEADBYTE missing '+name);
|
|
49
|
+
assert.deepEqual(Object.keys(unwrap(c[name]).shape).sort(),Object.keys(unwrap(refSchema).shape).sort(),name+' property names');
|
|
50
|
+
const expected=refSchema.safeParse({...known[name],__future_extra:'strip'});
|
|
51
|
+
const actual=c[name].safeParse({...known[name],__future_extra:'strip'});
|
|
52
|
+
assert.equal(actual.success,expected.success,name+' input acceptance');
|
|
53
|
+
if(actual.success)assert.deepEqual(actual.data,expected.data,name+' normalized defaults');
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {createCommanderExternalProcessRuntime} from '../src/commander-external-process-r48.mjs';
|
|
4
|
+
|
|
5
|
+
const snapshot='a'.repeat(64),stamp='2026-10-21T11:00:00.000Z';
|
|
6
|
+
const row=(pid)=>({pid,parent_pid:1,name:'test'+pid+'.exe',ownership:'external',
|
|
7
|
+
creation_time_utc:stamp,executable_path:null,command_line:null});
|
|
8
|
+
const page=(items,cursor=null,identity=snapshot)=>({status:'ok',snapshot_id:identity,observed_at_utc:stamp,
|
|
9
|
+
processes:items,next_cursor:cursor,receipt_id:'b'.repeat(24)});
|
|
10
|
+
function fixture(sequence){
|
|
11
|
+
let index=0;
|
|
12
|
+
const observationRuntime={async processList(){return sequence[Math.min(index++,sequence.length-1)];}};
|
|
13
|
+
return {runtime:createCommanderExternalProcessRuntime({observationRuntime}),calls:()=>index};
|
|
14
|
+
}
|
|
15
|
+
test('R138 rejects a repeated external process cursor instead of polling forever',async()=>{
|
|
16
|
+
const f=fixture([page([row(1)],'repeat'),page([row(2)],'repeat')]);
|
|
17
|
+
await assert.rejects(f.runtime.list(),/cursor.*repeat|pagination.*cycle/i);
|
|
18
|
+
assert.equal(f.calls(),2);
|
|
19
|
+
});
|
|
20
|
+
test('R138 rejects duplicate PID across pages instead of creating ambiguous kill target',async()=>{
|
|
21
|
+
const f=fixture([page([row(42)],'next'),page([row(42)],null)]);
|
|
22
|
+
await assert.rejects(f.runtime.list(),/duplicate.*PID/i);
|
|
23
|
+
assert.equal(f.calls(),2);
|
|
24
|
+
});
|
|
25
|
+
test('R138 rejects contradictory observation timestamps across a single snapshot',async()=>{
|
|
26
|
+
const f=fixture([page([row(1)],'next'),{...page([row(2)]),observed_at_utc:'2026-10-21T12:00:00.000Z'}]);
|
|
27
|
+
await assert.rejects(f.runtime.list(),/observation time.*changed|timestamp.*changed/i);
|
|
28
|
+
});
|
|
29
|
+
test('R138 rejects invalid missing snapshot identity and unbounded cursor',async()=>{
|
|
30
|
+
const f=fixture([{...page([row(1)]),snapshot_id:null}]);
|
|
31
|
+
await assert.rejects(f.runtime.list(),/snapshot.*identity/i);
|
|
32
|
+
const oversized=fixture([page([row(1)],'x'.repeat(12000))]);
|
|
33
|
+
await assert.rejects(oversized.runtime.list(),/cursor.*invalid/i);
|
|
34
|
+
});
|
|
35
|
+
test('R138 normal two-page process observation preserves ordered identities and receipts',async()=>{
|
|
36
|
+
const f=fixture([page([row(1)],'next'),page([row(2)])]);
|
|
37
|
+
const r=await f.runtime.list();
|
|
38
|
+
assert.deepEqual(r.processes.map(x=>x.pid),[1,2]);
|
|
39
|
+
assert.equal(r.snapshot_id,snapshot);
|
|
40
|
+
assert.deepEqual(r.receipt_ids,['b'.repeat(24)]);
|
|
41
|
+
assert.equal(f.calls(),2);
|
|
42
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {createCommanderWireAdapter} from '../src/commander-wire-adapter-r47.mjs';
|
|
4
|
+
test('R138 Commander sessions show PID, profile, root, and exit outcome in one line',async()=>{
|
|
5
|
+
const sessions=[
|
|
6
|
+
{session_id:'a'.repeat(64),child_pid:21138,state:'running',exit_code:null,profile_id:'powershell',root_id:'core',stream_head:17},
|
|
7
|
+
{session_id:'b'.repeat(64),child_pid:22138,state:'exited',exit_code:7,profile_id:'node',root_id:'build',stream_head:20}
|
|
8
|
+
];
|
|
9
|
+
const a=createCommanderWireAdapter({processRuntime:{async list(){return {sessions}}}});
|
|
10
|
+
const r=await a.handlers.list_sessions({});
|
|
11
|
+
assert.equal(r.isError,undefined);
|
|
12
|
+
assert.deepEqual(r.structuredContent.sessions,sessions);
|
|
13
|
+
assert.match(r.content[0].text,/PID=21138/);
|
|
14
|
+
assert.match(r.content[0].text,/PID=22138/);
|
|
15
|
+
assert.match(r.content[0].text,/exit=7/);
|
|
16
|
+
assert.match(r.content[0].text,/profile=node/);
|
|
17
|
+
assert.match(r.content[0].text,/root=build/);
|
|
18
|
+
});
|
|
19
|
+
test('R138 no sessions returns a readable empty summary',async()=>{
|
|
20
|
+
const a=createCommanderWireAdapter({processRuntime:{async list(){return {sessions:[]}}}});
|
|
21
|
+
const r=await a.handlers.list_sessions({});
|
|
22
|
+
assert.match(r.content[0].text,/No managed sessions/);
|
|
23
|
+
assert.deepEqual(r.structuredContent.sessions,[]);
|
|
24
|
+
});
|
|
@@ -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
|
+
});
|