deadbyte-mcp 0.11.2 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MANIFEST.SHA256 +101 -39
- package/README.txt +10 -9
- package/acceptance/r40/desktop-live-drift.mjs +106 -0
- package/acceptance/r40/desktop-live-ground.mjs +110 -0
- package/acceptance/r40/desktop-live-input.mjs +128 -0
- package/acceptance/r40/desktop-live-observe.mjs +100 -0
- package/acceptance/r40/desktop-live-uia.mjs +121 -0
- package/bin/deadbyte-desktop-input-r40.exe +0 -0
- package/bin/deadbyte-desktop-input-r40.obj +0 -0
- package/bin/deadbyte-desktop-observer-r40.exe +0 -0
- package/bin/deadbyte-desktop-observer-r40.obj +0 -0
- package/bin/deadbyte-desktop-uia-r40.exe +0 -0
- package/bin/deadbyte-desktop-uia-r40.obj +0 -0
- package/controller/README.TXT +7 -5
- package/controller/deadbyte-controller.config.psd1.example +3 -0
- package/controller/deadbyte-controller.ps1 +149 -4
- package/controller/deadbyte-desktop-policy.json +41 -0
- package/docs/ARCHITECTURE.md +6 -1
- package/docs/superpowers/specs/2026-09-19-r40-desktop-computer-use-design.md +214 -0
- package/native/deadbyte-desktop-input.cpp +455 -0
- package/native/deadbyte-desktop-observer.cpp +253 -0
- package/native/deadbyte-desktop-uia.cpp +307 -0
- package/package.json +10 -8
- package/scripts/build-desktop-r40.ps1 +106 -0
- package/scripts/deferred-slot-operation-r39.mjs +124 -0
- package/scripts/deferred-slot-operation-r40.mjs +126 -0
- package/scripts/final-closure-r39.mjs +113 -0
- package/scripts/final-closure-r40.mjs +114 -0
- package/scripts/final-closure-verify-r39.mjs +208 -0
- package/scripts/final-closure-verify-r40.mjs +212 -0
- package/scripts/gate-windows-r39.ps1 +42 -0
- package/scripts/gate-windows-r40.ps1 +43 -0
- package/scripts/init-desktop-policy-r40.ps1 +79 -0
- package/scripts/r40-compact-ledger-probe.mjs +4 -0
- package/scripts/release-parity-r39.mjs +200 -0
- package/scripts/release-parity-r40.mjs +212 -0
- package/scripts/release-parity-tests-r39.ps1 +9 -0
- package/scripts/release-parity-tests-r40.ps1 +9 -0
- package/scripts/windows-gate-evidence-r39.mjs +52 -0
- package/scripts/windows-gate-evidence-r40.mjs +52 -0
- package/src/autonomous-context-engine.mjs +2 -1
- package/src/autonomous-planner-contracts.mjs +19 -1
- package/src/autonomous-planner.mjs +22 -0
- package/src/autonomous-runtime.mjs +144 -24
- package/src/capability-ledger.mjs +6 -1
- package/src/compact-gateway-mcp-tools.mjs +14 -2
- package/src/deadbyte-cli.mjs +79 -47
- package/src/desktop-contract.mjs +263 -0
- package/src/desktop-input-r40.mjs +148 -0
- package/src/desktop-mcp-tools-r40.mjs +126 -0
- package/src/desktop-memory-r40.mjs +272 -0
- package/src/desktop-observer-r40.mjs +231 -0
- package/src/desktop-policy-r40.mjs +110 -0
- package/src/desktop-runtime-r40.mjs +318 -0
- package/src/desktop-uia-r40.mjs +123 -0
- package/src/mcp-server.mjs +74 -19
- package/src/r40-compact-disarmed-probe.mjs +71 -0
- package/src/r40-full-surface-probe.mjs +34 -0
- package/src/release-generation.mjs +38 -5
- package/src/remote-console.mjs +41 -0
- package/src/remote-live-log.mjs +215 -45
- package/src/remote-mcp-desktop-r40-smoke-client.mjs +70 -0
- package/src/remote-mcp-r40-parity-client.mjs +200 -0
- package/src/remote-mcp-smoke-client.mjs +20 -6
- package/src/remote-result-journal.mjs +10 -2
- package/src/runtime-update.mjs +55 -17
- package/src/tool-audit.mjs +29 -2
- package/src/version.mjs +1 -1
- package/test/autonomous-capability-r36.test.mjs +43 -1
- package/test/autonomous-desktop-r40.test.mjs +188 -0
- package/test/capability-ledger-r36.test.mjs +1 -0
- package/test/cli-entrypoint.test.mjs +1 -1
- package/test/compact-catalog-r36.test.mjs +2 -2
- package/test/compact-gateway-r36.test.mjs +56 -4
- package/test/compact-max-catalog-r40.test.mjs +95 -0
- package/test/controller-desktop-r40.test.mjs +85 -0
- package/test/core.test.mjs +1 -1
- package/test/desktop-adversarial-r40.test.mjs +62 -0
- package/test/desktop-contract-r40.test.mjs +200 -0
- package/test/desktop-input-r40.test.mjs +63 -0
- package/test/desktop-mcp-tools-r40.test.mjs +61 -0
- package/test/desktop-memory-r40.test.mjs +141 -0
- package/test/desktop-observer-r40.test.mjs +119 -0
- package/test/desktop-policy-r40.test.mjs +111 -0
- package/test/desktop-runtime-r40.test.mjs +278 -0
- package/test/desktop-uia-r40.test.mjs +42 -0
- package/test/full-max-catalog-r40.test.mjs +91 -0
- package/test/helpers/autonomous-r34-fixture.mjs +3 -3
- package/test/production-docs.test.mjs +3 -3
- package/test/r33-closeout-regression.test.mjs +8 -8
- package/test/r33-finalization.test.mjs +14 -14
- package/test/r37-remote-tool-call-timeline.test.mjs +7 -6
- package/test/r39-finalization.test.mjs +74 -0
- package/test/r40-finalization.test.mjs +118 -0
- package/test/release-generation.test.mjs +41 -9
- package/test/release-version.test.mjs +19 -58
- package/test/remote-console-r40.test.mjs +38 -0
- package/test/remote-live-log-r37.test.mjs +89 -32
- package/test/remote-r24-catalog.test.mjs +12 -3
- package/test/remote-result-journal.test.mjs +15 -0
- package/test/remote-session-cli.test.mjs +112 -55
- package/test/runtime-self-update.test.mjs +44 -0
|
@@ -66,17 +66,26 @@ test('R32 remote gate accepts observation only with machine plus coding configur
|
|
|
66
66
|
assert.match(source, /observation_runtime: observationRuntime/);
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
test('
|
|
69
|
+
test('R40 remote smoke preserves exact R36 compact compatibility and accepts only exact attested R40 compact catalog', async () => {
|
|
70
70
|
const source = await readFile(sourcePath, 'utf8');
|
|
71
71
|
assert.match(source, /const r36CompactTools = \[/);
|
|
72
72
|
assert.match(source, /r36CompactTools\.length !== 41/);
|
|
73
|
-
assert.match(source, /const
|
|
73
|
+
assert.match(source, /const r40CompactTools = \[\.\.\.r36CompactTools, 'desktop_control'\]\.sort\(\)/);
|
|
74
|
+
assert.match(source, /r40CompactTools\.length !== 42/);
|
|
75
|
+
assert.match(source, /const exactR36Compact = JSON\.stringify\(names\) === JSON\.stringify\(r36CompactTools\)/);
|
|
76
|
+
assert.match(source, /const exactR40Compact = JSON\.stringify\(names\) === JSON\.stringify\(r40CompactTools\)/);
|
|
77
|
+
assert.match(source, /const compactProfile = exactR36Compact \|\| exactR40Compact/);
|
|
78
|
+
assert.match(source, /deadbyte\.capability-manifest\.v1/);
|
|
79
|
+
assert.match(source, /deadbyte\.capability-manifest\.v2/);
|
|
80
|
+
assert.match(source, /manifest\.tool_count !== names\.length/);
|
|
81
|
+
assert.match(source, /JSON\.stringify\(manifest\.tool_catalog\) !== JSON\.stringify\(names\)/);
|
|
82
|
+
assert.match(source, /manifest\.catalog_sha256 !== expectedCatalogSha/);
|
|
74
83
|
assert.match(source, /capability_manifest/);
|
|
75
84
|
assert.match(source, /machine_observe/);
|
|
76
85
|
assert.match(source, /op: 'machine_status'/);
|
|
77
86
|
assert.match(source, /op: 'machine_ping'/);
|
|
78
87
|
assert.match(source, /if \(compactProfile\) \{[\s\S]*?\} else \{[\s\S]*?workspace_open/);
|
|
79
|
-
assert.doesNotMatch(source, /
|
|
88
|
+
assert.doesNotMatch(source, /r40CompactTools\.includes\(name\).*extras/s);
|
|
80
89
|
});
|
|
81
90
|
|
|
82
91
|
test('R34 disarmed public gate requires the exact canonical 73-tool catalog', async () => {
|
|
@@ -56,3 +56,18 @@ test('cursor parser rejects foreign sessions and malformed cursors',async()=>{
|
|
|
56
56
|
await assert.rejects(journal.replayAfter('r1:'+'b'.repeat(24)+':0',{limit:8}),/cursor session mismatch/);
|
|
57
57
|
}finally{await rm(root,{recursive:true,force:true});}
|
|
58
58
|
});
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
test('remote result journal preserves explicit unknown side-effect state across restart and replay',async()=>{
|
|
62
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r40-remote-unknown-'));
|
|
63
|
+
const file=path.join(root,'journal.json');
|
|
64
|
+
try{
|
|
65
|
+
let journal=await openRemoteResultJournal({filePath:file,sessionId:session,maxEntries:8});
|
|
66
|
+
await journal.recordTerminal({...item('req-unknown',H('4'),'error'),side_effect_state:'unknown',reason:'runtime_restart'});
|
|
67
|
+
journal=await openRemoteResultJournal({filePath:file,sessionId:session,maxEntries:8});
|
|
68
|
+
const replay=await journal.replayAfter('r1:'+session+':0',{limit:8});
|
|
69
|
+
assert.equal(replay.events[0].side_effect_state,'unknown');
|
|
70
|
+
assert.equal(replay.events[0].reason,'runtime_restart');
|
|
71
|
+
await assert.rejects(journal.recordTerminal({...item('req-invalid',H('5'),'error'),side_effect_state:'committed'}),/side_effect_state invalid/);
|
|
72
|
+
}finally{await rm(root,{recursive:true,force:true});}
|
|
73
|
+
});
|
|
@@ -16,11 +16,15 @@ async function fixture({failCommand='',statusOnline=false}={}){
|
|
|
16
16
|
const state=path.join(dir,'state');
|
|
17
17
|
const bin=path.join(state,'bin');
|
|
18
18
|
const trace=path.join(dir,'trace.txt');
|
|
19
|
+
const startedMarker=path.join(dir,'started.txt');
|
|
20
|
+
const spawnedStdout=path.join(dir,'spawned.stdout.log');
|
|
21
|
+
const spawnedStderr=path.join(dir,'spawned.stderr.log');
|
|
22
|
+
const spawnedPid=path.join(dir,'spawned.pid');
|
|
19
23
|
await mkdir(bin,{recursive:true});
|
|
20
24
|
const launcher=path.join(bin,'DEADBYTE-MCP.ps1');
|
|
21
|
-
const ps1=`param([string]$Command)\n$ErrorActionPreference='Stop'\nfor($i=0;$i -lt 20;$i++){ try { [IO.File]::AppendAllText($env:DEADBYTE_REMOTE_TEST_TRACE,(\"$Command\" + [Environment]::NewLine),[Text.Encoding]::UTF8); break } catch { if($i -eq 19){ throw }; Start-Sleep -Milliseconds 10 } }\nif ($env:DEADBYTE_REMOTE_TEST_FAIL -and $Command -eq $env:DEADBYTE_REMOTE_TEST_FAIL) { Write-Error \"forced $Command failure\"; exit 9 }\nswitch ($Command) {\n 'start' { Write-Host 'DEADBYTE MCP = ONLINE' }\n 'status' { if ($env:DEADBYTE_REMOTE_TEST_STATUS_ONLINE -eq '1') { Write-Host 'DEADBYTE MCP = ONLINE'; Write-Host 'Bridge PID : 1001 [verified]'; Write-Host 'Cloud PID : 1002 [verified]'; Write-Host 'Local ready : True'; Write-Host 'Loop supervisor: RUNNING [verified]' } else { Write-Host 'DEADBYTE MCP = OFFLINE' } }\n 'processstatus' { Write-Host 'Process plane : ARMED [standing-grant]' }\n 'codingstatus' { Write-Host 'Coding plane : ARMED [standing-grant]' }\n 'autonomousstatus' { Write-Host 'Autonomous : ARMED [standing-grant]' }\n 'hoststatus' { Write-Host 'Host plane : DISARMED [disarmed]' }\n 'park' { Write-Host 'DEADBYTE MCP = PARKED' }\n default { Write-Host \"$Command OK\" }\n}\nexit 0\n`;
|
|
25
|
+
const ps1=`param([string]$Command)\n$ErrorActionPreference='Stop'\nfor($i=0;$i -lt 20;$i++){ try { [IO.File]::AppendAllText($env:DEADBYTE_REMOTE_TEST_TRACE,(\"$Command\" + [Environment]::NewLine),[Text.Encoding]::UTF8); break } catch { if($i -eq 19){ throw }; Start-Sleep -Milliseconds 10 } }\nif ($env:DEADBYTE_REMOTE_TEST_FAIL -and $Command -eq $env:DEADBYTE_REMOTE_TEST_FAIL) { Write-Error \"forced $Command failure\"; exit 9 }\nswitch ($Command) {\n 'start' { if ($env:DEADBYTE_REMOTE_TEST_SPAWN_CHILD -eq '1') { $node=(Get-Command node.exe).Source; $spawned=Start-Process -FilePath $node -ArgumentList @('-e','setTimeout(()=>process.exit(0),10000)') -PassThru -WindowStyle Hidden -RedirectStandardOutput $env:DEADBYTE_REMOTE_TEST_SPAWN_STDOUT -RedirectStandardError $env:DEADBYTE_REMOTE_TEST_SPAWN_STDERR; if ($env:DEADBYTE_REMOTE_TEST_SPAWN_PID) { [IO.File]::WriteAllText($env:DEADBYTE_REMOTE_TEST_SPAWN_PID,[string]$spawned.Id,[Text.Encoding]::UTF8) } }; if ($env:DEADBYTE_REMOTE_TEST_STARTED) { [IO.File]::WriteAllText($env:DEADBYTE_REMOTE_TEST_STARTED,'1',[Text.Encoding]::UTF8) }; Write-Host 'DEADBYTE MCP = ONLINE' }\n 'status' { if ($env:DEADBYTE_REMOTE_TEST_STATUS_ONLINE -eq '1' -or ($env:DEADBYTE_REMOTE_TEST_STARTED -and (Test-Path -LiteralPath $env:DEADBYTE_REMOTE_TEST_STARTED))) { Write-Host 'DEADBYTE MCP = ONLINE'; Write-Host 'Bridge PID : 1001 [verified]'; Write-Host 'Cloud PID : 1002 [verified]'; Write-Host 'Local ready : True'; Write-Host 'Loop supervisor: RUNNING [verified]' } else { Write-Host 'DEADBYTE MCP = OFFLINE' } }\n 'processstatus' { Write-Host 'Process plane : ARMED [standing-grant]' }\n 'codingstatus' { Write-Host 'Coding plane : ARMED [standing-grant]' }\n 'autonomousstatus' { Write-Host 'Autonomous : ARMED [standing-grant]' }\n 'hoststatus' { Write-Host 'Host plane : DISARMED [disarmed]' }\n 'park' { if ($env:DEADBYTE_REMOTE_TEST_STARTED) { Remove-Item -LiteralPath $env:DEADBYTE_REMOTE_TEST_STARTED -Force -ErrorAction SilentlyContinue }; Write-Host 'DEADBYTE MCP = PARKED' }\n default { Write-Host \"$Command OK\" }\n}\nexit 0\n`;
|
|
22
26
|
await writeFile(launcher,ps1,'utf8');
|
|
23
|
-
return {dir,state,trace,env:{...process.env,DEADBYTE_STATE_ROOT:state,DEADBYTE_REMOTE_TEST_TRACE:trace,DEADBYTE_REMOTE_TEST_FAIL:failCommand,DEADBYTE_REMOTE_TEST_STATUS_ONLINE:statusOnline?'1':'0'}};
|
|
27
|
+
return {dir,state,trace,env:{...process.env,DEADBYTE_STATE_ROOT:state,DEADBYTE_REMOTE_TEST_TRACE:trace,DEADBYTE_REMOTE_TEST_FAIL:failCommand,DEADBYTE_REMOTE_TEST_STATUS_ONLINE:statusOnline?'1':'0',DEADBYTE_REMOTE_TEST_STARTED:startedMarker,DEADBYTE_REMOTE_TEST_SPAWN_STDOUT:spawnedStdout,DEADBYTE_REMOTE_TEST_SPAWN_STDERR:spawnedStderr,DEADBYTE_REMOTE_TEST_SPAWN_PID:spawnedPid},spawnedPid};
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
async function traceCommands(file,{read=readFile,retryDelayMs=10,maxAttempts=20}={}){
|
|
@@ -80,8 +84,9 @@ winTest('bare remote reuses a fully verified ONLINE runtime without invoking blo
|
|
|
80
84
|
assert.equal(child.exitCode,null,'remote session must remain foreground after reusing ONLINE runtime');
|
|
81
85
|
assert.match(output.stdout,/Runtime already ONLINE; reusing verified bridge\/cloud\/supervisor/);
|
|
82
86
|
const authDeadline=Date.now()+3000;
|
|
83
|
-
while(Date.now()<authDeadline
|
|
84
|
-
assert.match(output.stdout
|
|
87
|
+
while(Date.now()<authDeadline&&!/\[AUTH\] ARMED: process, coding, autonomous \| host DISARMED/.test(output.stdout)) await sleep(50);
|
|
88
|
+
assert.match(output.stdout,/\[DISPLAY\] ASCII fallback: stdout_not_tty/);
|
|
89
|
+
assert.match(output.stdout,/\[AUTH\] ARMED: process, coding, autonomous \| host DISARMED/);
|
|
85
90
|
child.kill('SIGINT');
|
|
86
91
|
await output.closed;
|
|
87
92
|
}finally{
|
|
@@ -90,29 +95,60 @@ winTest('bare remote reuses a fully verified ONLINE runtime without invoking blo
|
|
|
90
95
|
}
|
|
91
96
|
});
|
|
92
97
|
|
|
98
|
+
winTest('bare remote PARKED startup does not wait for a long-lived child holding inherited pipe handles',async()=>{
|
|
99
|
+
const fx=await fixture();
|
|
100
|
+
fx.env.DEADBYTE_REMOTE_TEST_SPAWN_CHILD='1';
|
|
101
|
+
let child=null;
|
|
102
|
+
try{
|
|
103
|
+
const startedAt=Date.now();
|
|
104
|
+
child=spawn(process.execPath,[cliPath,'remote'],{env:fx.env,stdio:['pipe','pipe','pipe'],windowsHide:true});
|
|
105
|
+
const output=await collect(child);
|
|
106
|
+
const ready=await waitFor(fx.trace,commands=>commands.includes('hoststatus'),{timeoutMs:8000});
|
|
107
|
+
const elapsed=Date.now()-startedAt;
|
|
108
|
+
assert.deepEqual(ready.slice(0,10),[
|
|
109
|
+
'status','start','status','armprocess','armcoding','armautonomous',
|
|
110
|
+
'processstatus','codingstatus','autonomousstatus','hoststatus'
|
|
111
|
+
]);
|
|
112
|
+
assert.ok(elapsed<7000,`PARKED startup waited on inherited child pipe handles: ${elapsed}ms`);
|
|
113
|
+
assert.equal(child.exitCode,null,'remote session must remain foreground after PARKED startup');
|
|
114
|
+
assert.match(output.stdout,/\[BOOT\] Starting bridge\/runtime\.\.\./);
|
|
115
|
+
assert.match(output.stdout,/\[OK\] ONLINE/);
|
|
116
|
+
child.stdin.end();
|
|
117
|
+
const ended=await Promise.race([output.closed,sleep(6000).then(()=>({timeout:true}))]);
|
|
118
|
+
assert.equal(ended.timeout,undefined,'remote session did not exit after PARKED startup proof');
|
|
119
|
+
assert.equal(ended.code,0,output.stderr);
|
|
120
|
+
}finally{
|
|
121
|
+
if(child?.exitCode===null) child.kill('SIGKILL');
|
|
122
|
+
try{const pid=Number((await readFile(fx.spawnedPid,'utf8')).trim());if(Number.isInteger(pid)&&pid>0) process.kill(pid,'SIGKILL');}catch{}
|
|
123
|
+
await sleep(100);
|
|
124
|
+
await rm(fx.dir,{recursive:true,force:true});
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
93
128
|
winTest('bare remote uses one unified live timeline, keeps authority semantics, then disarms and parks when the foreground session ends',async()=>{
|
|
94
129
|
const fx=await fixture();
|
|
95
130
|
let child=null;
|
|
96
131
|
try{
|
|
97
132
|
child=spawn(process.execPath,[cliPath,'remote'],{env:fx.env,stdio:['pipe','pipe','pipe'],windowsHide:true});
|
|
98
133
|
const output=await collect(child);
|
|
99
|
-
const ready=await waitFor(fx.trace,commands=>commands.slice(0,
|
|
100
|
-
assert.deepEqual(ready.slice(0,
|
|
101
|
-
'status','start','armprocess','armcoding','armautonomous',
|
|
134
|
+
const ready=await waitFor(fx.trace,commands=>commands.slice(0,10).join(',')==='status,start,status,armprocess,armcoding,armautonomous,processstatus,codingstatus,autonomousstatus,hoststatus');
|
|
135
|
+
assert.deepEqual(ready.slice(0,10),[
|
|
136
|
+
'status','start','status','armprocess','armcoding','armautonomous',
|
|
102
137
|
'processstatus','codingstatus','autonomousstatus','hoststatus'
|
|
103
138
|
]);
|
|
104
139
|
assert.equal(child.exitCode,null,'remote session must remain foreground after becoming ready');
|
|
105
140
|
const tailDeadline=Date.now()+3000;
|
|
106
|
-
while(Date.now()<tailDeadline
|
|
107
|
-
assert.match(output.stdout
|
|
108
|
-
assert.match(output.stdout
|
|
109
|
-
assert.match(output.stdout
|
|
141
|
+
while(Date.now()<tailDeadline&&!/\[AUTH\] ARMED: process, coding, autonomous \| host DISARMED/.test(output.stdout)) await sleep(50);
|
|
142
|
+
assert.match(output.stdout,/\[DISPLAY\] ASCII fallback: stdout_not_tty/);
|
|
143
|
+
assert.match(output.stdout,/\[DEVICE\] Starting DEADBYTE MCP/);
|
|
144
|
+
assert.match(output.stdout,/\[BRIDGE\] Local bridge connected/);
|
|
145
|
+
assert.match(output.stdout,/\[CLOUD\] Remote MCP connected/);
|
|
110
146
|
assert.match(output.stdout,/\[SESSION\] New foreground session/);
|
|
111
147
|
assert.match(output.stdout,/\[BOOT\] Starting local \+ remote runtime/);
|
|
112
148
|
assert.match(output.stdout,/\[OK\] ONLINE/);
|
|
113
|
-
assert.match(output.stdout
|
|
114
|
-
assert.match(output.stdout
|
|
115
|
-
assert.match(output.stdout,/\[LOG\]
|
|
149
|
+
assert.match(output.stdout,/\[READY\] DEADBYTE MCP online/);
|
|
150
|
+
assert.match(output.stdout,/\[AUTH\] ARMED: process, coding, autonomous \| host DISARMED/);
|
|
151
|
+
assert.match(output.stdout,/\[LOG\] Human live timeline\. Transport IN\/OUT hidden; full replay\/audit detail remains on disk\./);
|
|
116
152
|
assert.doesNotMatch(output.stdout,/Process plane\s*:/);
|
|
117
153
|
assert.doesNotMatch(output.stdout,/Coding plane\s*:/);
|
|
118
154
|
const auditDir=path.join(fx.state,'audit');
|
|
@@ -135,12 +171,12 @@ winTest('bare remote uses one unified live timeline, keeps authority semantics,
|
|
|
135
171
|
'[TOOL OUT] 2026-09-16T00:00:00.002Z #000002 coding_file_read {"is_error":true}'
|
|
136
172
|
].join('\n')+'\n','utf8');
|
|
137
173
|
const logDeadline=Date.now()+3000;
|
|
138
|
-
while(Date.now()<logDeadline
|
|
139
|
-
assert.
|
|
140
|
-
assert.match(output.stdout
|
|
141
|
-
assert.match(output.stdout,/^
|
|
142
|
-
assert.
|
|
143
|
-
assert.
|
|
174
|
+
while(Date.now()<logDeadline&&!/^R .*#4 \[fedcba98\] coding_file_read projects:src\/deadbyte-cli\.mjs 1\.73s ERR$/m.test(output.stdout)) await sleep(50);
|
|
175
|
+
assert.doesNotMatch(output.stdout,/\| Arguments:/,'default human view must not print request JSON');
|
|
176
|
+
assert.match(output.stdout,/^S .*#2 \[abcdef12\] coding_tree projects:src 42ms OK$/m);
|
|
177
|
+
assert.match(output.stdout,/^R .*#4 \[fedcba98\] coding_file_read projects:src\/deadbyte-cli\.mjs 1\.73s ERR$/m);
|
|
178
|
+
assert.equal((output.stdout.match(/\[abcdef12\].*coding_tree.*42ms.*OK/g)||[]).length,1,'duplicate terminal result must print once');
|
|
179
|
+
assert.doesNotMatch(output.stdout,/\[cursor r1:/,'full replay cursor belongs in the journal, not every human line');
|
|
144
180
|
const resultJournal=JSON.parse(await readFile(path.join(fx.state,'runtime','remote-results',`${marker.result_session_id}.json`),'utf8'));
|
|
145
181
|
assert.deepEqual(resultJournal.events.map(event=>event.seq),[1,2,3,4]);
|
|
146
182
|
assert.deepEqual(resultJournal.events.map(event=>event.event_type),['tool.call','tool.result','tool.call','tool.result']);
|
|
@@ -150,9 +186,7 @@ winTest('bare remote uses one unified live timeline, keeps authority semantics,
|
|
|
150
186
|
assert.match(output.stdout,/abcdef12/);
|
|
151
187
|
assert.match(output.stdout,/fedcba98/);
|
|
152
188
|
assert.doesNotMatch(output.stdout,/DO_NOT_PRINT_PAYLOAD|DO_NOT_PRINT_TOKEN/);
|
|
153
|
-
assert.
|
|
154
|
-
assert.match(output.stdout,/\[BRIDGE\.STDOUT\] OUT coding_tree/);
|
|
155
|
-
assert.match(output.stdout,/\[BRIDGE\.STDOUT\] ERR coding_file_read/);
|
|
189
|
+
assert.doesNotMatch(output.stdout,/\[BRIDGE\.(?:STDOUT|DEBUG)\]/,'transport diagnostics are hidden in default human mode');
|
|
156
190
|
assert.doesNotMatch(output.stdout,/\[TOOL IN \]|\[TOOL OUT\]/);
|
|
157
191
|
assert.doesNotMatch(output.stdout,/payload bytes=/);
|
|
158
192
|
child.stdin.end();
|
|
@@ -161,11 +195,10 @@ winTest('bare remote uses one unified live timeline, keeps authority semantics,
|
|
|
161
195
|
const commands=await traceCommands(fx.trace);
|
|
162
196
|
assert.deepEqual(commands.slice(-5),['disarmautonomous','disarmcoding','disarmprocess','disarmhost','park']);
|
|
163
197
|
assert.match(output.stdout,/^DEADBYTE MCP$/m);
|
|
164
|
-
assert.match(output.stdout,/
|
|
165
|
-
assert.match(output.stdout
|
|
198
|
+
assert.match(output.stdout,/Human live timeline/i);
|
|
199
|
+
assert.match(output.stdout,/\[AUTH\] Revoking session authority/);
|
|
166
200
|
assert.doesNotMatch(output.stdout,/disarmautonomous OK|disarmcoding OK|disarmprocess OK|disarmhost OK|DEADBYTE MCP = PARKED/);
|
|
167
|
-
|
|
168
|
-
assert.doesNotMatch(withoutApprovedEmoji,/\p{Extended_Pictographic}/u,'human live stream may use only the approved semantic emoji set');
|
|
201
|
+
assert.doesNotMatch(output.stdout,/\p{Extended_Pictographic}/u,'piped Windows fallback must remain ASCII-safe');
|
|
169
202
|
}finally{
|
|
170
203
|
if(child?.exitCode===null) child.kill();
|
|
171
204
|
await rm(fx.dir,{recursive:true,force:true});
|
|
@@ -180,7 +213,9 @@ winTest('remote --resume replays sanitized tool-call and terminal result events
|
|
|
180
213
|
const firstOutput=await collect(first);
|
|
181
214
|
await waitFor(fx.trace,commands=>commands.includes('hoststatus'));
|
|
182
215
|
const liveDeadline=Date.now()+3000;
|
|
183
|
-
while(Date.now()<liveDeadline&&!/\[LOG\]
|
|
216
|
+
while(Date.now()<liveDeadline&&!/\[LOG\] Human live timeline/.test(firstOutput.stdout)) await sleep(50);
|
|
217
|
+
const marker=JSON.parse(await readFile(path.join(fx.state,'runtime','remote-session.json'),'utf8'));
|
|
218
|
+
assert.match(String(marker.result_session_id||''),/^[0-9a-f]{24}$/);
|
|
184
219
|
const auditDir=path.join(fx.state,'audit');await mkdir(auditDir,{recursive:true});
|
|
185
220
|
const auditFile=path.join(auditDir,'segment-00000001.jsonl');
|
|
186
221
|
const firstBatch=[
|
|
@@ -189,16 +224,19 @@ winTest('remote --resume replays sanitized tool-call and terminal result events
|
|
|
189
224
|
];
|
|
190
225
|
await writeFile(auditFile,firstBatch.map(JSON.stringify).join('\n')+'\n','utf8');
|
|
191
226
|
let deadline=Date.now()+3000;
|
|
192
|
-
while(Date.now()<deadline
|
|
193
|
-
const
|
|
194
|
-
|
|
227
|
+
while(Date.now()<deadline&&!/^S .*#2 \[resume-r\] coding_tree projects:src.*OK$/m.test(firstOutput.stdout)) await sleep(50);
|
|
228
|
+
const journalFile=path.join(fx.state,'runtime','remote-results',`${marker.result_session_id}.json`);
|
|
229
|
+
const firstJournal=JSON.parse(await readFile(journalFile,'utf8'));
|
|
230
|
+
assert.deepEqual(firstJournal.events.map(event=>event.seq),[1,2]);
|
|
231
|
+
const cursor1=`r1:${marker.result_session_id}:2`;
|
|
232
|
+
assert.doesNotMatch(firstOutput.stdout,/\[cursor r1:/);
|
|
195
233
|
const secondBatch=[
|
|
196
234
|
{seq:3,event_type:'tool.request',tool:'coding_file_read',request_id:'resume-request-2',created_at_utc:'2026-09-18T16:30:01.000Z',args:{root_id:'projects',path:'src/deadbyte-cli.mjs',token:'DO_NOT_REPLAY_SECRET'}},
|
|
197
235
|
{seq:4,event_type:'tool.result',tool:'coding_file_read',request_id:'resume-request-2',created_at_utc:'2026-09-18T16:30:01.100Z',outcome:{is_error:false}}
|
|
198
236
|
];
|
|
199
237
|
await writeFile(auditFile,secondBatch.map(JSON.stringify).join('\n')+'\n',{encoding:'utf8',flag:'a'});
|
|
200
238
|
deadline=Date.now()+3000;
|
|
201
|
-
while(Date.now()<deadline
|
|
239
|
+
while(Date.now()<deadline&&!/^R .*#4 \[resume-r\] coding_file_read projects:src\/deadbyte-cli\.mjs.*OK$/m.test(firstOutput.stdout)) await sleep(50);
|
|
202
240
|
first.stdin.end();
|
|
203
241
|
const firstEnded=await Promise.race([firstOutput.closed,sleep(6000).then(()=>({timeout:true}))]);
|
|
204
242
|
assert.equal(firstEnded.timeout,undefined);
|
|
@@ -206,19 +244,19 @@ winTest('remote --resume replays sanitized tool-call and terminal result events
|
|
|
206
244
|
second=spawn(process.execPath,[cliPath,'remote','--resume',cursor1],{env:fx.env,stdio:['pipe','pipe','pipe'],windowsHide:true});
|
|
207
245
|
const secondOutput=await collect(second);
|
|
208
246
|
deadline=Date.now()+5000;
|
|
209
|
-
while(Date.now()<deadline&&(!/coding_file_read\s+\| Arguments:
|
|
210
|
-
!/\[REPLAY\]
|
|
211
|
-
!/\[LOG\]
|
|
212
|
-
assert.match(secondOutput.stdout,/^2026-09-
|
|
213
|
-
assert.match(secondOutput.stdout,/\[REPLAY\]
|
|
214
|
-
assert.match(secondOutput.stdout
|
|
247
|
+
while(Date.now()<deadline&&(!/coding_file_read\s+\| Arguments: .*\| REPLAY #3/.test(secondOutput.stdout)||
|
|
248
|
+
!/\[REPLAY\] #4 coding_file_read OK/.test(secondOutput.stdout)||
|
|
249
|
+
!/\[LOG\] Human live timeline/.test(secondOutput.stdout))) await sleep(50);
|
|
250
|
+
assert.match(secondOutput.stdout,/^2026-09-18T23:30:01\.000\+07:00 \| coding_file_read\s+\| Arguments: .*"path":"src\/deadbyte-cli\.mjs".*\| REPLAY #3$/m);
|
|
251
|
+
assert.match(secondOutput.stdout,/\[REPLAY\] #4 coding_file_read OK/);
|
|
252
|
+
assert.match(secondOutput.stdout,/\[SESSION\] Previous result stream replayed/);
|
|
215
253
|
assert.doesNotMatch(secondOutput.stdout,/DO_NOT_REPLAY_SECRET/);
|
|
216
|
-
assert.doesNotMatch(secondOutput.stdout,/\[REPLAY\]
|
|
254
|
+
assert.doesNotMatch(secondOutput.stdout,/\[REPLAY\].*coding_tree/);
|
|
217
255
|
assert.equal((secondOutput.stdout.match(/\| coding_file_read\s+\| Arguments:/g)||[]).length,1,'tool-call replay duplicated');
|
|
218
|
-
assert.equal((secondOutput.stdout.match(/\[REPLAY\]
|
|
256
|
+
assert.equal((secondOutput.stdout.match(/\[REPLAY\] #4 coding_file_read OK/g)||[]).length,1,'terminal replay duplicated');
|
|
219
257
|
const callReplayIndex=secondOutput.stdout.indexOf('| coding_file_read');
|
|
220
|
-
const resultReplayIndex=secondOutput.stdout.indexOf('[REPLAY]
|
|
221
|
-
const liveIndex=secondOutput.stdout.indexOf('[LOG]
|
|
258
|
+
const resultReplayIndex=secondOutput.stdout.indexOf('[REPLAY] #4 coding_file_read OK');
|
|
259
|
+
const liveIndex=secondOutput.stdout.indexOf('[LOG] Human live timeline');
|
|
222
260
|
assert.ok(callReplayIndex>=0&&resultReplayIndex>callReplayIndex&&liveIndex>resultReplayIndex,'resume ordering must be call -> result -> live tail');
|
|
223
261
|
second.stdin.end();
|
|
224
262
|
const secondEnded=await Promise.race([secondOutput.closed,sleep(6000).then(()=>({timeout:true}))]);
|
|
@@ -230,17 +268,35 @@ winTest('remote --resume replays sanitized tool-call and terminal result events
|
|
|
230
268
|
}
|
|
231
269
|
});
|
|
232
270
|
|
|
233
|
-
winTest('remote --verbose
|
|
271
|
+
winTest('remote --verbose enables sanitized request detail while preserving foreground authority cleanup',async()=>{
|
|
234
272
|
const fx=await fixture();
|
|
273
|
+
let child=null;
|
|
235
274
|
try{
|
|
236
|
-
|
|
275
|
+
child=spawn(process.execPath,[cliPath,'remote','--verbose'],{env:fx.env,stdio:['pipe','pipe','pipe'],windowsHide:true});
|
|
237
276
|
const output=await collect(child);
|
|
277
|
+
await waitFor(fx.trace,commands=>commands.includes('hoststatus'));
|
|
278
|
+
assert.equal(child.exitCode,null,'verbose remote remains a foreground session');
|
|
279
|
+
assert.match(output.stdout,/\[LOG\] Verbose live timeline\. Full sanitized request detail enabled/);
|
|
280
|
+
const auditDir=path.join(fx.state,'audit');await mkdir(auditDir,{recursive:true});
|
|
281
|
+
const audit=[
|
|
282
|
+
{event_type:'tool.request',tool:'coding_tree',request_id:'verbose-request-1',created_at_utc:'2026-09-19T00:00:00.000Z',args:{root_id:'projects',path:'src',token:'DO_NOT_PRINT_VERBOSE_SECRET'}},
|
|
283
|
+
{event_type:'tool.result',tool:'coding_tree',request_id:'verbose-request-1',created_at_utc:'2026-09-19T00:00:00.050Z',outcome:{is_error:false,duration_ms:50}}
|
|
284
|
+
];
|
|
285
|
+
await writeFile(path.join(auditDir,'segment-00000001.jsonl'),audit.map(JSON.stringify).join('\n')+'\n','utf8');
|
|
286
|
+
const deadline=Date.now()+3000;
|
|
287
|
+
while(Date.now()<deadline&&!/coding_tree\s+\| Arguments:/.test(output.stdout)) await sleep(50);
|
|
288
|
+
assert.match(output.stdout,/coding_tree\s+\| Arguments: .*"path":"src".*"root_id":"projects"/);
|
|
289
|
+
assert.doesNotMatch(output.stdout,/DO_NOT_PRINT_VERBOSE_SECRET/);
|
|
290
|
+
child.stdin.end();
|
|
238
291
|
const ended=await Promise.race([output.closed,sleep(6000).then(()=>({timeout:true}))]);
|
|
239
292
|
assert.equal(ended.timeout,undefined);
|
|
240
|
-
assert.
|
|
241
|
-
|
|
242
|
-
assert.deepEqual(
|
|
243
|
-
}finally{
|
|
293
|
+
assert.equal(ended.code,0,output.stderr);
|
|
294
|
+
const commands=await traceCommands(fx.trace);
|
|
295
|
+
assert.deepEqual(commands.slice(-5),['disarmautonomous','disarmcoding','disarmprocess','disarmhost','park']);
|
|
296
|
+
}finally{
|
|
297
|
+
if(child?.exitCode===null) child.kill('SIGKILL');
|
|
298
|
+
await rm(fx.dir,{recursive:true,force:true});
|
|
299
|
+
}
|
|
244
300
|
});
|
|
245
301
|
|
|
246
302
|
winTest('detached guardian revokes authority, parks, and removes the session marker after the owner is hard-killed',async()=>{
|
|
@@ -249,9 +305,9 @@ winTest('detached guardian revokes authority, parks, and removes the session mar
|
|
|
249
305
|
try{
|
|
250
306
|
child=spawn(process.execPath,[cliPath,'remote'],{env:fx.env,stdio:['pipe','pipe','pipe'],windowsHide:true});
|
|
251
307
|
const output=await collect(child);
|
|
252
|
-
const ready=await waitFor(fx.trace,commands=>commands.slice(0,
|
|
253
|
-
assert.deepEqual(ready.slice(0,
|
|
254
|
-
'status','start','armprocess','armcoding','armautonomous',
|
|
308
|
+
const ready=await waitFor(fx.trace,commands=>commands.slice(0,10).join(',')==='status,start,status,armprocess,armcoding,armautonomous,processstatus,codingstatus,autonomousstatus,hoststatus');
|
|
309
|
+
assert.deepEqual(ready.slice(0,10),[
|
|
310
|
+
'status','start','status','armprocess','armcoding','armautonomous',
|
|
255
311
|
'processstatus','codingstatus','autonomousstatus','hoststatus'
|
|
256
312
|
]);
|
|
257
313
|
const markerPath=path.join(fx.state,'runtime','remote-session.json');
|
|
@@ -294,7 +350,7 @@ winTest('bare remote rolls back authority and parks if one plane fails to arm',a
|
|
|
294
350
|
assert.equal(ended.timeout,undefined,'failed startup did not terminate');
|
|
295
351
|
assert.notEqual(ended.code,0);
|
|
296
352
|
const commands=await traceCommands(fx.trace);
|
|
297
|
-
assert.deepEqual(commands.slice(0,
|
|
353
|
+
assert.deepEqual(commands.slice(0,5),['status','start','status','armprocess','armcoding']);
|
|
298
354
|
assert.deepEqual(commands.slice(-5),['disarmautonomous','disarmcoding','disarmprocess','disarmhost','park']);
|
|
299
355
|
assert.doesNotMatch(output.stdout,/Live logs/i);
|
|
300
356
|
}finally{await rm(fx.dir,{recursive:true,force:true});}
|
|
@@ -313,7 +369,7 @@ winTest('bare remote recovers a stale crashed session before arming a new one',a
|
|
|
313
369
|
assert.deepEqual(ready.slice(0,5),['disarmautonomous','disarmcoding','disarmprocess','disarmhost','park']);
|
|
314
370
|
assert.equal(ready[5],'status');
|
|
315
371
|
assert.equal(ready[6],'start');
|
|
316
|
-
assert.match(output.stdout
|
|
372
|
+
assert.match(output.stdout,/\[RECOVERY\] Stale remote session pid=2147483000; revoking authority before restart\./);
|
|
317
373
|
child.stdin.end();
|
|
318
374
|
const ended=await Promise.race([output.closed,sleep(6000).then(()=>({timeout:true}))]);
|
|
319
375
|
assert.equal(ended.timeout,undefined);
|
|
@@ -324,7 +380,7 @@ winTest('bare remote recovers a stale crashed session before arming a new one',a
|
|
|
324
380
|
}
|
|
325
381
|
});
|
|
326
382
|
|
|
327
|
-
winTest('remote warning lines use
|
|
383
|
+
winTest('remote warning lines use deterministic ASCII fallback on piped Windows output',async()=>{
|
|
328
384
|
const fx=await fixture();
|
|
329
385
|
let child=null;
|
|
330
386
|
try{
|
|
@@ -336,7 +392,8 @@ winTest('remote warning lines use the approved warning emoji without changing th
|
|
|
336
392
|
child.stdin.end();
|
|
337
393
|
const ended=await Promise.race([output.closed,sleep(6000).then(()=>({timeout:true}))]);
|
|
338
394
|
assert.equal(ended.timeout,undefined,'remote session did not exit after warning fixture');
|
|
339
|
-
assert.match(output.stderr
|
|
395
|
+
assert.match(output.stderr,/^\[WARN\] remote session marker cleanup:/m);
|
|
396
|
+
assert.doesNotMatch(output.stderr,/\p{Extended_Pictographic}/u);
|
|
340
397
|
}finally{
|
|
341
398
|
if(child?.exitCode===null) child.kill();
|
|
342
399
|
await rm(fx.dir,{recursive:true,force:true});
|
|
@@ -263,6 +263,50 @@ test('R31 slot-update CLI binds process-policy bootstrap and machine-FS enable e
|
|
|
263
263
|
assert.equal(parsed.enableMachineFs,true);
|
|
264
264
|
});
|
|
265
265
|
|
|
266
|
+
test('R40 slot-update CLI binds Desktop policy as one exact pair and enables runtime only explicitly', () => {
|
|
267
|
+
const base=['--stage','stage','--development-root','dev','--expected-version','0.12.0','--expected-manifest',hex('a')];
|
|
268
|
+
assert.throws(()=>parseUpdateArgs([...base,'--desktop-policy','C:/candidate/desktop.json']),/desktop policy.*pair/i);
|
|
269
|
+
assert.throws(()=>parseUpdateArgs([...base,'--desktop-policy-sha256',hex('b')]),/desktop policy.*pair/i);
|
|
270
|
+
const plain=parseUpdateArgs([...base,'--desktop-policy','C:/candidate/desktop.json','--desktop-policy-sha256',hex('b')]);
|
|
271
|
+
assert.equal(plain.desktopPolicyBootstrap.path,path.resolve('C:/candidate/desktop.json'));
|
|
272
|
+
assert.equal(plain.desktopPolicyBootstrap.sha256,hex('b'));
|
|
273
|
+
assert.equal(plain.enableDesktop,false);
|
|
274
|
+
const enabled=parseUpdateArgs([...base,'--desktop-policy','C:/candidate/desktop.json','--desktop-policy-sha256',hex('b'),'--enable-desktop']);
|
|
275
|
+
assert.equal(enabled.enableDesktop,true);
|
|
276
|
+
assert.throws(()=>parseUpdateArgs([...base,'--enable-desktop','--enable-desktop']),/duplicate self-update argument/i);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test('R40 Desktop policy bootstrap accepts only exact pinned enabled policy bytes',async()=>{
|
|
280
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r40-desktop-bootstrap-'));
|
|
281
|
+
const file=path.join(root,'desktop.json');
|
|
282
|
+
const policy={schema:'deadbyte.desktop-policy.v1',enabled:true,grant_file:'C:/old/grant.json',evidence_dir:'C:/old/evidence',
|
|
283
|
+
allowed_actions:['click_primary'],allowed_input_desktops:['Default'],limits:{},
|
|
284
|
+
observer:{executable:'C:/old/observer.exe',sha256:hex('1')},
|
|
285
|
+
input:{executable:'C:/old/input.exe',sha256:hex('2')},
|
|
286
|
+
grounder:{executable:'C:/old/uia.exe',sha256:hex('3')}};
|
|
287
|
+
const bytes=Buffer.from(JSON.stringify(policy)+'\n','utf8');await writeFile(file,bytes);
|
|
288
|
+
assert.equal(typeof runtimeUpdate.prepareDesktopPolicyBootstrap,'function');
|
|
289
|
+
const accepted=await runtimeUpdate.prepareDesktopPolicyBootstrap({path:file,sha256:sha(bytes)});
|
|
290
|
+
assert.equal(accepted.sha256,sha(bytes));assert.deepEqual(accepted.policy,policy);
|
|
291
|
+
await assert.rejects(runtimeUpdate.prepareDesktopPolicyBootstrap({path:file,sha256:hex('f')}),/desktop policy.*SHA-256 mismatch/i);
|
|
292
|
+
const bad=path.join(root,'bad.json');await writeFile(bad,JSON.stringify({...policy,enabled:false}));
|
|
293
|
+
await assert.rejects(runtimeUpdate.prepareDesktopPolicyBootstrap({path:bad,sha256:sha(await readFile(bad))}),/desktop policy schema\/enabled/i);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('R40 updater threads Desktop policy into generation and never restores Desktop authority automatically',async()=>{
|
|
297
|
+
const source=await readFile(new URL('../src/runtime-update.mjs',import.meta.url),'utf8');
|
|
298
|
+
assert.match(source,/prepareDesktopPolicyBootstrap\(desktopPolicyBootstrap\)/);
|
|
299
|
+
assert.match(source,/control\.desktopPolicy=desktopBootstrap\.policy/);
|
|
300
|
+
assert.match(source,/control\.config\.DesktopRuntimeEnabled=true/);
|
|
301
|
+
assert.match(source,/desktopPolicy:control\.desktopPolicy/);
|
|
302
|
+
assert.match(source,/desktopPolicyBootstrap:args\.desktopPolicyBootstrap/);
|
|
303
|
+
assert.match(source,/enableDesktop:args\.enableDesktop/);
|
|
304
|
+
assert.match(source,/controllerCaptured\(ctx,'disarmdesktop',15000\)/);
|
|
305
|
+
assert.doesNotMatch(source,/controllerCaptured\(ctx,'armdesktop',15000\)/);
|
|
306
|
+
assert.match(source,/desktop plane must remain disarmed after slot switch/);
|
|
307
|
+
assert.match(source,/desktop_restore_policy:'explicit-rearm-required'/);
|
|
308
|
+
});
|
|
309
|
+
|
|
266
310
|
test('R35 slot-update CLI binds semantic LSP config as one exact path plus SHA-256 pair', () => {
|
|
267
311
|
const base=['--stage','stage','--development-root','dev','--expected-version','0.9.0','--expected-manifest',hex('a')];
|
|
268
312
|
assert.throws(() => parseUpdateArgs([...base,'--semantic-lsp-config','C:/candidate/semantic.json']), /semantic LSP config.*pair/i);
|