deadbyte-mcp 0.15.34 → 0.15.35
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 +67 -22
- package/PROVENANCE.json +57 -30
- package/README.txt +18 -14
- package/RELEASE.json +40 -31
- package/SBOM.cdx.json +25 -3
- package/TEST-MATRIX.json +129 -48
- package/bridge/package.json +11 -0
- package/bridge/src/bridge.mjs +412 -0
- package/bridge/src/node-http-adapter.mjs +56 -0
- package/bridge/src/public-tool-definitions.mjs +14 -0
- package/bridge/src/r40-governor.mjs +160 -0
- package/controller/deadbyte-controller.ps1 +84 -8
- package/controller/deadbyte-process-policy.json +4 -4
- package/package.json +5 -3
- package/scripts/bridge-benchmark-r58.mjs +67 -0
- package/scripts/deploy-r48.mjs +2 -3
- package/scripts/gate-windows-r58.ps1 +33 -0
- package/scripts/owner-probe-r58.mjs +31 -0
- package/scripts/performance-r58.mjs +57 -0
- package/scripts/public-baseline-r58.mjs +72 -0
- package/scripts/public-surface-r58.mjs +84 -0
- package/scripts/r58-cross-plane.ps1 +6 -0
- package/scripts/r58-focused.ps1 +6 -0
- package/scripts/windows-gate-evidence-r58.mjs +93 -0
- package/src/deadbyte-cli.mjs +40 -12
- package/src/doctor-foreground-r58.mjs +44 -0
- package/src/doctor-installation-r58.mjs +63 -0
- package/src/doctor-r45.mjs +21 -4
- package/src/doctor-runtime-r58.mjs +82 -0
- package/src/doctor-transactions-r58.mjs +40 -0
- package/src/foreground-process-identity.mjs +27 -0
- package/src/foreground-session-lock.mjs +20 -0
- package/src/foreground-session-lock.ps1 +15 -0
- package/src/release-generation.mjs +1 -0
- package/src/release-slot.mjs +2 -1
- package/src/remote-session-guardian.mjs +8 -1
- package/src/runtime-update.mjs +85 -2
- package/src/version.mjs +1 -1
- package/test/controller-r211.test.mjs +4 -1
- package/test/controller.test.mjs +2 -1
- package/test/helpers/r58-doctor-fixture.mjs +26 -0
- package/test/r49-windows-gate.test.mjs +2 -2
- package/test/r57-release-contract.test.mjs +5 -3
- package/test/r58-bridge-dependency-binding.test.mjs +62 -0
- package/test/r58-bundled-bridge.test.mjs +22 -0
- package/test/r58-cleanup-retention.test.mjs +45 -0
- package/test/r58-completed-update-receipt.test.mjs +76 -0
- package/test/r58-concurrent-recovery.test.mjs +54 -0
- package/test/r58-controller-core-observation.test.mjs +36 -0
- package/test/r58-controller-json.test.mjs +48 -0
- package/test/r58-controller-live-local.test.mjs +85 -0
- package/test/r58-controller-readonly.test.mjs +23 -0
- package/test/r58-controller-state-coherence.test.mjs +45 -0
- package/test/r58-doctor-cli.test.mjs +20 -0
- package/test/r58-doctor-foreground.test.mjs +50 -0
- package/test/r58-doctor-installation.test.mjs +55 -0
- package/test/r58-doctor-pointer.test.mjs +41 -0
- package/test/r58-doctor-runtime.test.mjs +45 -0
- package/test/r58-doctor-transactions.test.mjs +21 -0
- package/test/r58-guardian-channel.test.mjs +26 -0
- package/test/r58-launcher-shell.test.mjs +28 -0
- package/test/r58-owner-identity.test.mjs +20 -0
- package/test/r58-release-contract.test.mjs +49 -0
- package/test/r58-session-lock.test.mjs +26 -0
- package/test/r58-stale-owner.test.mjs +71 -0
- package/test/r58-windows-gate.test.mjs +31 -0
- package/test/release-generation.test.mjs +4 -1
- package/test/remote-session-cli.test.mjs +55 -2
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {mkdir,mkdtemp} from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {spawn} from 'node:child_process';
|
|
6
|
+
import {setTimeout as delay} from 'node:timers/promises';
|
|
7
|
+
import {acquireForegroundSessionLock} from '../src/foreground-session-lock.mjs';
|
|
8
|
+
|
|
9
|
+
test('R58 startup lock excludes competitors and releases after owner hard death without stale lockfiles',{timeout:15000},async()=>{
|
|
10
|
+
const base='D:/Projects/deadbyte-mcp/testing/0.15.35/fixtures';await mkdir(base,{recursive:true});const root=await mkdtemp(path.join(base,'session-lock-'));
|
|
11
|
+
const powershell=path.join(process.env.SystemRoot,'System32/WindowsPowerShell/v1.0/powershell.exe');
|
|
12
|
+
const moduleUrl=new URL('../src/foreground-session-lock.mjs',import.meta.url).href;
|
|
13
|
+
const script=`const {acquireForegroundSessionLock}=await import(${JSON.stringify(moduleUrl)});await acquireForegroundSessionLock(${JSON.stringify({root,powershell})});console.log('OWNER_LOCK_READY');process.stdin.resume();`;
|
|
14
|
+
const owner=spawn(process.execPath,['--input-type=module','-e',script],{windowsHide:true,stdio:['pipe','pipe','pipe']});let stdout='',stderr='';
|
|
15
|
+
owner.stdout.on('data',b=>{stdout+=b;});owner.stderr.on('data',b=>{stderr+=b;});const exited=new Promise(resolve=>owner.once('exit',resolve));
|
|
16
|
+
let lock;
|
|
17
|
+
try{
|
|
18
|
+
const until=Date.now()+4000;while(!stdout.includes('OWNER_LOCK_READY')&&owner.exitCode===null&&Date.now()<until)await delay(20);
|
|
19
|
+
assert.match(stdout,/OWNER_LOCK_READY/,stderr);
|
|
20
|
+
await assert.rejects(acquireForegroundSessionLock({root,powershell}),/already claimed/);
|
|
21
|
+
owner.kill('SIGKILL');await exited;
|
|
22
|
+
const deadline=Date.now()+5000;
|
|
23
|
+
while(Date.now()<deadline){try{lock=await acquireForegroundSessionLock({root,powershell});break;}catch(error){if(!/already claimed/.test(error.message))throw error;await delay(20);}}
|
|
24
|
+
assert.ok(lock,'dead owner must not require manual lockfile removal');lock.assertHeld();await lock.release();lock=null;
|
|
25
|
+
}finally{if(owner.exitCode===null&&owner.signalCode===null)owner.kill();if(lock)await lock.release();}
|
|
26
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {mkdir,mkdtemp,writeFile,readFile} from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {fileURLToPath} from 'node:url';
|
|
6
|
+
import {spawn} from 'node:child_process';
|
|
7
|
+
import {queryForegroundIdentity} from '../src/foreground-process-identity.mjs';
|
|
8
|
+
|
|
9
|
+
test('R58 inconsistent owner PID binding cannot trigger stale-session authority cleanup', {timeout:20000}, async()=>{
|
|
10
|
+
const base='D:/Projects/deadbyte-mcp/testing/0.15.35/fixtures';await mkdir(base,{recursive:true});
|
|
11
|
+
const root=await mkdtemp(path.join(base,'owner-binding-'));await mkdir(path.join(root,'runtime'));await mkdir(path.join(root,'bin'));
|
|
12
|
+
const trace=path.join(root,'trace.txt');
|
|
13
|
+
await writeFile(path.join(root,'bin','DEADBYTE-MCP.ps1'),`param([string]$Command)\n[IO.File]::AppendAllText('${trace.replaceAll("'","''")}',$Command+[Environment]::NewLine)\nexit 0\n`);
|
|
14
|
+
const identity=queryForegroundIdentity(process.pid);
|
|
15
|
+
const marker={schema:'deadbyte.remote-session.v2',owner_pid:process.pid,guardian_pid:0,session_nonce:'c'.repeat(64),owner_identity:{...identity,pid:process.pid+1}};
|
|
16
|
+
const markerPath=path.join(root,'runtime','remote-session.json');const bytes=JSON.stringify(marker);await writeFile(markerPath,bytes);
|
|
17
|
+
const child=spawn(process.execPath,[fileURLToPath(new URL('../src/deadbyte-cli.mjs',import.meta.url)),'remote'],{env:{...process.env,DEADBYTE_STATE_ROOT:root},windowsHide:true,stdio:['pipe','pipe','pipe']});
|
|
18
|
+
let stderr='';child.stderr.on('data',b=>{stderr+=b;});child.stdout.resume();
|
|
19
|
+
const code=await new Promise(resolve=>child.once('close',resolve));
|
|
20
|
+
await writeFile(path.join(root,'observation.json'),JSON.stringify({code,stderr}),{flag:'wx'});
|
|
21
|
+
assert.notEqual(code,0);assert.match(stderr,/ownership identity|binding/i);
|
|
22
|
+
assert.equal(await readFile(trace,'utf8').catch(e=>e.code==='ENOENT'?'':Promise.reject(e)),'','inconsistent marker must cause no controller actions');
|
|
23
|
+
assert.equal(await readFile(markerPath,'utf8'),bytes,'retain ambiguous ownership evidence');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('R58 live unrelated PID is not accepted as a remote foreground owner', {timeout:20000}, async()=>{
|
|
27
|
+
const base='D:/Projects/deadbyte-mcp/testing/0.15.35/fixtures';
|
|
28
|
+
await mkdir(base,{recursive:true});const root=await mkdtemp(path.join(base,'stale-owner-'));
|
|
29
|
+
await mkdir(path.join(root,'runtime'));await mkdir(path.join(root,'bin'));
|
|
30
|
+
const trace=path.join(root,'trace.txt');
|
|
31
|
+
await writeFile(path.join(root,'bin','DEADBYTE-MCP.ps1'),`param([string]$Command)\n[IO.File]::AppendAllText('${trace.replaceAll("'","''")}',$Command+[Environment]::NewLine)\nexit 0\n`);
|
|
32
|
+
// The test runner is alive but neither a remote CLI nor a guardian. This
|
|
33
|
+
// represents a stale numeric PID now occupied by an unrelated process.
|
|
34
|
+
const marker={schema:'deadbyte.remote-session.v2',owner_pid:process.pid,guardian_pid:process.pid,session_nonce:'a'.repeat(64),started_at_utc:'2000-01-01T00:00:00.000Z'};
|
|
35
|
+
await writeFile(path.join(root,'runtime','remote-session.json'),JSON.stringify(marker));
|
|
36
|
+
const child=spawn(process.execPath,[fileURLToPath(new URL('../src/deadbyte-cli.mjs',import.meta.url)),'remote'],{env:{...process.env,DEADBYTE_STATE_ROOT:root},windowsHide:true,stdio:['pipe','pipe','pipe']});
|
|
37
|
+
let stdout='',stderr='';child.stdout.on('data',b=>{stdout+=b;});child.stderr.on('data',b=>{stderr+=b;});
|
|
38
|
+
const code=await new Promise(resolve=>child.once('close',resolve));
|
|
39
|
+
await writeFile(path.join(root,'observation.json'),JSON.stringify({code,stdout,stderr}),{flag:'wx'});
|
|
40
|
+
assert.doesNotMatch(stderr,/remote session already active pid=/,'PID alone is not ownership proof');
|
|
41
|
+
// Fail-closed ambiguity is acceptable; do not kill the unrelated process or
|
|
42
|
+
// mutate authority based on unverified ownership. Automatic convergence still
|
|
43
|
+
// requires separately observed exact owner/guardian identity evidence.
|
|
44
|
+
assert.match(stderr,/identity|ownership|ambiguous|unverified/i);
|
|
45
|
+
assert.deepEqual(JSON.parse(await readFile(path.join(root,'runtime','remote-session.json'),'utf8')),marker);
|
|
46
|
+
assert.notEqual(code,0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('R58 bound stale birth identity reconciles on the first invocation without killing a reused PID', {timeout:20000}, async()=>{
|
|
50
|
+
const base='D:/Projects/deadbyte-mcp/testing/0.15.35/fixtures';
|
|
51
|
+
await mkdir(base,{recursive:true});const root=await mkdtemp(path.join(base,'stale-birth-'));
|
|
52
|
+
await mkdir(path.join(root,'runtime'));await mkdir(path.join(root,'bin'));
|
|
53
|
+
const trace=path.join(root,'trace.txt');
|
|
54
|
+
await writeFile(path.join(root,'bin','DEADBYTE-MCP.ps1'),`param([string]$Command)\n[IO.File]::AppendAllText('${trace.replaceAll("'","''")}',$Command+[Environment]::NewLine)\nexit 0\n`);
|
|
55
|
+
const current=queryForegroundIdentity(process.pid);
|
|
56
|
+
const stale={...current,creation_utc:'2000-01-01T00:00:00.000Z'};
|
|
57
|
+
const marker={schema:'deadbyte.remote-session.v2',owner_pid:process.pid,guardian_pid:process.pid,session_nonce:'b'.repeat(64),owner_identity:stale,guardian_identity:stale};
|
|
58
|
+
const markerPath=path.join(root,'runtime','remote-session.json');
|
|
59
|
+
await writeFile(markerPath,JSON.stringify(marker));
|
|
60
|
+
const child=spawn(process.execPath,[fileURLToPath(new URL('../src/deadbyte-cli.mjs',import.meta.url)),'remote'],{env:{...process.env,DEADBYTE_STATE_ROOT:root},windowsHide:true,stdio:['pipe','pipe','pipe']});
|
|
61
|
+
let stdout='',stderr='';child.stdout.on('data',b=>{stdout+=b;});child.stderr.on('data',b=>{stderr+=b;});
|
|
62
|
+
const code=await new Promise(resolve=>child.once('close',resolve));
|
|
63
|
+
await writeFile(path.join(root,'observation.json'),JSON.stringify({code,stdout,stderr,simulation:'birth identity drift at a live unrelated numeric PID'}),{flag:'wx'});
|
|
64
|
+
assert.match(stdout,/Stale remote session/);
|
|
65
|
+
assert.doesNotMatch(stderr,/already active|ownership unverified/);
|
|
66
|
+
assert.deepEqual((await readFile(trace,'utf8')).trim().split(/\r?\n/).slice(0,6),['disarmautonomous','disarmcoding','disarmprocess','disarmdesktop','disarmhost','park']);
|
|
67
|
+
assert.match(stderr,/without a verified ONLINE runtime/,'fixture deliberately has no real runtime; recovery must already have converged');
|
|
68
|
+
await assert.rejects(readFile(markerPath),{code:'ENOENT'});
|
|
69
|
+
assert.equal(foregroundAlive(),true,'unrelated live process must not be killed');
|
|
70
|
+
function foregroundAlive(){try{process.kill(process.pid,0);return true;}catch{return false;}}
|
|
71
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {readFile} from 'node:fs/promises';
|
|
4
|
+
|
|
5
|
+
const read=rel=>readFile(new URL('../'+rel,import.meta.url),'utf8');
|
|
6
|
+
|
|
7
|
+
test('R58 canonical Windows gate is final-candidate bound and leaves historical gates immutable',async()=>{
|
|
8
|
+
const [pkgText,gate,evidence,r57Gate,r57Evidence]=await Promise.all([
|
|
9
|
+
read('package.json'),read('scripts/gate-windows-r58.ps1'),read('scripts/windows-gate-evidence-r58.mjs'),
|
|
10
|
+
read('scripts/gate-windows-r57.ps1'),read('scripts/windows-gate-evidence-r57.mjs')
|
|
11
|
+
]);
|
|
12
|
+
const pkg=JSON.parse(pkgText);
|
|
13
|
+
assert.match(pkg.scripts['gate:windows'],/gate-windows-r58\.ps1$/);
|
|
14
|
+
assert.match(pkg.scripts['r58:focused:test'],/scripts\/r58-focused\.ps1$/);
|
|
15
|
+
assert.match(pkg.scripts['r58:cross-plane:test'],/scripts\/r58-cross-plane\.ps1$/);
|
|
16
|
+
assert.match(gate,/DEADBYTE 0\.15\.35 \/ R58 WINDOWS GATE: PASS/);
|
|
17
|
+
assert.match(gate,/r58-package-boundary/);
|
|
18
|
+
assert.match(gate,/r58-process-policy/);
|
|
19
|
+
assert.match(gate,/r58-focused-contracts/);
|
|
20
|
+
assert.match(gate,/full_regression\.status -ne 'PASS'/);
|
|
21
|
+
assert.match(evidence,/deadbyte\.r58-windows-gate\.v1/);
|
|
22
|
+
assert.match(evidence,/pkg\.version==='0\.15\.35'/);
|
|
23
|
+
assert.match(evidence,/release\.train==='R58'/);
|
|
24
|
+
assert.match(evidence,/release\.parent\?\.version==='0\.15\.34'/);
|
|
25
|
+
assert.match(evidence,/health\.assessment!==['"]FAIL['"]/);
|
|
26
|
+
assert.match(evidence,/\['ok','warn'\]\.includes\(health\.status\)/);
|
|
27
|
+
assert.match(evidence,/four_hour_soak_completed:false/);
|
|
28
|
+
assert.match(evidence,/clean_machine_completed:false/);
|
|
29
|
+
assert.match(r57Gate,/DEADBYTE 0\.15\.34 \/ R57 WINDOWS GATE: PASS/);
|
|
30
|
+
assert.match(r57Evidence,/deadbyte\.r57-windows-gate\.v1/);
|
|
31
|
+
});
|
|
@@ -35,7 +35,7 @@ test('R27 generation bundle rebinds authority and mutable state outside the rele
|
|
|
35
35
|
await writeFile(semanticConfigPath,semanticConfigBytes);
|
|
36
36
|
const currentConfig={AgentRuntimeEnabled:true,ProcessRuntimeEnabled:true,CodingRuntimeEnabled:true,MachineFsRuntimeEnabled:true,ObservationRuntimeEnabled:true,DesktopRuntimeEnabled:true,AutonomousRuntimeEnabled:true,AutonomousSupervisorEnabled:true,SemanticLspConfigFile:semanticConfigPath,SemanticLspConfigSha256:sha(semanticConfigBytes),
|
|
37
37
|
Supervisor:{FilePath:'C:/node.exe',FileSha256:EXE,Arguments:'src\\autonomous-supervisor-host.mjs',WorkingDirectory:'C:/release',ScriptPath:'C:/release/src/autonomous-supervisor-host.mjs',ScriptSha256:'2'.repeat(64)},
|
|
38
|
-
Bridge:{FilePath:'C:/node.exe',FileSha256:EXE,Arguments:'bridge.mjs',WorkingDirectory:'C:/bridge',ScriptPath:'C:/bridge/bridge.mjs',ScriptSha256:EXE},
|
|
38
|
+
Bridge:{FilePath:'C:/node.exe',FileSha256:EXE,Arguments:'bridge.mjs',WorkingDirectory:'C:/bridge',ScriptPath:'C:/bridge/bridge.mjs',ScriptSha256:EXE,Dependencies:[{Name:'bridge-validator',Path:'C:/bridge/public-tool-definitions.mjs',Sha256:EXE}]},
|
|
39
39
|
Cloud:{FilePath:'C:/deadbyte-tunnel-host.exe',FileSha256:EXE,Arguments:'"C:/cloudflared.exe" "C:/secret/tunnel-token.txt" "C:/bridge"',WorkingDirectory:'C:/release',Transport:'deadbyte.named-tunnel.v2',Dependencies:[
|
|
40
40
|
{Name:'cloudflared',Path:'C:/cloudflared.exe',Sha256:EXE},{Name:'tunnel-token',Path:'C:/secret/tunnel-token.txt',Sha256:EXE}
|
|
41
41
|
]},
|
|
@@ -70,6 +70,9 @@ test('R27 generation bundle rebinds authority and mutable state outside the rele
|
|
|
70
70
|
const build=args=>createGenerationBundle({...args,systemRoot});
|
|
71
71
|
const result=await build({stateRoot:state,releaseRoot:release,developmentRoot:development,workspaceRoot:workspace,releaseId:'v0.8.3-aaaaaaaaaaaaaaaa',currentConfig,codingPolicy:coding,autonomousPolicy:autonomous,hostPolicy:host,processPolicy,desktopPolicy});
|
|
72
72
|
const verified=await verifyGenerationBundle(result.generation_root,result.generation_manifest_sha256);
|
|
73
|
+
const bridgeConfig=await readFile(result.config_path,'utf8');
|
|
74
|
+
const bridgeBlock=bridgeConfig.slice(bridgeConfig.indexOf(' Bridge = @{'),bridgeConfig.indexOf(' Cloud = @{'));
|
|
75
|
+
assert.match(bridgeBlock,/Name = 'bridge-validator'; Path = 'C:\/bridge\/public-tool-definitions.mjs'; Sha256 = '1{64}'/,'generation must not discard explicitly pinned Bridge dependencies');
|
|
73
76
|
assert.equal(verified.files,7);
|
|
74
77
|
const generatedProcess=JSON.parse(await readFile(result.process_policy_path,'utf8'));
|
|
75
78
|
assert.equal(generatedProcess.grant_file,path.join(path.resolve(state),'runtime','process-grant.json'));
|
|
@@ -401,7 +401,60 @@ winTest('remote warning lines use deterministic ASCII fallback on piped Windows
|
|
|
401
401
|
}
|
|
402
402
|
});
|
|
403
403
|
|
|
404
|
-
winTest('
|
|
404
|
+
winTest('R58 concurrent cold remote claims have one winner and no loser cleanup',async()=>{
|
|
405
|
+
const fx=await fixture({statusOnline:true});const children=[],outputs=[];
|
|
406
|
+
try{
|
|
407
|
+
for(let i=0;i<3;i++){
|
|
408
|
+
const child=spawn(process.execPath,[cliPath,'remote'],{env:fx.env,stdio:['pipe','pipe','pipe'],windowsHide:true});
|
|
409
|
+
children.push(child);outputs.push(await collect(child));
|
|
410
|
+
}
|
|
411
|
+
const commands=await waitFor(fx.trace,items=>items.includes('hoststatus'));
|
|
412
|
+
assert.ok(commands.includes('hoststatus'),outputs.map(o=>o.stderr).join('\n'));
|
|
413
|
+
const deadline=Date.now()+5000;
|
|
414
|
+
while(Date.now()<deadline&&children.filter(c=>c.exitCode===null).length>1)await sleep(50);
|
|
415
|
+
const live=children.filter(c=>c.exitCode===null);
|
|
416
|
+
assert.equal(live.length,1,'exactly one verified foreground may survive');
|
|
417
|
+
const trace=await traceCommands(fx.trace);
|
|
418
|
+
for(const arm of ['armprocess','armcoding','armautonomous'])assert.equal(trace.filter(x=>x===arm).length,1);
|
|
419
|
+
assert.ok(!trace.some(x=>x.startsWith('disarm')||x==='park'),'losers must not revoke winner authority');
|
|
420
|
+
for(let i=0;i<children.length;i++)if(children[i]!==live[0])assert.match(outputs[i].stderr,/remote session already (claimed|active)/,'concurrent rejection must have deterministic ownership vocabulary');
|
|
421
|
+
const marker=JSON.parse(await readFile(path.join(fx.state,'runtime','remote-session.json'),'utf8'));
|
|
422
|
+
assert.equal(marker.owner_pid,live[0].pid);
|
|
423
|
+
live[0].stdin.end();assert.equal((await outputs[children.indexOf(live[0])].closed).code,0);
|
|
424
|
+
}finally{
|
|
425
|
+
for(const child of children)if(child.exitCode===null)child.kill();
|
|
426
|
+
await rm(fx.dir,{recursive:true,force:true});
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
winTest('R58 duplicate remote verifies the real live owner and leaves its authority untouched',async()=>{
|
|
431
|
+
const fx=await fixture({statusOnline:true});let first=null,second=null;
|
|
432
|
+
try{
|
|
433
|
+
first=spawn(process.execPath,[cliPath,'remote'],{env:fx.env,stdio:['pipe','pipe','pipe'],windowsHide:true});
|
|
434
|
+
const firstOutput=await collect(first);
|
|
435
|
+
const commands=await waitFor(fx.trace,items=>items.includes('hoststatus'));
|
|
436
|
+
assert.ok(commands.includes('hoststatus'),firstOutput.stderr);
|
|
437
|
+
const before=await traceCommands(fx.trace);
|
|
438
|
+
const markerPath=path.join(fx.state,'runtime','remote-session.json');
|
|
439
|
+
const marker=JSON.parse(await readFile(markerPath,'utf8'));
|
|
440
|
+
assert.equal(marker.owner_identity.pid,first.pid);
|
|
441
|
+
assert.equal(marker.guardian_identity.parent_pid,first.pid);
|
|
442
|
+
second=spawn(process.execPath,[cliPath,'remote'],{env:fx.env,stdio:['pipe','pipe','pipe'],windowsHide:true});
|
|
443
|
+
const secondOutput=await collect(second);
|
|
444
|
+
const stopped=await secondOutput.closed;
|
|
445
|
+
assert.notEqual(stopped.code,0);
|
|
446
|
+
assert.match(secondOutput.stderr,/remote session already active/);
|
|
447
|
+
assert.deepEqual(await traceCommands(fx.trace),before,'duplicate must not run any controller operation');
|
|
448
|
+
assert.deepEqual(JSON.parse(await readFile(markerPath,'utf8')),marker);
|
|
449
|
+
assert.equal(first.exitCode,null,'verified legitimate foreground must survive');
|
|
450
|
+
first.stdin.end();assert.equal((await firstOutput.closed).code,0,firstOutput.stderr);
|
|
451
|
+
}finally{
|
|
452
|
+
if(first?.exitCode===null)first.kill();if(second?.exitCode===null)second.kill();
|
|
453
|
+
await rm(fx.dir,{recursive:true,force:true});
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
winTest('bare remote refuses unverified legacy ownership without touching authority',async()=>{
|
|
405
458
|
const fx=await fixture();
|
|
406
459
|
let child=null;
|
|
407
460
|
try{
|
|
@@ -416,7 +469,7 @@ winTest('bare remote refuses a second live foreground session without touching a
|
|
|
416
469
|
assert.equal(ended.timeout,undefined);
|
|
417
470
|
assert.notEqual(ended.code,0);
|
|
418
471
|
assert.deepEqual(await traceCommands(fx.trace),[]);
|
|
419
|
-
assert.match(output.stderr,/remote session
|
|
472
|
+
assert.match(output.stderr,/remote session ownership unverified/i);
|
|
420
473
|
}finally{
|
|
421
474
|
if(child?.exitCode===null) child.kill();
|
|
422
475
|
await rm(fx.dir,{recursive:true,force:true});
|