deadbyte-mcp 0.15.36 → 0.15.37
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 +55 -27
- package/PROVENANCE.json +114 -87
- package/README.txt +12 -12
- package/RELEASE.json +214 -187
- package/SBOM.cdx.json +10 -25
- package/TEST-MATRIX.json +297 -258
- package/bridge/package.json +1 -1
- package/bridge/src/bridge-health-r59.mjs +4 -3
- package/bridge/src/bridge.mjs +71 -14
- package/bridge/src/canonical-json-r60.mjs +13 -0
- package/bridge/src/reconnect-delay-r60.mjs +31 -0
- package/bridge/src/remote-call-claims-r60.mjs +157 -0
- package/bridge/src/remote-dispatch-r60.mjs +75 -0
- package/controller/deadbyte-controller.ps1 +6 -1
- package/controller/deadbyte-process-policy.json +4 -4
- package/docs/R59-PROVENANCE.json +87 -0
- package/docs/R59-RELEASE.json +187 -0
- package/docs/R59-SBOM.cdx.json +68 -0
- package/docs/R59-TEST-MATRIX.json +258 -0
- package/docs/R59-bridge-package.json +13 -0
- package/docs/R59-package-lock.json +184 -0
- package/docs/R59-package.json +106 -0
- package/docs/R59-version.mjs +6 -0
- package/docs/R60-TASK-CONTRACT.md +22 -0
- package/package.json +5 -3
- package/scripts/full-regression-r60.mjs +32 -0
- package/scripts/gate-windows-r60-prepublish.ps1 +74 -0
- package/src/capability-snapshot.mjs +6 -2
- package/src/deadbyte-cli.mjs +80 -34
- package/src/remote-authority.mjs +56 -0
- package/src/remote-enforcement-probe-r60.mjs +17 -0
- package/src/remote-live-log.mjs +19 -2
- package/src/remote-log-decode-r60.mjs +32 -0
- package/src/remote-mcp-machine-smoke-client.mjs +19 -1
- package/src/remote-session-guardian.mjs +20 -3
- package/src/tool-audit.mjs +5 -0
- package/src/version.mjs +1 -1
- package/test/r48-release-contract.test.mjs +1 -1
- package/test/r51-runtime-coherence.test.mjs +2 -2
- package/test/r58-cleanup-retention.test.mjs +9 -3
- package/test/r58-concurrent-recovery.test.mjs +9 -3
- package/test/r58-controller-live-local.test.mjs +22 -3
- package/test/r58-stale-owner.test.mjs +2 -2
- package/test/r59-release-contract.test.mjs +6 -6
- package/test/r60-authority-coherence.test.mjs +36 -0
- package/test/r60-authority-enforcement.test.mjs +28 -0
- package/test/r60-bridge-dispatch.test.mjs +119 -0
- package/test/r60-clock-discipline.test.mjs +15 -0
- package/test/r60-live-timeline.test.mjs +29 -0
- package/test/r60-reconnect-errors.test.mjs +21 -0
- package/test/r60-reconnect-jitter.test.mjs +18 -0
- package/test/r60-release-contract.test.mjs +81 -0
- package/test/r60-remote-call-recovery.test.mjs +170 -0
- package/test/r60-windows-log-decode.test.mjs +25 -0
- package/test/remote-live-log-r37.test.mjs +1 -1
- package/test/remote-session-cli.test.mjs +200 -43
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, rm, writeFile, readFile } from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { openRemoteCallClaims } from '../bridge/src/remote-call-claims-r60.mjs';
|
|
7
|
+
import {canonicalJson as bridgeCanonicalJson} from '../bridge/src/canonical-json-r60.mjs';
|
|
8
|
+
import {canonicalJson as coreCanonicalJson} from '../src/canonical-json.mjs';
|
|
9
|
+
|
|
10
|
+
const binding={device_id:'device-1',session_id:'session-1',request_id:'call-1',tool:'coding_file_write',
|
|
11
|
+
arguments:{root_id:'fixture',path:'a.txt',content:'A'},expires_at_utc:'2030-01-01T00:00:00.000Z',
|
|
12
|
+
release_id:'v0.15.37-test',generation_manifest_sha256:'a'.repeat(64),machine_id:'d'.repeat(64),online_epoch:'c'.repeat(64),classification:'mutating'};
|
|
13
|
+
|
|
14
|
+
async function withStore(fn){
|
|
15
|
+
const dir=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r60-claims-'));
|
|
16
|
+
try{await fn(openRemoteCallClaims({root:dir,clock:()=>Date.parse('2029-01-01T00:00:00.000Z')}));}
|
|
17
|
+
finally{await rm(dir,{recursive:true,force:true});}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test('Bridge and Core canonical argument encoding remain byte-identical',()=>{
|
|
21
|
+
const value={z:[{b:2,a:1}],a:{thai:'ทดสอบ',x:null}};
|
|
22
|
+
assert.equal(bridgeCanonicalJson(value),coreCanonicalJson(value));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('claim refuses missing machine/epoch/classification identity before execution',async()=>withStore(async store=>{
|
|
26
|
+
await assert.rejects(store.claim({...binding,machine_id:undefined}),/machine_id/);
|
|
27
|
+
await assert.rejects(store.claim({...binding,online_epoch:undefined}),/online epoch/);
|
|
28
|
+
await assert.rejects(store.claim({...binding,classification:undefined}),/classification/);
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
test('duplicate request identity with same canonical payload never receives a second execution claim',async()=>withStore(async store=>{
|
|
32
|
+
const first=await store.claim(binding);
|
|
33
|
+
assert.equal(first.owner,true);
|
|
34
|
+
const second=await store.claim({...binding,arguments:{content:'A',path:'a.txt',root_id:'fixture'}});
|
|
35
|
+
assert.equal(second.owner,false);
|
|
36
|
+
assert.equal(second.state,'CLAIMED');
|
|
37
|
+
await store.dispatch(first.claim_id);
|
|
38
|
+
const uncertain=await store.claim(binding);
|
|
39
|
+
assert.equal(uncertain.owner,false);
|
|
40
|
+
assert.equal(uncertain.state,'EXECUTING');
|
|
41
|
+
assert.equal(uncertain.side_effect_state,'may_have_started');
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
test('changed payload and expired request fail closed',async()=>withStore(async store=>{
|
|
45
|
+
await store.claim(binding);
|
|
46
|
+
await assert.rejects(store.claim({...binding,arguments:{...binding.arguments,content:'B'}}),/request_id_conflict/);
|
|
47
|
+
await assert.rejects(store.claim({...binding,request_id:'expired',expires_at_utc:'2020-01-01T00:00:00.000Z'}),/expired/);
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
test('same JSON-RPC request id in a distinct session is not a duplicate',async()=>withStore(async store=>{
|
|
51
|
+
const first=await store.claim(binding);
|
|
52
|
+
const second=await store.claim({...binding,session_id:'session-2'});
|
|
53
|
+
assert.equal(first.owner,true);
|
|
54
|
+
assert.equal(second.owner,true);
|
|
55
|
+
assert.notEqual(first.claim_id,second.claim_id);
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
test('an already claimed call cannot acquire a fresh deadline after its original expiry',async()=>{
|
|
59
|
+
const dir=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r60-expiry-'));
|
|
60
|
+
let now=Date.parse('2029-01-01T00:00:00.000Z');
|
|
61
|
+
try{
|
|
62
|
+
const store=openRemoteCallClaims({root:dir,clock:()=>now});
|
|
63
|
+
await store.claim({...binding,expires_at_utc:'2029-01-01T00:00:01.000Z'});
|
|
64
|
+
now+=2000;
|
|
65
|
+
await assert.rejects(store.claim({...binding,expires_at_utc:'2029-01-02T00:00:00.000Z'}),/expired/);
|
|
66
|
+
}finally{await rm(dir,{recursive:true,force:true});}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('stable idempotency key survives transport session and JSON-RPC id changes',async()=>withStore(async store=>{
|
|
70
|
+
const first=await store.claim({...binding,idempotency_key:'goal-write-1'});
|
|
71
|
+
await store.dispatch(first.claim_id);
|
|
72
|
+
await store.terminal(first.claim_id,{status:'ok',receipt_sha256:'b'.repeat(64)});
|
|
73
|
+
const replay=await store.claim({...binding,idempotency_key:'goal-write-1',session_id:'session-2',request_id:'call-99'});
|
|
74
|
+
assert.equal(replay.owner,false);
|
|
75
|
+
assert.equal(replay.state,'TERMINAL');
|
|
76
|
+
assert.equal(replay.claim_id,first.claim_id);
|
|
77
|
+
assert.deepEqual(replay.result,{status:'ok',receipt_sha256:'b'.repeat(64)});
|
|
78
|
+
await assert.rejects(store.claim({...binding,idempotency_key:'goal-write-1',session_id:'session-3',request_id:'call-100',arguments:{...binding.arguments,content:'B'}}),/idempotency_key_conflict/);
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
test('same JSON-RPC id with a different stable key is a distinct logical mutation',async()=>withStore(async store=>{
|
|
82
|
+
const first=await store.claim({...binding,idempotency_key:'goal-write-1'});
|
|
83
|
+
const second=await store.claim({...binding,idempotency_key:'goal-write-2'});
|
|
84
|
+
assert.equal(first.owner,true);
|
|
85
|
+
assert.equal(second.owner,true);
|
|
86
|
+
assert.notEqual(first.claim_id,second.claim_id);
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
test('concurrent duplicate claims have one owner and terminal result is replayable without dispatch',async()=>withStore(async store=>{
|
|
90
|
+
const claims=await Promise.all(Array.from({length:12},()=>store.claim(binding)));
|
|
91
|
+
assert.equal(claims.filter(item=>item.owner).length,1);
|
|
92
|
+
const id=claims[0].claim_id;
|
|
93
|
+
await store.dispatch(id);
|
|
94
|
+
await assert.rejects(store.dispatch(id),/already dispatched/);
|
|
95
|
+
await store.terminal(id,{status:'ok',receipt_sha256:'b'.repeat(64)});
|
|
96
|
+
const replay=await store.claim(binding);
|
|
97
|
+
assert.equal(replay.owner,false);
|
|
98
|
+
assert.equal(replay.state,'TERMINAL');
|
|
99
|
+
assert.deepEqual(replay.result,{status:'ok',receipt_sha256:'b'.repeat(64)});
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
test('corrupted terminal bytes fail closed instead of replaying altered success',async()=>{
|
|
103
|
+
const dir=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r60-corrupt-'));
|
|
104
|
+
try{
|
|
105
|
+
const store=openRemoteCallClaims({root:dir,clock:()=>Date.parse('2029-01-01T00:00:00.000Z')});
|
|
106
|
+
const first=await store.claim(binding);
|
|
107
|
+
await store.dispatch(first.claim_id);
|
|
108
|
+
await store.terminal(first.claim_id,{status:'ok',receipt_sha256:'b'.repeat(64)});
|
|
109
|
+
const file=path.join(dir,first.claim_id.slice(0,2),`${first.claim_id}.terminal.json`);
|
|
110
|
+
const original=await readFile(file,'utf8');
|
|
111
|
+
await writeFile(file,original.replace('"status":"ok"','"status":"forged"'));
|
|
112
|
+
await assert.rejects(store.claim(binding),/terminal.*hash|corrupt/i);
|
|
113
|
+
}finally{await rm(dir,{recursive:true,force:true});}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('corrupted required claim record fails closed before any duplicate dispatch',async()=>{
|
|
117
|
+
const dir=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r60-corrupt-'));
|
|
118
|
+
try{
|
|
119
|
+
const store=openRemoteCallClaims({root:dir,clock:()=>Date.parse('2029-01-01T00:00:00.000Z')});
|
|
120
|
+
const first=await store.claim(binding);
|
|
121
|
+
const file=path.join(dir,first.claim_id.slice(0,2),`${first.claim_id}.claim.json`);
|
|
122
|
+
const original=await readFile(file,'utf8');
|
|
123
|
+
await writeFile(file,original.replace('"tool":"coding_file_write"','"tool":"host_exec"'));
|
|
124
|
+
await assert.rejects(store.claim(binding),/claim.*hash|corrupt/i);
|
|
125
|
+
}finally{await rm(dir,{recursive:true,force:true});}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('startup recovery marks only post-dispatch mutations interrupted and leaves pre-effect claims recoverable',async()=>withStore(async store=>{
|
|
129
|
+
const pending=await store.claim({...binding,request_id:'pre-effect'});
|
|
130
|
+
const uncertain=await store.claim({...binding,request_id:'post-dispatch'});
|
|
131
|
+
await store.dispatch(uncertain.claim_id);
|
|
132
|
+
const batch=await store.recover({limit:100});
|
|
133
|
+
assert.equal(batch.records.length,2);
|
|
134
|
+
assert.equal((await store.state(pending.claim_id)).state,'CLAIMED');
|
|
135
|
+
const after=await store.state(uncertain.claim_id);
|
|
136
|
+
assert.equal(after.state,'INTERRUPTED');
|
|
137
|
+
assert.equal(after.side_effect_state,'unknown');
|
|
138
|
+
}));
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
test('recovery inventory excludes terminal and expired claims while retaining eligible pre-effect work',async()=>{
|
|
142
|
+
const dir=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r60-recovery-filter-'));
|
|
143
|
+
let now=Date.parse('2029-01-01T00:00:00.000Z');
|
|
144
|
+
try{
|
|
145
|
+
const store=openRemoteCallClaims({root:dir,clock:()=>now});
|
|
146
|
+
const terminal=await store.claim({...binding,request_id:'terminal-history',expires_at_utc:'2030-01-01T00:00:00.000Z'});
|
|
147
|
+
await store.dispatch(terminal.claim_id);
|
|
148
|
+
await store.terminal(terminal.claim_id,{status:'ok',receipt_sha256:'b'.repeat(64)});
|
|
149
|
+
const expired=await store.claim({...binding,request_id:'expired-history',expires_at_utc:'2029-01-01T00:00:01.000Z'});
|
|
150
|
+
const pending=await store.claim({...binding,request_id:'eligible-pending',expires_at_utc:'2030-01-01T00:00:00.000Z'});
|
|
151
|
+
now+=2000;
|
|
152
|
+
const batch=await store.recover({limit:100});
|
|
153
|
+
const ids=new Set(batch.records.map(item=>item.claim_id));
|
|
154
|
+
assert.equal(ids.has(terminal.claim_id),false,'terminal history is not recovery work');
|
|
155
|
+
assert.equal(ids.has(expired.claim_id),false,'expired work must not consume recovery budget');
|
|
156
|
+
assert.equal(ids.has(pending.claim_id),true,'eligible pre-effect claim must remain recoverable');
|
|
157
|
+
}finally{await rm(dir,{recursive:true,force:true});}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
test('stable idempotency key cannot replay execution evidence across machine online epoch rotation',async()=>withStore(async store=>{
|
|
162
|
+
const keyed={...binding,idempotency_key:'epoch-bound-write'};
|
|
163
|
+
const first=await store.claim(keyed);
|
|
164
|
+
await store.dispatch(first.claim_id);
|
|
165
|
+
await store.terminal(first.claim_id,{status:'ok',receipt_sha256:'b'.repeat(64)});
|
|
166
|
+
await assert.rejects(
|
|
167
|
+
store.claim({...keyed,session_id:'session-after-revoke',request_id:'retry-after-revoke',online_epoch:'e'.repeat(64)}),
|
|
168
|
+
/epoch|identity drift/i
|
|
169
|
+
);
|
|
170
|
+
}));
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { decodeRemoteLogBytes, boundedRemoteDiagnostic, safeRemoteLogPrefixLength } from '../src/remote-log-decode-r60.mjs';
|
|
4
|
+
|
|
5
|
+
test('known Windows log source decodes UTF-16LE BOM without corrupting text',()=>{
|
|
6
|
+
const bytes=Buffer.concat([Buffer.from([0xff,0xfe]),Buffer.from('[TOOL] ✓ host_file_read\r\n','utf16le')]);
|
|
7
|
+
assert.equal(decodeRemoteLogBytes('controller-start.stdout.log',bytes),'[TOOL] ✓ host_file_read\r\n');
|
|
8
|
+
assert.notEqual(decodeRemoteLogBytes('segment-00000001.jsonl',bytes),'[TOOL] ✓ host_file_read\r\n');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('diagnostic preview removes NUL/control injection and remains bounded',()=>{
|
|
12
|
+
const value=boundedRemoteDiagnostic('ok\0secret\r\n'+('x'.repeat(3000)),128);
|
|
13
|
+
assert.ok(!value.includes('\0'));
|
|
14
|
+
assert.ok(value.length<=131);
|
|
15
|
+
assert.equal(value.split('\n').length,1);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('bounded log pages do not split UTF-8 or UTF-16 characters',()=>{
|
|
19
|
+
const utf8=Buffer.from('A😀','utf8');
|
|
20
|
+
assert.equal(safeRemoteLogPrefixLength(utf8.subarray(0,4),'utf8'),1);
|
|
21
|
+
assert.equal(safeRemoteLogPrefixLength(utf8,'utf8'),5);
|
|
22
|
+
const utf16=Buffer.from('A😀','utf16le');
|
|
23
|
+
assert.equal(safeRemoteLogPrefixLength(utf16.subarray(0,4),'utf16le'),2);
|
|
24
|
+
assert.equal(safeRemoteLogPrefixLength(utf16,'utf16le'),6);
|
|
25
|
+
});
|
|
@@ -33,7 +33,7 @@ test('R40 long or mutating call gets start line and terminal error carries sanit
|
|
|
33
33
|
|
|
34
34
|
test('R40 heartbeat burst folds repeated status calls instead of printing every round-trip',()=>{
|
|
35
35
|
let now=1000;
|
|
36
|
-
const tracker=createLiveTimelineTracker({clock:()=>now,heartbeatBurstLimit:5,heartbeatWindowMs:3000});
|
|
36
|
+
const tracker=createLiveTimelineTracker({clock:()=>now,foldHeartbeats:true,heartbeatBurstLimit:5,heartbeatWindowMs:3000});
|
|
37
37
|
for(let i=0;i<5;i++){
|
|
38
38
|
const id='ping-'+String(i).padStart(12,'0');
|
|
39
39
|
tracker.observe({event_type:'tool.request',tool:'machine_ping',request_id:id,args:{nonce:'mcpbench-'+i}},{sequence:30+i*2});
|
|
@@ -24,7 +24,12 @@ async function fixture({failCommand='',statusOnline=false}={}){
|
|
|
24
24
|
await mkdir(bin,{recursive:true});
|
|
25
25
|
const launcher=path.join(bin,'DEADBYTE-MCP.ps1');
|
|
26
26
|
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) { $runtime=Join-Path $env:DEADBYTE_STATE_ROOT 'runtime'; New-Item -ItemType Directory -Path $runtime -Force | Out-Null; $failure=@{ schema='deadbyte.controller-start-failure.v1'; command=$Command; failed_at_utc='2026-09-26T00:00:00.000Z'; message='fixture controller evidence detail' } | ConvertTo-Json -Compress; [IO.File]::WriteAllText((Join-Path $runtime 'controller-start-failure.json'),$failure,[Text.Encoding]::UTF8); 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 'desktopstatus' { Write-Host 'Desktop plane : DISARMED [disarmed]' }\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`;
|
|
27
|
-
|
|
27
|
+
const structured=`function PlaneArmed([string]$name,[bool]$default) { $base=Join-Path $env:DEADBYTE_STATE_ROOT ('runtime\\' + $name); if (Test-Path -LiteralPath ($base + '.disarmed')) { return $false }; if (Test-Path -LiteralPath ($base + '.armed')) { return $true }; return $default }
|
|
28
|
+
if ($Command -eq 'status' -and $Json) { $names=@('process','coding','autonomous','desktop','host'); $authority=@{}; foreach($name in $names){ $initial=($name -in @('process','coding','autonomous')) -or (($name -eq 'host') -and ($env:DEADBYTE_REMOTE_TEST_HOST_ARMED -eq '1')) -or (($name -eq 'desktop') -and ($env:DEADBYTE_REMOTE_TEST_DESKTOP_ARMED -eq '1')); $armed=PlaneArmed $name $initial; if($name -eq 'host' -and (($env:DEADBYTE_REMOTE_TEST_JSON_HOST_LIE_ALWAYS -eq '1') -or (($env:DEADBYTE_REMOTE_TEST_JSON_HOST_LIE -eq '1') -and (Test-Path -LiteralPath (Join-Path $env:DEADBYTE_STATE_ROOT 'runtime\\host.disarm-attempt'))))){$armed=$false}; $authority[$name]=@{ configured=$true; armed=$armed; reason=$(if($armed){'standing-grant'}else{'disarmed'}) } }; @{schema='deadbyte.controller-observation.v1';state='ONLINE';local_ready=$true;core_root='fixture';bridge=@{alive=$true;reason='verified'};core=@{alive=$true;reason='verified'};cloud=@{alive=$true;reason='verified'};supervisor=@{running=$true};authority=$authority} | ConvertTo-Json -Depth 5 -Compress; exit 0 }
|
|
29
|
+
if ($Command -eq 'machinetest' -and $env:DEADBYTE_REMOTE_PROBE_MODE) { $hostArmed=PlaneArmed 'host' ($env:DEADBYTE_REMOTE_TEST_HOST_ARMED -eq '1'); $desktopArmed=PlaneArmed 'desktop' ($env:DEADBYTE_REMOTE_TEST_DESKTOP_ARMED -eq '1'); $wantHost=$env:DEADBYTE_REMOTE_PROBE_MODE -in @('host','full'); $wantDesktop=$env:DEADBYTE_REMOTE_PROBE_MODE -in @('desktop','full'); if($hostArmed -ne $wantHost -or $desktopArmed -ne $wantDesktop){ Write-Error 'remote enforcement probe mismatch'; exit 9 } }
|
|
30
|
+
if ($Command -match '^(arm|disarm)(process|coding|autonomous|desktop|host)$') { $name=$Matches[2]; $base=Join-Path $env:DEADBYTE_STATE_ROOT ('runtime\\' + $name); New-Item -ItemType Directory -Path (Split-Path $base) -Force | Out-Null; if ($Matches[1] -eq 'arm') { [IO.File]::WriteAllText(($base+'.armed'),'1'); Remove-Item -LiteralPath ($base+'.disarmed') -Force -ErrorAction SilentlyContinue } elseif ($env:DEADBYTE_REMOTE_TEST_DISARM_NOOP -ne $name) { [IO.File]::WriteAllText(($base+'.disarmed'),'1'); Remove-Item -LiteralPath ($base+'.armed') -Force -ErrorAction SilentlyContinue } else { [IO.File]::WriteAllText(($base+'.disarm-attempt'),'1') } }
|
|
31
|
+
switch ($Command) {`;
|
|
32
|
+
await writeFile(launcher,ps1.replace('param([string]$Command)','param([string]$Command,[switch]$Json)').replace('switch ($Command) {',()=>structured),'utf8');
|
|
28
33
|
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};
|
|
29
34
|
}
|
|
30
35
|
|
|
@@ -76,18 +81,15 @@ winTest('bare remote reuses a fully verified ONLINE runtime without invoking blo
|
|
|
76
81
|
try{
|
|
77
82
|
child=spawn(process.execPath,[cliPath,'remote'],{env:fx.env,stdio:['pipe','pipe','pipe'],windowsHide:true});
|
|
78
83
|
const output=await collect(child);
|
|
79
|
-
const ready=await waitFor(fx.trace,commands=>commands.
|
|
80
|
-
assert.deepEqual(ready.slice(0,
|
|
81
|
-
'status','armprocess','armcoding','armautonomous',
|
|
82
|
-
'processstatus','codingstatus','autonomousstatus','desktopstatus','hoststatus'
|
|
83
|
-
]);
|
|
84
|
+
const ready=await waitFor(fx.trace,commands=>commands.filter(x=>x==='status').length>=3,{timeoutMs:8000});
|
|
85
|
+
assert.deepEqual(ready.slice(0,6),['status','status','armprocess','armcoding','armautonomous','status'],output.stderr);
|
|
84
86
|
assert.doesNotMatch(ready.join(','),/(^|,)start(,|$)/);
|
|
85
87
|
assert.equal(child.exitCode,null,'remote session must remain foreground after reusing ONLINE runtime');
|
|
86
88
|
assert.match(output.stdout,/Runtime already ONLINE; reusing verified bridge\/cloud\/supervisor/);
|
|
87
89
|
const authDeadline=Date.now()+3000;
|
|
88
|
-
while(Date.now()<authDeadline&&!/\[AUTH\] ARMED
|
|
90
|
+
while(Date.now()<authDeadline&&!/\[AUTH\] process ARMED \| coding ARMED \| autonomous ARMED \| desktop DISARMED \| host DISARMED/.test(output.stdout)) await sleep(50);
|
|
89
91
|
assert.match(output.stdout,/\[DISPLAY\] ASCII fallback: stdout_not_tty/);
|
|
90
|
-
assert.match(output.stdout,/\[AUTH\] ARMED
|
|
92
|
+
assert.match(output.stdout,/\[AUTH\] process ARMED \| coding ARMED \| autonomous ARMED \| desktop DISARMED \| host DISARMED/);
|
|
91
93
|
child.kill('SIGINT');
|
|
92
94
|
await output.closed;
|
|
93
95
|
}finally{
|
|
@@ -96,6 +98,141 @@ winTest('bare remote reuses a fully verified ONLINE runtime without invoking blo
|
|
|
96
98
|
}
|
|
97
99
|
});
|
|
98
100
|
|
|
101
|
+
winTest('bounded remote disarms a stale standing Host grant before READY',async()=>{
|
|
102
|
+
const fx=await fixture({statusOnline:true});
|
|
103
|
+
fx.env.DEADBYTE_REMOTE_TEST_HOST_ARMED='1';
|
|
104
|
+
let child=null;
|
|
105
|
+
try{
|
|
106
|
+
child=spawn(process.execPath,[cliPath,'remote'],{env:fx.env,stdio:['pipe','pipe','pipe'],windowsHide:true});
|
|
107
|
+
const output=await collect(child);
|
|
108
|
+
const commands=await waitFor(fx.trace,items=>items.filter(x=>x==='status').length>=3,{timeoutMs:8000});
|
|
109
|
+
assert.ok(commands.indexOf('disarmhost')>commands.indexOf('status'));
|
|
110
|
+
const deadline=Date.now()+3000;
|
|
111
|
+
while(Date.now()<deadline&&!/\[READY\]/.test(output.stdout)) await sleep(50);
|
|
112
|
+
assert.match(output.stdout,/\[READY\]/,output.stderr);
|
|
113
|
+
child.stdin.end();
|
|
114
|
+
assert.equal((await output.closed).code,0,output.stderr);
|
|
115
|
+
}finally{
|
|
116
|
+
if(child&&child.exitCode===null) child.kill('SIGKILL');
|
|
117
|
+
await rm(fx.dir,{recursive:true,force:true});
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
winTest('remote --full verifies all five planes before READY and revokes them on disconnect',async()=>{
|
|
122
|
+
const fx=await fixture({statusOnline:true});
|
|
123
|
+
let child=null;
|
|
124
|
+
try{
|
|
125
|
+
child=spawn(process.execPath,[cliPath,'remote','--full'],{env:fx.env,stdio:['pipe','pipe','pipe'],windowsHide:true});
|
|
126
|
+
const output=await collect(child);
|
|
127
|
+
const deadline=Date.now()+8000;
|
|
128
|
+
while(Date.now()<deadline&&!/\[READY\]/.test(output.stdout)) await sleep(50);
|
|
129
|
+
assert.match(output.stdout,/\[AUTH\] process ARMED \| coding ARMED \| autonomous ARMED \| desktop ARMED \| host ARMED/,output.stderr);
|
|
130
|
+
assert.match(output.stdout,/\[READY\]/);
|
|
131
|
+
const before=await traceCommands(fx.trace);
|
|
132
|
+
assert.ok(before.includes('armdesktop')&&before.includes('armhost'));
|
|
133
|
+
child.stdin.end();
|
|
134
|
+
assert.equal((await output.closed).code,0,output.stderr);
|
|
135
|
+
const after=await traceCommands(fx.trace);
|
|
136
|
+
assert.deepEqual(after.slice(-8),['disarmautonomous','disarmcoding','disarmprocess','disarmdesktop','disarmhost','status','machinetest','park']);
|
|
137
|
+
}finally{
|
|
138
|
+
if(child?.exitCode===null) child.kill('SIGKILL');
|
|
139
|
+
await rm(fx.dir,{recursive:true,force:true});
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
for(const [flag,armedPlane,disarmedPlane] of [['--desktop','desktop','host'],['--host','host','desktop']]){
|
|
144
|
+
winTest(`remote ${flag} arms only the requested extra plane`,async()=>{
|
|
145
|
+
const fx=await fixture({statusOnline:true});
|
|
146
|
+
let child=null;
|
|
147
|
+
try{
|
|
148
|
+
child=spawn(process.execPath,[cliPath,'remote',flag],{env:fx.env,stdio:['pipe','pipe','pipe'],windowsHide:true});
|
|
149
|
+
const output=await collect(child);
|
|
150
|
+
const deadline=Date.now()+8000;
|
|
151
|
+
while(Date.now()<deadline&&!new RegExp(`${armedPlane} ARMED`).test(output.stdout)) await sleep(50);
|
|
152
|
+
assert.match(output.stdout,/\[READY\]/,output.stderr);
|
|
153
|
+
assert.match(output.stdout,new RegExp(`${armedPlane} ARMED`));
|
|
154
|
+
assert.match(output.stdout,new RegExp(`${disarmedPlane} DISARMED`));
|
|
155
|
+
const commands=await traceCommands(fx.trace);
|
|
156
|
+
assert.ok(commands.includes(`arm${armedPlane}`));
|
|
157
|
+
assert.ok(!commands.includes(`arm${disarmedPlane}`));
|
|
158
|
+
child.stdin.end();
|
|
159
|
+
assert.equal((await output.closed).code,0,output.stderr);
|
|
160
|
+
}finally{
|
|
161
|
+
if(child?.exitCode===null) child.kill('SIGKILL');
|
|
162
|
+
await rm(fx.dir,{recursive:true,force:true});
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
winTest('conflicting remote authority modes fail before controller mutation',async()=>{
|
|
168
|
+
const fx=await fixture({statusOnline:true});
|
|
169
|
+
try{
|
|
170
|
+
const child=spawn(process.execPath,[cliPath,'remote','--host','--desktop'],{env:fx.env,stdio:['ignore','pipe','pipe'],windowsHide:true});
|
|
171
|
+
const output=await collect(child);
|
|
172
|
+
assert.notEqual((await output.closed).code,0);
|
|
173
|
+
assert.match(output.stderr,/mutually exclusive/);
|
|
174
|
+
assert.deepEqual(await traceCommands(fx.trace),[]);
|
|
175
|
+
}finally{await rm(fx.dir,{recursive:true,force:true});}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
winTest('remote refuses READY when public signed MCP execution probe fails',async()=>{
|
|
179
|
+
const fx=await fixture({statusOnline:true,failCommand:'machinetest'});
|
|
180
|
+
let child=null;
|
|
181
|
+
try{
|
|
182
|
+
child=spawn(process.execPath,[cliPath,'remote'],{env:fx.env,stdio:['pipe','pipe','pipe'],windowsHide:true});
|
|
183
|
+
const output=await collect(child);
|
|
184
|
+
const ended=await Promise.race([output.closed,sleep(6500).then(()=>({timeout:true}))]);
|
|
185
|
+
assert.equal(ended.timeout,undefined,'remote did not fail closed after execution probe failure');
|
|
186
|
+
assert.notEqual(ended.code,0);
|
|
187
|
+
assert.doesNotMatch(output.stdout,/\[READY\]|\[OK\] ONLINE/);
|
|
188
|
+
assert.match(output.stderr,/machinetest|execution probe/i);
|
|
189
|
+
}finally{
|
|
190
|
+
if(child?.exitCode===null) child.kill('SIGKILL');
|
|
191
|
+
await rm(fx.dir,{recursive:true,force:true});
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
winTest('remote refuses READY when controller snapshot lies about Host enforcement',async()=>{
|
|
196
|
+
const fx=await fixture({statusOnline:true});
|
|
197
|
+
fx.env.DEADBYTE_REMOTE_TEST_HOST_ARMED='1';
|
|
198
|
+
fx.env.DEADBYTE_REMOTE_TEST_JSON_HOST_LIE_ALWAYS='1';
|
|
199
|
+
let child=null;
|
|
200
|
+
try{
|
|
201
|
+
child=spawn(process.execPath,[cliPath,'remote'],{env:fx.env,stdio:['pipe','pipe','pipe'],windowsHide:true});
|
|
202
|
+
const output=await collect(child);
|
|
203
|
+
const ended=await Promise.race([output.closed,sleep(6500).then(()=>({timeout:true}))]);
|
|
204
|
+
assert.equal(ended.timeout,undefined,'remote did not reject split authority truth');
|
|
205
|
+
assert.notEqual(ended.code,0);
|
|
206
|
+
assert.doesNotMatch(output.stdout,/\[READY\]|\[OK\] ONLINE/);
|
|
207
|
+
assert.match(output.stderr,/machinetest|enforcement/i);
|
|
208
|
+
}finally{
|
|
209
|
+
if(child?.exitCode===null) child.kill('SIGKILL');
|
|
210
|
+
await rm(fx.dir,{recursive:true,force:true});
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
winTest('foreground cleanup retains ownership when status lies about a no-op Host revoke',async()=>{
|
|
215
|
+
const fx=await fixture({statusOnline:true});
|
|
216
|
+
fx.env.DEADBYTE_REMOTE_TEST_DISARM_NOOP='host';
|
|
217
|
+
fx.env.DEADBYTE_REMOTE_TEST_JSON_HOST_LIE='1';
|
|
218
|
+
let child=null;
|
|
219
|
+
try{
|
|
220
|
+
child=spawn(process.execPath,[cliPath,'remote','--full'],{env:fx.env,stdio:['pipe','pipe','pipe'],windowsHide:true});
|
|
221
|
+
const output=await collect(child);
|
|
222
|
+
const deadline=Date.now()+8000;
|
|
223
|
+
while(Date.now()<deadline&&!/\[READY\]/.test(output.stdout)) await sleep(50);
|
|
224
|
+
assert.match(output.stdout,/\[READY\]/,output.stderr);
|
|
225
|
+
child.stdin.end();
|
|
226
|
+
assert.notEqual((await output.closed).code,0,'cleanup must fail closed');
|
|
227
|
+
assert.match(output.stderr,/cleanup.*verification|enforcement/i);
|
|
228
|
+
assert.equal(JSON.parse(await readFile(path.join(fx.state,'runtime','remote-session.json'),'utf8')).schema,'deadbyte.remote-session.v2');
|
|
229
|
+
assert.ok(!(await traceCommands(fx.trace)).includes('park'));
|
|
230
|
+
}finally{
|
|
231
|
+
if(child?.exitCode===null) child.kill('SIGKILL');
|
|
232
|
+
await rm(fx.dir,{recursive:true,force:true});
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
99
236
|
winTest('bare remote PARKED startup does not wait for a long-lived child holding inherited pipe handles',async()=>{
|
|
100
237
|
const fx=await fixture();
|
|
101
238
|
fx.env.DEADBYTE_REMOTE_TEST_SPAWN_CHILD='1';
|
|
@@ -104,15 +241,14 @@ winTest('bare remote PARKED startup does not wait for a long-lived child holding
|
|
|
104
241
|
const startedAt=Date.now();
|
|
105
242
|
child=spawn(process.execPath,[cliPath,'remote'],{env:fx.env,stdio:['pipe','pipe','pipe'],windowsHide:true});
|
|
106
243
|
const output=await collect(child);
|
|
107
|
-
const ready=await waitFor(fx.trace,commands=>commands.
|
|
244
|
+
const ready=await waitFor(fx.trace,commands=>commands.filter(x=>x==='status').length>=4,{timeoutMs:8000});
|
|
108
245
|
const elapsed=Date.now()-startedAt;
|
|
109
|
-
assert.deepEqual(ready.slice(0,
|
|
110
|
-
'status','start','status','armprocess','armcoding','armautonomous',
|
|
111
|
-
'processstatus','codingstatus','autonomousstatus','desktopstatus','hoststatus'
|
|
112
|
-
]);
|
|
246
|
+
assert.deepEqual(ready.slice(0,8),['status','start','status','status','armprocess','armcoding','armautonomous','status']);
|
|
113
247
|
assert.ok(elapsed<7000,`PARKED startup waited on inherited child pipe handles: ${elapsed}ms`);
|
|
114
248
|
assert.equal(child.exitCode,null,'remote session must remain foreground after PARKED startup');
|
|
115
249
|
assert.match(output.stdout,/\[BOOT\] Starting bridge\/runtime\.\.\./);
|
|
250
|
+
const readyDeadline=Date.now()+3000;
|
|
251
|
+
while(Date.now()<readyDeadline&&!/\[OK\] ONLINE/.test(output.stdout)) await sleep(50);
|
|
116
252
|
assert.match(output.stdout,/\[OK\] ONLINE/);
|
|
117
253
|
child.stdin.end();
|
|
118
254
|
const ended=await Promise.race([output.closed,sleep(6000).then(()=>({timeout:true}))]);
|
|
@@ -132,14 +268,11 @@ winTest('bare remote uses one unified live timeline, keeps authority semantics,
|
|
|
132
268
|
try{
|
|
133
269
|
child=spawn(process.execPath,[cliPath,'remote'],{env:fx.env,stdio:['pipe','pipe','pipe'],windowsHide:true});
|
|
134
270
|
const output=await collect(child);
|
|
135
|
-
const ready=await waitFor(fx.trace,commands=>commands.
|
|
136
|
-
assert.deepEqual(ready.slice(0,
|
|
137
|
-
'status','start','status','armprocess','armcoding','armautonomous',
|
|
138
|
-
'processstatus','codingstatus','autonomousstatus','desktopstatus','hoststatus'
|
|
139
|
-
]);
|
|
271
|
+
const ready=await waitFor(fx.trace,commands=>commands.filter(x=>x==='status').length>=4);
|
|
272
|
+
assert.deepEqual(ready.slice(0,8),['status','start','status','status','armprocess','armcoding','armautonomous','status']);
|
|
140
273
|
assert.equal(child.exitCode,null,'remote session must remain foreground after becoming ready');
|
|
141
274
|
const tailDeadline=Date.now()+3000;
|
|
142
|
-
while(Date.now()<tailDeadline&&!/\[AUTH\] ARMED
|
|
275
|
+
while(Date.now()<tailDeadline&&!/\[AUTH\] process ARMED \| coding ARMED \| autonomous ARMED \| desktop DISARMED \| host DISARMED/.test(output.stdout)) await sleep(50);
|
|
143
276
|
assert.match(output.stdout,/\[DISPLAY\] ASCII fallback: stdout_not_tty/);
|
|
144
277
|
assert.match(output.stdout,/\[DEVICE\] Starting DEADBYTE MCP/);
|
|
145
278
|
assert.match(output.stdout,/\[BRIDGE\] Local bridge connected/);
|
|
@@ -148,7 +281,7 @@ winTest('bare remote uses one unified live timeline, keeps authority semantics,
|
|
|
148
281
|
assert.match(output.stdout,/\[BOOT\] Starting local \+ remote runtime/);
|
|
149
282
|
assert.match(output.stdout,/\[OK\] ONLINE/);
|
|
150
283
|
assert.match(output.stdout,/\[READY\] DEADBYTE MCP online/);
|
|
151
|
-
assert.match(output.stdout,/\[AUTH\] ARMED
|
|
284
|
+
assert.match(output.stdout,/\[AUTH\] process ARMED \| coding ARMED \| autonomous ARMED \| desktop DISARMED \| host DISARMED/);
|
|
152
285
|
assert.match(output.stdout,/\[LOG\] Human live timeline\. Transport IN\/OUT hidden; full replay\/audit detail remains on disk\./);
|
|
153
286
|
assert.doesNotMatch(output.stdout,/Process plane\s*:/);
|
|
154
287
|
assert.doesNotMatch(output.stdout,/Coding plane\s*:/);
|
|
@@ -194,7 +327,7 @@ winTest('bare remote uses one unified live timeline, keeps authority semantics,
|
|
|
194
327
|
const ended=await Promise.race([output.closed,sleep(6000).then(()=>({timeout:true}))]);
|
|
195
328
|
assert.equal(ended.timeout,undefined,'remote session did not exit after foreground input closed');
|
|
196
329
|
const commands=await traceCommands(fx.trace);
|
|
197
|
-
assert.deepEqual(commands.slice(-
|
|
330
|
+
assert.deepEqual(commands.slice(-8),['disarmautonomous','disarmcoding','disarmprocess','disarmdesktop','disarmhost','status','machinetest','park']);
|
|
198
331
|
assert.match(output.stdout,/^DEADBYTE MCP$/m);
|
|
199
332
|
assert.match(output.stdout,/Human live timeline/i);
|
|
200
333
|
assert.match(output.stdout,/\[AUTH\] Revoking session authority/);
|
|
@@ -212,7 +345,7 @@ winTest('remote --resume replays sanitized tool-call and terminal result events
|
|
|
212
345
|
try{
|
|
213
346
|
first=spawn(process.execPath,[cliPath,'remote'],{env:fx.env,stdio:['pipe','pipe','pipe'],windowsHide:true});
|
|
214
347
|
const firstOutput=await collect(first);
|
|
215
|
-
await waitFor(fx.trace,commands=>commands.
|
|
348
|
+
await waitFor(fx.trace,commands=>commands.filter(x=>x==='status').length>=4);
|
|
216
349
|
const liveDeadline=Date.now()+3000;
|
|
217
350
|
while(Date.now()<liveDeadline&&!/\[LOG\] Human live timeline/.test(firstOutput.stdout)) await sleep(50);
|
|
218
351
|
const marker=JSON.parse(await readFile(path.join(fx.state,'runtime','remote-session.json'),'utf8'));
|
|
@@ -275,7 +408,7 @@ winTest('remote --verbose enables sanitized request detail while preserving fore
|
|
|
275
408
|
try{
|
|
276
409
|
child=spawn(process.execPath,[cliPath,'remote','--verbose'],{env:fx.env,stdio:['pipe','pipe','pipe'],windowsHide:true});
|
|
277
410
|
const output=await collect(child);
|
|
278
|
-
await waitFor(fx.trace,commands=>commands.
|
|
411
|
+
await waitFor(fx.trace,commands=>commands.filter(x=>x==='status').length>=4);
|
|
279
412
|
assert.equal(child.exitCode,null,'verbose remote remains a foreground session');
|
|
280
413
|
assert.match(output.stdout,/\[LOG\] Verbose live timeline\. Full sanitized request detail enabled/);
|
|
281
414
|
const auditDir=path.join(fx.state,'audit');await mkdir(auditDir,{recursive:true});
|
|
@@ -293,7 +426,7 @@ winTest('remote --verbose enables sanitized request detail while preserving fore
|
|
|
293
426
|
assert.equal(ended.timeout,undefined);
|
|
294
427
|
assert.equal(ended.code,0,output.stderr);
|
|
295
428
|
const commands=await traceCommands(fx.trace);
|
|
296
|
-
assert.deepEqual(commands.slice(-
|
|
429
|
+
assert.deepEqual(commands.slice(-8),['disarmautonomous','disarmcoding','disarmprocess','disarmdesktop','disarmhost','status','machinetest','park']);
|
|
297
430
|
}finally{
|
|
298
431
|
if(child?.exitCode===null) child.kill('SIGKILL');
|
|
299
432
|
await rm(fx.dir,{recursive:true,force:true});
|
|
@@ -306,11 +439,8 @@ winTest('detached guardian revokes authority, parks, and removes the session mar
|
|
|
306
439
|
try{
|
|
307
440
|
child=spawn(process.execPath,[cliPath,'remote'],{env:fx.env,stdio:['pipe','pipe','pipe'],windowsHide:true});
|
|
308
441
|
const output=await collect(child);
|
|
309
|
-
const ready=await waitFor(fx.trace,commands=>commands.
|
|
310
|
-
assert.deepEqual(ready.slice(0,
|
|
311
|
-
'status','start','status','armprocess','armcoding','armautonomous',
|
|
312
|
-
'processstatus','codingstatus','autonomousstatus','desktopstatus','hoststatus'
|
|
313
|
-
]);
|
|
442
|
+
const ready=await waitFor(fx.trace,commands=>commands.filter(x=>x==='status').length>=4);
|
|
443
|
+
assert.deepEqual(ready.slice(0,8),['status','start','status','status','armprocess','armcoding','armautonomous','status']);
|
|
314
444
|
const markerPath=path.join(fx.state,'runtime','remote-session.json');
|
|
315
445
|
const markerDeadline=Date.now()+3000;
|
|
316
446
|
let marker=null;
|
|
@@ -327,8 +457,8 @@ winTest('detached guardian revokes authority, parks, and removes the session mar
|
|
|
327
457
|
assert.equal(killed,true,'failed to hard-kill remote owner');
|
|
328
458
|
await Promise.race([output.closed,sleep(3000)]);
|
|
329
459
|
|
|
330
|
-
const cleaned=await waitFor(fx.trace,commands=>commands.slice(-
|
|
331
|
-
assert.deepEqual(cleaned.slice(-
|
|
460
|
+
const cleaned=await waitFor(fx.trace,commands=>commands.slice(-8).join(',')==='disarmautonomous,disarmcoding,disarmprocess,disarmdesktop,disarmhost,status,machinetest,park',{timeoutMs:10000});
|
|
461
|
+
assert.deepEqual(cleaned.slice(-8),['disarmautonomous','disarmcoding','disarmprocess','disarmdesktop','disarmhost','status','machinetest','park']);
|
|
332
462
|
const markerGoneDeadline=Date.now()+3000;
|
|
333
463
|
let markerGone=false;
|
|
334
464
|
while(Date.now()<markerGoneDeadline){
|
|
@@ -342,6 +472,30 @@ winTest('detached guardian revokes authority, parks, and removes the session mar
|
|
|
342
472
|
}
|
|
343
473
|
});
|
|
344
474
|
|
|
475
|
+
winTest('guardian retains ownership marker when disarm exits zero but Host remains armed',async()=>{
|
|
476
|
+
const fx=await fixture({statusOnline:true});
|
|
477
|
+
fx.env.DEADBYTE_REMOTE_TEST_DISARM_NOOP='host';
|
|
478
|
+
let child=null;
|
|
479
|
+
try{
|
|
480
|
+
child=spawn(process.execPath,[cliPath,'remote','--full'],{env:fx.env,stdio:['pipe','pipe','pipe'],windowsHide:true});
|
|
481
|
+
const output=await collect(child);
|
|
482
|
+
const deadline=Date.now()+8000;
|
|
483
|
+
while(Date.now()<deadline&&!/\[READY\]/.test(output.stdout)) await sleep(50);
|
|
484
|
+
assert.match(output.stdout,/\[READY\]/,output.stderr);
|
|
485
|
+
child.kill('SIGKILL');
|
|
486
|
+
await output.closed;
|
|
487
|
+
await waitFor(fx.trace,commands=>commands.includes('disarmhost'),{timeoutMs:8000});
|
|
488
|
+
await sleep(750);
|
|
489
|
+
const marker=JSON.parse(await readFile(path.join(fx.state,'runtime','remote-session.json'),'utf8'));
|
|
490
|
+
assert.equal(marker.schema,'deadbyte.remote-session.v2');
|
|
491
|
+
const commands=await traceCommands(fx.trace);
|
|
492
|
+
assert.ok(!commands.includes('park'),'guardian must not park after unverified revocation');
|
|
493
|
+
}finally{
|
|
494
|
+
if(child?.exitCode===null) child.kill('SIGKILL');
|
|
495
|
+
await rm(fx.dir,{recursive:true,force:true});
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
345
499
|
winTest('bare remote rolls back authority and parks if one plane fails to arm',async()=>{
|
|
346
500
|
const fx=await fixture({failCommand:'armcoding'});
|
|
347
501
|
try{
|
|
@@ -351,8 +505,8 @@ winTest('bare remote rolls back authority and parks if one plane fails to arm',a
|
|
|
351
505
|
assert.equal(ended.timeout,undefined,'failed startup did not terminate');
|
|
352
506
|
assert.notEqual(ended.code,0);
|
|
353
507
|
const commands=await traceCommands(fx.trace);
|
|
354
|
-
assert.deepEqual(commands.slice(0,
|
|
355
|
-
assert.deepEqual(commands.slice(-
|
|
508
|
+
assert.deepEqual(commands.slice(0,6),['status','start','status','status','armprocess','armcoding']);
|
|
509
|
+
assert.deepEqual(commands.slice(-8),['disarmautonomous','disarmcoding','disarmprocess','disarmdesktop','disarmhost','status','machinetest','park']);
|
|
356
510
|
assert.doesNotMatch(output.stdout,/Live logs/i);
|
|
357
511
|
}finally{await rm(fx.dir,{recursive:true,force:true});}
|
|
358
512
|
});
|
|
@@ -366,10 +520,10 @@ winTest('bare remote recovers a stale crashed session before arming a new one',a
|
|
|
366
520
|
await writeFile(path.join(runtime,'remote-session.json'),JSON.stringify({schema:'deadbyte.remote-session.v1',pid:2147483000,started_at_utc:'2026-01-01T00:00:00.000Z'}),'utf8');
|
|
367
521
|
child=spawn(process.execPath,[cliPath,'remote'],{env:fx.env,stdio:['pipe','pipe','pipe'],windowsHide:true});
|
|
368
522
|
const output=await collect(child);
|
|
369
|
-
const ready=await waitFor(fx.trace,commands=>commands.
|
|
370
|
-
assert.deepEqual(ready.slice(0,
|
|
371
|
-
assert.equal(ready[
|
|
372
|
-
assert.equal(ready[
|
|
523
|
+
const ready=await waitFor(fx.trace,commands=>commands.filter(x=>x==='status').length>=5);
|
|
524
|
+
assert.deepEqual(ready.slice(0,8),['disarmautonomous','disarmcoding','disarmprocess','disarmdesktop','disarmhost','status','machinetest','park']);
|
|
525
|
+
assert.equal(ready[8],'status');
|
|
526
|
+
assert.equal(ready[9],'start');
|
|
373
527
|
assert.match(output.stdout,/\[RECOVERY\] Stale remote session pid=2147483000; revoking authority before restart\./);
|
|
374
528
|
child.stdin.end();
|
|
375
529
|
const ended=await Promise.race([output.closed,sleep(6000).then(()=>({timeout:true}))]);
|
|
@@ -387,7 +541,7 @@ winTest('remote warning lines use deterministic ASCII fallback on piped Windows
|
|
|
387
541
|
try{
|
|
388
542
|
child=spawn(process.execPath,[cliPath,'remote'],{env:fx.env,stdio:['pipe','pipe','pipe'],windowsHide:true});
|
|
389
543
|
const output=await collect(child);
|
|
390
|
-
await waitFor(fx.trace,commands=>commands.
|
|
544
|
+
await waitFor(fx.trace,commands=>commands.filter(x=>x==='status').length>=4);
|
|
391
545
|
const markerPath=path.join(fx.state,'runtime','remote-session.json');
|
|
392
546
|
await writeFile(markerPath,'{invalid-json\n','utf8');
|
|
393
547
|
child.stdin.end();
|
|
@@ -408,8 +562,8 @@ winTest('R58 concurrent cold remote claims have one winner and no loser cleanup'
|
|
|
408
562
|
const child=spawn(process.execPath,[cliPath,'remote'],{env:fx.env,stdio:['pipe','pipe','pipe'],windowsHide:true});
|
|
409
563
|
children.push(child);outputs.push(await collect(child));
|
|
410
564
|
}
|
|
411
|
-
const commands=await waitFor(fx.trace,items=>items.
|
|
412
|
-
assert.ok(commands.
|
|
565
|
+
const commands=await waitFor(fx.trace,items=>items.filter(x=>x==='status').length>=3);
|
|
566
|
+
assert.ok(commands.filter(x=>x==='status').length>=3,outputs.map(o=>o.stderr).join('\n'));
|
|
413
567
|
const deadline=Date.now()+5000;
|
|
414
568
|
while(Date.now()<deadline&&children.filter(c=>c.exitCode===null).length>1)await sleep(50);
|
|
415
569
|
const live=children.filter(c=>c.exitCode===null);
|
|
@@ -432,8 +586,11 @@ winTest('R58 duplicate remote verifies the real live owner and leaves its author
|
|
|
432
586
|
try{
|
|
433
587
|
first=spawn(process.execPath,[cliPath,'remote'],{env:fx.env,stdio:['pipe','pipe','pipe'],windowsHide:true});
|
|
434
588
|
const firstOutput=await collect(first);
|
|
435
|
-
const commands=await waitFor(fx.trace,items=>items.
|
|
436
|
-
assert.ok(commands.
|
|
589
|
+
const commands=await waitFor(fx.trace,items=>items.filter(x=>x==='status').length>=3);
|
|
590
|
+
assert.ok(commands.filter(x=>x==='status').length>=3,firstOutput.stderr);
|
|
591
|
+
const ownerReadyDeadline=Date.now()+3000;
|
|
592
|
+
while(Date.now()<ownerReadyDeadline&&!/\[READY\]/.test(firstOutput.stdout)) await sleep(50);
|
|
593
|
+
assert.match(firstOutput.stdout,/\[READY\]/,firstOutput.stderr);
|
|
437
594
|
const before=await traceCommands(fx.trace);
|
|
438
595
|
const markerPath=path.join(fx.state,'runtime','remote-session.json');
|
|
439
596
|
const marker=JSON.parse(await readFile(markerPath,'utf8'));
|
|
@@ -529,7 +686,7 @@ winTest('remote --trace --filter selects presentation without dropping journaled
|
|
|
529
686
|
env:fx.env,stdio:['pipe','pipe','pipe'],windowsHide:true
|
|
530
687
|
});
|
|
531
688
|
const output=await collect(child);
|
|
532
|
-
await waitFor(fx.trace,commands=>commands.
|
|
689
|
+
await waitFor(fx.trace,commands=>commands.filter(x=>x==='status').length>=4);
|
|
533
690
|
const marker=JSON.parse(await readFile(path.join(fx.state,'runtime','remote-session.json'),'utf8'));
|
|
534
691
|
const auditDir=path.join(fx.state,'audit');await mkdir(auditDir,{recursive:true});
|
|
535
692
|
const audit=[
|