deadbyte-mcp 0.15.74 → 0.15.75
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 +102 -42
- package/PROVENANCE.json +19 -21
- package/README.txt +23 -18
- package/RELEASE.json +51 -18
- package/SBOM.cdx.json +2 -2
- package/TEST-MATRIX.json +22 -2
- package/bin/deadbyte-desktop-uia-r199-fixed.exe +0 -0
- package/bin/deadbyte-desktop-uia-r199-fixed.obj +0 -0
- package/bin/deadbyte-desktop-uia-r199.exe +0 -0
- package/bin/deadbyte-desktop-uia-r199.obj +0 -0
- package/bin/deadbyte-fs-read-r197-fixed.exe +0 -0
- package/bin/deadbyte-fs-read-r198.exe +0 -0
- package/bin/deadbyte-fs-read-r198.obj +0 -0
- package/bin/deadbyte-root-relative-probe-r197.exe +0 -0
- package/bridge/package.json +1 -1
- package/controller/deadbyte-process-policy.json +4 -4
- package/docs/R184-package-lock.json +2 -8
- package/native/deadbyte-desktop-uia-r199.cpp +345 -0
- package/native/deadbyte-fs-read-r197.cpp +174 -0
- package/native/deadbyte-fs-read-r198.cpp +181 -0
- package/native/deadbyte-root-relative-probe-r197.cpp +97 -0
- package/package.json +4 -2
- package/scripts/build-native-desktop-uia-r199-fixed.ps1 +28 -0
- package/scripts/build-native-desktop-uia-r199.ps1 +28 -0
- package/scripts/build-native-fs-read-r197-fixed.ps1 +28 -0
- package/scripts/build-native-fs-read-r197.ps1 +28 -0
- package/scripts/build-native-fs-read-r198.ps1 +28 -0
- package/scripts/build-native-root-probe-r197.ps1 +28 -0
- package/scripts/gate-windows-r195.ps1 +114 -0
- package/scripts/gate-windows-r206.ps1 +116 -0
- package/scripts/init-desktop-policy-r199.ps1 +85 -0
- package/scripts/npm-release-r48.mjs +17 -0
- package/scripts/npm-release-readiness-r195.mjs +72 -0
- package/scripts/npm-release-readiness-r206.mjs +76 -0
- package/scripts/r193-owner-request-live.mjs +5 -6
- package/scripts/r193-owner-uac-broker.mjs +3 -3
- package/scripts/r205-candidate-selection.mjs +22 -0
- package/src/candidate-selection-evidence-r206.mjs +71 -0
- package/src/candidate-selection-r205.mjs +96 -0
- package/src/desktop-postcondition-r196.mjs +30 -0
- package/src/desktop-runtime-r40.mjs +44 -7
- package/src/desktop-uia-identity-r199.mjs +51 -0
- package/src/desktop-uia-r40.mjs +9 -1
- package/src/machine-fs-handle-read-r196.mjs +43 -0
- package/src/machine-fs-native-read-r197.mjs +73 -0
- package/src/machine-fs-native-read-r198.mjs +76 -0
- package/src/machine-fs-read-worker.mjs +5 -25
- package/src/machine-fs.mjs +31 -16
- package/src/r195-canonical-owner-observation.mjs +15 -0
- package/src/remote-destination-journal-r202.mjs +120 -0
- package/src/remote-device-r44.mjs +89 -14
- package/src/remote-payload-boundary-r201.mjs +71 -0
- package/src/remote-terminal-evidence-r200.mjs +60 -0
- package/src/version.mjs +1 -1
- package/test/desktop-runtime-r40.test.mjs +111 -0
- package/test/desktop-uia-r40.test.mjs +15 -0
- package/test/fixtures/r197-junction-swap-worker.mjs +21 -0
- package/test/fixtures/r203-journal-writer.mjs +6 -0
- package/test/process-policy-refresh.test.mjs +4 -4
- package/test/r130-release-security-contract.test.mjs +8 -8
- package/test/r132-release-identity.test.mjs +7 -7
- package/test/r153-prepublish-identity.test.mjs +1 -1
- package/test/r160-release-contract.test.mjs +3 -3
- package/test/r165-live-cli-skew.test.mjs +1 -1
- package/test/r167-release-contract.test.mjs +11 -11
- package/test/r168-release-contract.test.mjs +4 -4
- package/test/r172-release-contract.test.mjs +6 -6
- package/test/r173-release-contract.test.mjs +16 -16
- package/test/r179-release-contract.test.mjs +12 -12
- package/test/r190-release-contract.test.mjs +10 -10
- package/test/r191-release-contract.test.mjs +12 -12
- package/test/r192-release-identity.test.mjs +9 -9
- package/test/r193-release-identity.test.mjs +24 -22
- package/test/r193-release-readiness.test.mjs +2 -2
- package/test/r193-windows-gate.test.mjs +8 -8
- package/test/r194-release-identity.test.mjs +19 -15
- package/test/r194-release-readiness.test.mjs +2 -2
- package/test/r194-windows-gate.test.mjs +7 -6
- package/test/r195-canonical-owner-observation.test.mjs +32 -0
- package/test/r195-release-readiness.test.mjs +22 -0
- package/test/r196-desktop-postcondition.test.mjs +53 -0
- package/test/r196-handle-bound-read.test.mjs +99 -0
- package/test/r196-remote-outcome.test.mjs +94 -0
- package/test/r197-namespace-race.test.mjs +41 -0
- package/test/r197-native-dispatch-race.test.mjs +49 -0
- package/test/r197-native-file-read.test.mjs +60 -0
- package/test/r197-native-root-open.test.mjs +61 -0
- package/test/r198-root-identity.test.mjs +88 -0
- package/test/r199-native-uia-worker.test.mjs +28 -0
- package/test/r199-uia-identity.test.mjs +43 -0
- package/test/r200-remote-reconcile.test.mjs +106 -0
- package/test/r201-payload-boundary.test.mjs +78 -0
- package/test/r201-payload-fuzz.test.mjs +37 -0
- package/test/r201-tool-boundary-fuzz.test.mjs +120 -0
- package/test/r202-destination-journal.test.mjs +92 -0
- package/test/r203-atomic-journal.test.mjs +86 -0
- package/test/r205-candidate-selection-cli.test.mjs +31 -0
- package/test/r205-candidate-selection.test.mjs +83 -0
- package/test/r206-candidate-evidence.test.mjs +64 -0
- package/test/r206-release-contract.test.mjs +42 -0
- package/test/r206-release-readiness.test.mjs +23 -0
- package/test/r59-tool-surface-compat.test.mjs +1 -1
- package/test/r64-machine-fs-large-read.test.mjs +26 -12
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {generateKeyPairSync,createHash,sign} from 'node:crypto';
|
|
4
|
+
import {mkdtemp,readFile,rm,writeFile} from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import {openRemoteDestinationJournalR202} from '../src/remote-destination-journal-r202.mjs';
|
|
8
|
+
import {REMOTE_TERMINAL_DOMAIN,REMOTE_TERMINAL_EVIDENCE_SCHEMA} from '../src/remote-terminal-evidence-r200.mjs';
|
|
9
|
+
import {canonicalJson} from '../src/canonical-json.mjs';
|
|
10
|
+
import {createRemoteDevicePlaneR44} from '../src/remote-device-r44.mjs';
|
|
11
|
+
|
|
12
|
+
const sha=b=>createHash('sha256').update(b).digest('hex');
|
|
13
|
+
async function fixture(t){
|
|
14
|
+
const base=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r202-journal-'));
|
|
15
|
+
t.after(()=>rm(base,{recursive:true,force:true}));
|
|
16
|
+
const keys=generateKeyPairSync('ed25519');
|
|
17
|
+
const spki=Buffer.from(keys.publicKey.export({type:'spki',format:'der'}));
|
|
18
|
+
const device={device_id:sha(spki),epoch:1,public_key_spki_base64:spki.toString('base64')};
|
|
19
|
+
const dir=path.join(base,'terminal-journal');
|
|
20
|
+
const journal=await openRemoteDestinationJournalR202({directory:dir,device});
|
|
21
|
+
const request={device_id:device.device_id,epoch:1,request_id:'9'.repeat(64),
|
|
22
|
+
request_sha256:'a'.repeat(64),capability_id:'machine.fs.write'};
|
|
23
|
+
const envelope=(terminal,overrides={})=>{
|
|
24
|
+
const evidence={schema:REMOTE_TERMINAL_EVIDENCE_SCHEMA,...request,terminal,...overrides};
|
|
25
|
+
const sig=sign(null,Buffer.concat([REMOTE_TERMINAL_DOMAIN,Buffer.from(canonicalJson(evidence),'utf8')]),keys.privateKey).toString('base64');
|
|
26
|
+
return {evidence,attestation:{algorithm:'ed25519',key_id:device.device_id,signature_base64:sig}};
|
|
27
|
+
};
|
|
28
|
+
return {base,dir,journal,device,keys,request,envelope};
|
|
29
|
+
}
|
|
30
|
+
const terminal=(hex='b')=>({status:'completed',result_sha256:hex.repeat(64)});
|
|
31
|
+
test('R202 stores only paired signed evidence; lookup survives restart and is idempotent',async t=>{
|
|
32
|
+
const f=await fixture(t),proof=f.envelope(terminal());
|
|
33
|
+
const r=await f.journal.commitSignedTerminal({request:f.request,envelope:proof});
|
|
34
|
+
assert.equal(r.status,'stored');assert.match(r.terminal_evidence_sha256,/^[a-f0-9]{64}$/);
|
|
35
|
+
const r2=await f.journal.commitSignedTerminal({request:f.request,envelope:proof});
|
|
36
|
+
assert.equal(r2.status,'existing');
|
|
37
|
+
const restarted=await openRemoteDestinationJournalR202({directory:f.dir,device:f.device});
|
|
38
|
+
assert.deepEqual(await restarted.lookupTerminal(f.request),proof);
|
|
39
|
+
});
|
|
40
|
+
test('R202 a second signed but conflicting terminal cannot overwrite committed evidence',async t=>{
|
|
41
|
+
const f=await fixture(t),one=f.envelope(terminal('b')),two=f.envelope(terminal('c'));
|
|
42
|
+
await f.journal.commitSignedTerminal({request:f.request,envelope:one});
|
|
43
|
+
await assert.rejects(f.journal.commitSignedTerminal({request:f.request,envelope:two}),/conflicting signed terminal/);
|
|
44
|
+
assert.deepEqual(await f.journal.lookupTerminal(f.request),one);
|
|
45
|
+
});
|
|
46
|
+
test('R202 unsigned, wrong request, epoch and corrupted stored evidence fail closed',async t=>{
|
|
47
|
+
const f=await fixture(t),proof=f.envelope(terminal());
|
|
48
|
+
const altered=structuredClone(proof);altered.evidence.terminal.result_sha256='d'.repeat(64);
|
|
49
|
+
await assert.rejects(f.journal.commitSignedTerminal({request:f.request,envelope:altered}),/signature invalid/);
|
|
50
|
+
await assert.rejects(f.journal.commitSignedTerminal({request:{...f.request,epoch:2},envelope:proof}),/request identity invalid/);
|
|
51
|
+
await f.journal.commitSignedTerminal({request:f.request,envelope:proof});
|
|
52
|
+
const file=path.join(f.dir,f.request.request_id+'.json');
|
|
53
|
+
await writeFile(file,JSON.stringify(altered)+'\n');
|
|
54
|
+
await assert.rejects(f.journal.lookupTerminal(f.request),/signature invalid/);
|
|
55
|
+
});
|
|
56
|
+
test('R202 missing or partially written journal remains UNKNOWN and denies bad selectors',async t=>{
|
|
57
|
+
const f=await fixture(t);
|
|
58
|
+
assert.equal(await f.journal.lookupTerminal(f.request),null);
|
|
59
|
+
await assert.rejects(f.journal.lookupTerminal({...f.request,request_id:'../evil'}),/request identity invalid/);
|
|
60
|
+
await assert.rejects(f.journal.lookupTerminal({...f.request,request_sha256:'f'.repeat(64),device_id:'b'.repeat(64)}),/lookup request identity invalid/);
|
|
61
|
+
const file=path.join(f.dir,f.request.request_id+'.json');
|
|
62
|
+
await writeFile(file,'');
|
|
63
|
+
await assert.rejects(f.journal.lookupTerminal(f.request),/record is not a bounded regular file/);
|
|
64
|
+
});
|
|
65
|
+
test('R202 local relay adapter recovers lost ACK with exactly one remote dispatch',async t=>{
|
|
66
|
+
const f=await fixture(t);
|
|
67
|
+
let calls=0;
|
|
68
|
+
const relay={
|
|
69
|
+
async probe(x){return {device_id:x.device_id,epoch:x.epoch,nonce:x.nonce,ready:true};},
|
|
70
|
+
async request(){calls++;return null;},
|
|
71
|
+
lookupTerminal:args=>f.journal.lookupTerminal(args)
|
|
72
|
+
};
|
|
73
|
+
const stateDir=path.join(f.base,'sender');
|
|
74
|
+
const plane=await createRemoteDevicePlaneR44({stateDir,relay,authorizeCapability:async()=>true,
|
|
75
|
+
relayTimeoutMs:1000,probeRetries:0,reachableTtlMs:10000});
|
|
76
|
+
await plane.pair({publicKeySpkiBase64:f.device.public_key_spki_base64,displayName:'R202 simulated destination'});
|
|
77
|
+
await plane.reportOnline({deviceId:f.device.device_id,epoch:1,connectionId:'R202 local transport'});
|
|
78
|
+
await plane.probe({deviceId:f.device.device_id,epoch:1});
|
|
79
|
+
const args={deviceId:f.device.device_id,epoch:1,capabilityId:f.request.capability_id,
|
|
80
|
+
payload:{path:'readme.txt',content:'one'},requestId:f.request.request_id};
|
|
81
|
+
const first=await plane.dispatch(args);
|
|
82
|
+
assert.equal(first.effect_state,'unknown');
|
|
83
|
+
assert.equal(calls,1);
|
|
84
|
+
const proof=f.envelope(terminal(),{request_sha256:first.request_sha256});
|
|
85
|
+
await f.journal.commitSignedTerminal({request:{...f.request,request_sha256:first.request_sha256},envelope:proof});
|
|
86
|
+
const next=await plane.reconcile({requestId:args.requestId});
|
|
87
|
+
assert.equal(next.effect_state,'confirmed');assert.equal(next.state,'completed');
|
|
88
|
+
assert.equal(next.reconciliation_state,'signed_terminal');
|
|
89
|
+
assert.equal(calls,1,'lookup retried the mutation');
|
|
90
|
+
const replay=await plane.dispatch(args);
|
|
91
|
+
assert.equal(replay.replay,true);assert.equal(calls,1);
|
|
92
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {generateKeyPairSync,createHash,sign} from 'node:crypto';
|
|
4
|
+
import {mkdtemp,mkdir,readFile,readdir,rm,writeFile,stat} from 'node:fs/promises';
|
|
5
|
+
import {spawn} from 'node:child_process';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import {fileURLToPath} from 'node:url';
|
|
9
|
+
import {openRemoteDestinationJournalR202} from '../src/remote-destination-journal-r202.mjs';
|
|
10
|
+
import {REMOTE_TERMINAL_DOMAIN,REMOTE_TERMINAL_EVIDENCE_SCHEMA} from '../src/remote-terminal-evidence-r200.mjs';
|
|
11
|
+
import {canonicalJson} from '../src/canonical-json.mjs';
|
|
12
|
+
const sha=b=>createHash('sha256').update(b).digest('hex');
|
|
13
|
+
const worker=fileURLToPath(new URL('./fixtures/r203-journal-writer.mjs',import.meta.url));
|
|
14
|
+
async function fixture(t){
|
|
15
|
+
const dir=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r203-journal-'));
|
|
16
|
+
t.after(()=>rm(dir,{recursive:true,force:true}));
|
|
17
|
+
const key=generateKeyPairSync('ed25519'),spki=Buffer.from(key.publicKey.export({format:'der',type:'spki'}));
|
|
18
|
+
const device={device_id:sha(spki),epoch:1,public_key_spki_base64:spki.toString('base64')};
|
|
19
|
+
const journalDir=path.join(dir,'journal');
|
|
20
|
+
const journal=await openRemoteDestinationJournalR202({directory:journalDir,device});
|
|
21
|
+
const request={device_id:device.device_id,epoch:1,request_id:'a'.repeat(64),request_sha256:'b'.repeat(64),capability_id:'machine.fs.write'};
|
|
22
|
+
const signEvidence=terminal=>{
|
|
23
|
+
const evidence={schema:REMOTE_TERMINAL_EVIDENCE_SCHEMA,...request,terminal};
|
|
24
|
+
const signature_base64=sign(null,Buffer.concat([REMOTE_TERMINAL_DOMAIN,Buffer.from(canonicalJson(evidence))]),key.privateKey).toString('base64');
|
|
25
|
+
return {evidence,attestation:{algorithm:'ed25519',key_id:device.device_id,signature_base64}};
|
|
26
|
+
};
|
|
27
|
+
return {dir,journalDir,journal,device,request,signEvidence};
|
|
28
|
+
}
|
|
29
|
+
const completed={status:'completed',result_sha256:'c'.repeat(64)};
|
|
30
|
+
test('R203 orphan unpublished temp never poisons absent destination terminal',async t=>{
|
|
31
|
+
const f=await fixture(t);
|
|
32
|
+
const garbage=path.join(f.journalDir,'.'+f.request.request_id+'.1234567890abcdef12345678.tmp');
|
|
33
|
+
await writeFile(garbage,'{broken');
|
|
34
|
+
assert.equal(await f.journal.lookupTerminal(f.request),null);
|
|
35
|
+
const record=f.signEvidence(completed);
|
|
36
|
+
assert.equal((await f.journal.commitSignedTerminal({request:f.request,envelope:record})).status,'stored');
|
|
37
|
+
assert.deepEqual(await f.journal.lookupTerminal(f.request),record);
|
|
38
|
+
assert.equal((await readFile(garbage,'utf8')),'{broken','the orphan is left to explicit cleanup, not silently deleted');
|
|
39
|
+
});
|
|
40
|
+
test('R203 8 independent Windows processes race for the same terminal; exactly one publication',async t=>{
|
|
41
|
+
const f=await fixture(t),envelope=f.signEvidence(completed);
|
|
42
|
+
const fixturePath=path.join(f.dir,'case.json');
|
|
43
|
+
await writeFile(fixturePath,JSON.stringify({directory:f.journalDir,device:f.device,request:f.request,envelope}));
|
|
44
|
+
const spawnWorker=()=>new Promise((resolve,reject)=>{
|
|
45
|
+
const child=spawn(process.execPath,[worker,fixturePath],{shell:false,windowsHide:true,stdio:['ignore','pipe','pipe']});
|
|
46
|
+
let stdout='',stderr='';
|
|
47
|
+
child.stdout.setEncoding('utf8');child.stderr.setEncoding('utf8');
|
|
48
|
+
child.stdout.on('data',s=>stdout+=s);child.stderr.on('data',s=>stderr+=s);
|
|
49
|
+
child.once('error',reject);child.once('close',code=>code===0?resolve(JSON.parse(stdout.trim())):reject(Error('worker exit '+code+' '+stderr)));
|
|
50
|
+
});
|
|
51
|
+
const results=await Promise.all(Array.from({length:8},spawnWorker));
|
|
52
|
+
assert.equal(results.filter(r=>r.status==='stored').length,1);
|
|
53
|
+
assert.equal(results.filter(r=>r.status==='existing').length,7);
|
|
54
|
+
assert.deepEqual(await f.journal.lookupTerminal(f.request),envelope);
|
|
55
|
+
const children=(await readdir(f.journalDir)).filter(x=>x.endsWith('.tmp'));
|
|
56
|
+
assert.deepEqual(children,[],'all successful writers removed private temp files');
|
|
57
|
+
});
|
|
58
|
+
test('R203 competing distinct signed terminal results cannot overwrite a winner',async t=>{
|
|
59
|
+
const f=await fixture(t);
|
|
60
|
+
const first=f.signEvidence(completed);
|
|
61
|
+
const second=f.signEvidence({status:'completed',result_sha256:'d'.repeat(64)});
|
|
62
|
+
const results=await Promise.allSettled([f.journal.commitSignedTerminal({request:f.request,envelope:first}),
|
|
63
|
+
f.journal.commitSignedTerminal({request:f.request,envelope:second})]);
|
|
64
|
+
assert.equal(results.filter(r=>r.status==='fulfilled').length,1);
|
|
65
|
+
assert.equal(results.filter(r=>r.status==='rejected'&&/conflicting signed terminal/.test(r.reason?.message)).length,1);
|
|
66
|
+
const actual=await f.journal.lookupTerminal(f.request);
|
|
67
|
+
assert.ok([first,second].some(p=>p.evidence.terminal.result_sha256===actual.evidence.terminal.result_sha256));
|
|
68
|
+
});
|
|
69
|
+
test('R203 preexisting directory or oversized forged final record is never overwritten',async t=>{
|
|
70
|
+
const f=await fixture(t),file=path.join(f.journalDir,f.request.request_id+'.json');
|
|
71
|
+
await mkdir(file);
|
|
72
|
+
await assert.rejects(f.journal.commitSignedTerminal({request:f.request,envelope:f.signEvidence(completed)}),/bounded regular file/);
|
|
73
|
+
await rm(file,{recursive:true});
|
|
74
|
+
await writeFile(file,' '.repeat(17000));
|
|
75
|
+
await assert.rejects(f.journal.lookupTerminal(f.request),/bounded regular file/);
|
|
76
|
+
});
|
|
77
|
+
test('R203 signed record read uses bounded handle and detects invalid mutable payload',async t=>{
|
|
78
|
+
const f=await fixture(t),envelope=f.signEvidence(completed);
|
|
79
|
+
await f.journal.commitSignedTerminal({request:f.request,envelope});
|
|
80
|
+
const file=path.join(f.journalDir,f.request.request_id+'.json');
|
|
81
|
+
const initial=await stat(file);
|
|
82
|
+
assert.ok(initial.size>64);
|
|
83
|
+
const bad={...envelope,evidence:{...envelope.evidence,terminal:{...completed,result_sha256:'d'.repeat(64)}}};
|
|
84
|
+
await writeFile(file,JSON.stringify(bad));
|
|
85
|
+
await assert.rejects(f.journal.lookupTerminal(f.request),/signature invalid/);
|
|
86
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const cli=fileURLToPath(new URL('../scripts/r205-candidate-selection.mjs',import.meta.url));
|
|
7
|
+
const H=c=>c.repeat(64);
|
|
8
|
+
const fixture=H('a'),env=H('b');
|
|
9
|
+
function input(status='PASS'){
|
|
10
|
+
const artifact=H('4');
|
|
11
|
+
const correct=(h,s)=>({status:s,artifact_sha256:h,fixture_sha256:fixture,receipt_sha256:H('2')});
|
|
12
|
+
const benchmark=(h,samples)=>({artifact_sha256:h,fixture_sha256:fixture,environment_sha256:env,receipt_sha256:H('3'),samples_ms:samples});
|
|
13
|
+
return {schema:'deadbyte.candidate-selection.r205.v1',fixture_sha256:fixture,environment_sha256:env,
|
|
14
|
+
baseline:{artifact_sha256:H('1'),correctness:correct(H('1'),'PASS'),benchmark:benchmark(H('1'),Array(7).fill(100))},
|
|
15
|
+
candidates:[{candidate_id:'candidate',artifact_sha256:artifact,correctness:correct(artifact,status),...(status==='PASS'?{benchmark:benchmark(artifact,Array(7).fill(75))}:{})}]};
|
|
16
|
+
}
|
|
17
|
+
const run=stdin=>spawnSync(process.execPath,[cli],{input:stdin,encoding:'utf8',windowsHide:true,timeout:10000,stdio:['pipe','pipe','pipe']});
|
|
18
|
+
test('R205 CLI returns provisional evidence only, no release authorization',()=>{
|
|
19
|
+
const r=run(JSON.stringify(input()));assert.equal(r.status,0,r.stderr);
|
|
20
|
+
const j=JSON.parse(r.stdout);assert.equal(j.state,'PROVISIONAL_CANDIDATE');assert.equal(j.release_authorized,false);assert.equal(j.evidence_verified,false);
|
|
21
|
+
});
|
|
22
|
+
test('R205 CLI exits 2 for ungraded result without turning uncertainty into failure score',()=>{
|
|
23
|
+
const r=run(JSON.stringify(input('UNKNOWN')));assert.equal(r.status,2,r.stderr);
|
|
24
|
+
const j=JSON.parse(r.stdout);assert.equal(j.candidates[0].verdict,'UNGRADED');assert.equal(j.state,'NO_QUALIFIED_CANDIDATE');
|
|
25
|
+
});
|
|
26
|
+
test('R205 CLI refuses malformed JSON with exit 3',()=>{
|
|
27
|
+
const r=run('{invalid');assert.equal(r.status,3);assert.match(r.stderr,/R205_SELECTION_REFUSED/);
|
|
28
|
+
});
|
|
29
|
+
test('R205 CLI refuses oversized stdin before evaluation',()=>{
|
|
30
|
+
const r=run('x'.repeat(65537));assert.equal(r.status,3);assert.match(r.stderr,/R205_INPUT_LIMIT_EXCEEDED/);
|
|
31
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {CANDIDATE_SELECTION_SCHEMA,buildBlindCandidateViewR205,evaluateCandidateSelectionR205} from '../src/candidate-selection-r205.mjs';
|
|
4
|
+
const H=c=>c.repeat(64);
|
|
5
|
+
const fixture=H('a'),environment=H('b');
|
|
6
|
+
const evidence=(status,artifact,tag)=>({status,artifact_sha256:artifact,fixture_sha256:fixture,receipt_sha256:H(tag)});
|
|
7
|
+
const bench=(artifact,times,tag,env=environment)=>({artifact_sha256:artifact,fixture_sha256:fixture,environment_sha256:env,receipt_sha256:H(tag),samples_ms:times});
|
|
8
|
+
const candidate=(id,artifact,status,times,tag='5')=>({candidate_id:id,artifact_sha256:artifact,correctness:evidence(status,artifact,tag),...(times?{benchmark:bench(artifact,times,'6')}:{})});
|
|
9
|
+
const input=()=>({schema:CANDIDATE_SELECTION_SCHEMA,fixture_sha256:fixture,environment_sha256:environment,
|
|
10
|
+
baseline:{artifact_sha256:H('1'),correctness:evidence('PASS',H('1'),'2'),benchmark:bench(H('1'),[100,100,100,100,100,100,100],'3')},
|
|
11
|
+
candidates:[candidate('fast',H('4'),'PASS',[80,81,79,80,81,79,80]) ]});
|
|
12
|
+
test('R205 fast and correct candidate is provisional, never a release authorization',()=>{
|
|
13
|
+
const r=evaluateCandidateSelectionR205(input());
|
|
14
|
+
assert.equal(r.state,'PROVISIONAL_CANDIDATE');assert.equal(r.winner_candidate_id,'fast');
|
|
15
|
+
assert.equal(r.evidence_verified,false);assert.equal(r.release_authorized,false);assert.match(r.report_sha256,/^[0-9a-f]{64}$/);
|
|
16
|
+
});
|
|
17
|
+
test('R205 faster incorrect candidate is rejected even if its benchmark looks good',()=>{
|
|
18
|
+
const i=input();i.candidates.push(candidate('bad',H('7'),'FAIL',[1,1,1,1,1,1,1]));
|
|
19
|
+
const r=evaluateCandidateSelectionR205(i);
|
|
20
|
+
assert.equal(r.candidates.find(x=>x.candidate_id==='bad').verdict,'REJECT_CORRECTNESS');
|
|
21
|
+
assert.equal(r.winner_candidate_id,'fast');
|
|
22
|
+
});
|
|
23
|
+
test('R205 unknown grader verdict stays UNGRADED not score zero',()=>{
|
|
24
|
+
const i=input();i.candidates=[candidate('unknown',H('7'),'UNKNOWN',null)];
|
|
25
|
+
const r=evaluateCandidateSelectionR205(i);
|
|
26
|
+
assert.equal(r.state,'NO_QUALIFIED_CANDIDATE');assert.equal(r.candidates[0].verdict,'UNGRADED');
|
|
27
|
+
});
|
|
28
|
+
test('R205 invalid and untested grader status stay UNGRADED',()=>{
|
|
29
|
+
for(const state of ['INVALID','UNTESTED']){
|
|
30
|
+
const i=input();i.candidates=[candidate(state,H('7'),state,null)];
|
|
31
|
+
assert.equal(evaluateCandidateSelectionR205(i).candidates[0].verdict,'UNGRADED');
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
test('R205 unstable or noisy benchmark cannot win',()=>{
|
|
35
|
+
const i=input();i.candidates=[candidate('noisy',H('7'),'PASS',[75,75,75,75,150,150,150])];
|
|
36
|
+
assert.equal(evaluateCandidateSelectionR205(i).state,'NO_QUALIFIED_CANDIDATE');
|
|
37
|
+
});
|
|
38
|
+
test('R205 no meaningful speedup cannot win',()=>{
|
|
39
|
+
const i=input();i.candidates=[candidate('tiny',H('7'),'PASS',[98,98,98,98,98,98,98])];
|
|
40
|
+
assert.equal(evaluateCandidateSelectionR205(i).state,'NO_QUALIFIED_CANDIDATE');
|
|
41
|
+
});
|
|
42
|
+
test('R205 close candidates produce AMBIGUOUS, never arbitrary promotion',()=>{
|
|
43
|
+
const i=input();i.candidates.push(candidate('near',H('7'),'PASS',[81,81,81,81,81,81,81]));
|
|
44
|
+
const r=evaluateCandidateSelectionR205(i);
|
|
45
|
+
assert.equal(r.state,'AMBIGUOUS');assert.equal(r.winner_candidate_id,null);
|
|
46
|
+
});
|
|
47
|
+
test('R205 candidate ordering and names do not alter blind evaluation hash',()=>{
|
|
48
|
+
const i=input();i.candidates.push(candidate('other',H('7'),'FAIL',null));
|
|
49
|
+
const hash1=evaluateCandidateSelectionR205(i).blind_evaluation_sha256;
|
|
50
|
+
i.candidates.reverse();i.candidates[0].candidate_id='new_other';i.candidates[1].candidate_id='new_fast';
|
|
51
|
+
assert.equal(evaluateCandidateSelectionR205(i).blind_evaluation_sha256,hash1);
|
|
52
|
+
assert.equal(JSON.stringify(buildBlindCandidateViewR205(i)).includes('new_fast'),false);
|
|
53
|
+
});
|
|
54
|
+
test('R205 disallows proposer identity and hidden metadata from evaluation',()=>{
|
|
55
|
+
const i=input();i.candidates[0].proposer='model-b';
|
|
56
|
+
assert.throws(()=>evaluateCandidateSelectionR205(i),/unexpected field proposer/);
|
|
57
|
+
});
|
|
58
|
+
test('R205 refuses forged correctness artifact identity',()=>{
|
|
59
|
+
const i=input();i.candidates[0].correctness.artifact_sha256=H('f');
|
|
60
|
+
assert.throws(()=>evaluateCandidateSelectionR205(i),/identity mismatch/);
|
|
61
|
+
});
|
|
62
|
+
test('R205 refuses benchmark environment drift',()=>{
|
|
63
|
+
const i=input();i.candidates[0].benchmark.environment_sha256=H('f');
|
|
64
|
+
assert.throws(()=>evaluateCandidateSelectionR205(i),/identity mismatch/);
|
|
65
|
+
});
|
|
66
|
+
test('R205 refuses duplicate candidate artifact or baseline artifact',()=>{
|
|
67
|
+
const i=input();i.candidates.push(candidate('copy',H('4'),'PASS',[90,90,90,90,90,90,90]));
|
|
68
|
+
assert.throws(()=>evaluateCandidateSelectionR205(i),/duplicated/);
|
|
69
|
+
i.candidates=[candidate('base',H('1'),'PASS',[70,70,70,70,70,70,70])];
|
|
70
|
+
assert.throws(()=>evaluateCandidateSelectionR205(i),/duplicated/);
|
|
71
|
+
});
|
|
72
|
+
test('R205 refuses missing/insufficient samples rather than accepting ungraded benchmark',()=>{
|
|
73
|
+
const i=input();i.candidates[0].benchmark.samples_ms=[80,80];
|
|
74
|
+
assert.throws(()=>evaluateCandidateSelectionR205(i),/samples invalid/);
|
|
75
|
+
i.candidates=[candidate('no',H('5'),'PASS',null)];
|
|
76
|
+
assert.throws(()=>evaluateCandidateSelectionR205(i),/benchmark missing/);
|
|
77
|
+
});
|
|
78
|
+
test('R205 refuses mismatched paired sample counts and malformed values',()=>{
|
|
79
|
+
const i=input();i.candidates[0].benchmark.samples_ms=[80,80,80,80,80,80,80,80];
|
|
80
|
+
assert.throws(()=>evaluateCandidateSelectionR205(i),/length mismatch/);
|
|
81
|
+
i.candidates[0].benchmark.samples_ms=[80,80,80,80,80,80,NaN];
|
|
82
|
+
assert.throws(()=>evaluateCandidateSelectionR205(i),/samples invalid/);
|
|
83
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { canonicalJson } from '../src/canonical-json.mjs';
|
|
5
|
+
import { verifyCandidateSelectionEvidenceR206 } from '../src/candidate-selection-evidence-r206.mjs';
|
|
6
|
+
|
|
7
|
+
const H=c=>c.repeat(64);
|
|
8
|
+
const fixture=H('a'),environment=H('b');
|
|
9
|
+
const sha=value=>createHash('sha256').update(Buffer.from(canonicalJson(value),'utf8')).digest('hex');
|
|
10
|
+
const samples={baseline:[100,100,100,100,100,100,100],candidate:[75,75,75,75,75,75,75]};
|
|
11
|
+
const input=()=>({
|
|
12
|
+
schema:'deadbyte.candidate-selection.r205.v1',fixture_sha256:fixture,environment_sha256:environment,
|
|
13
|
+
baseline:{artifact_sha256:H('1'),correctness:{status:'PASS',artifact_sha256:H('1'),fixture_sha256:fixture,receipt_sha256:H('2')},
|
|
14
|
+
benchmark:{artifact_sha256:H('1'),fixture_sha256:fixture,environment_sha256:environment,receipt_sha256:H('3'),samples_ms:samples.baseline}},
|
|
15
|
+
candidates:[{candidate_id:'candidate',artifact_sha256:H('4'),
|
|
16
|
+
correctness:{status:'PASS',artifact_sha256:H('4'),fixture_sha256:fixture,receipt_sha256:H('5')},
|
|
17
|
+
benchmark:{artifact_sha256:H('4'),fixture_sha256:fixture,environment_sha256:environment,receipt_sha256:H('6'),samples_ms:samples.candidate}}]
|
|
18
|
+
});
|
|
19
|
+
const receipts=()=>new Map([
|
|
20
|
+
[H('2'),{verified:true,receipt_sha256:H('2'),evidence_kind:'correctness',signer_role:'independent_correctness_oracle',artifact_sha256:H('1'),fixture_sha256:fixture,status:'PASS'}],
|
|
21
|
+
[H('3'),{verified:true,receipt_sha256:H('3'),evidence_kind:'benchmark',signer_role:'independent_benchmark_runner',artifact_sha256:H('1'),fixture_sha256:fixture,environment_sha256:environment,samples_sha256:sha(samples.baseline)}],
|
|
22
|
+
[H('5'),{verified:true,receipt_sha256:H('5'),evidence_kind:'correctness',signer_role:'independent_correctness_oracle',artifact_sha256:H('4'),fixture_sha256:fixture,status:'PASS'}],
|
|
23
|
+
[H('6'),{verified:true,receipt_sha256:H('6'),evidence_kind:'benchmark',signer_role:'independent_benchmark_runner',artifact_sha256:H('4'),fixture_sha256:fixture,environment_sha256:environment,samples_sha256:sha(samples.candidate)}]
|
|
24
|
+
]);
|
|
25
|
+
const verifier=map=>async receiptSha=>map.get(receiptSha)??{verified:false,receipt_sha256:receiptSha};
|
|
26
|
+
|
|
27
|
+
test('R206 independently verifies every decision-bearing receipt but never authorizes release',async()=>{
|
|
28
|
+
const result=await verifyCandidateSelectionEvidenceR206(input(),{verifyReceipt:verifier(receipts())});
|
|
29
|
+
assert.equal(result.state,'PROVISIONAL_CANDIDATE');
|
|
30
|
+
assert.equal(result.evidence_verified,true);
|
|
31
|
+
assert.equal(result.verified_receipt_count,4);
|
|
32
|
+
assert.equal(result.release_authorized,false);
|
|
33
|
+
assert.match(result.evidence_binding_sha256,/^[0-9a-f]{64}$/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('R206 rejects a forged or unverifiable receipt',async()=>{
|
|
37
|
+
const map=receipts();map.set(H('5'),{...map.get(H('5')),verified:false});
|
|
38
|
+
await assert.rejects(()=>verifyCandidateSelectionEvidenceR206(input(),{verifyReceipt:verifier(map)}),/receipt verification failed/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('R206 rejects correctness status or artifact drift after grading',async()=>{
|
|
42
|
+
for(const change of [{status:'FAIL'},{artifact_sha256:H('7')}]){
|
|
43
|
+
const map=receipts();map.set(H('5'),{...map.get(H('5')),...change});
|
|
44
|
+
await assert.rejects(()=>verifyCandidateSelectionEvidenceR206(input(),{verifyReceipt:verifier(map)}),/correctness receipt binding mismatch/);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('R206 rejects benchmark sample or environment drift',async()=>{
|
|
49
|
+
for(const change of [{samples_sha256:H('7')},{environment_sha256:H('7')}]){
|
|
50
|
+
const map=receipts();map.set(H('6'),{...map.get(H('6')),...change});
|
|
51
|
+
await assert.rejects(()=>verifyCandidateSelectionEvidenceR206(input(),{verifyReceipt:verifier(map)}),/benchmark receipt binding mismatch/);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('R206 rejects proposer-controlled signer roles and receipt reuse',async()=>{
|
|
56
|
+
const proposer=receipts();proposer.set(H('5'),{...proposer.get(H('5')),signer_role:'candidate_proposer'});
|
|
57
|
+
await assert.rejects(()=>verifyCandidateSelectionEvidenceR206(input(),{verifyReceipt:verifier(proposer)}),/correctness receipt binding mismatch/);
|
|
58
|
+
const reused=input();reused.candidates[0].correctness.receipt_sha256=H('2');
|
|
59
|
+
await assert.rejects(()=>verifyCandidateSelectionEvidenceR206(reused,{verifyReceipt:verifier(receipts())}),/receipt reused/);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('R206 refuses a missing verifier instead of trusting hash-shaped claims',async()=>{
|
|
63
|
+
await assert.rejects(()=>verifyCandidateSelectionEvidenceR206(input(),{}),/independent receipt verifier required/);
|
|
64
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
const root=path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
8
|
+
const json=async name=>JSON.parse(await readFile(path.join(root,name),'utf8'));
|
|
9
|
+
|
|
10
|
+
test('R206 identity binds R205 source separately from active 0.15.74 runtime',async()=>{
|
|
11
|
+
const [pkg,lock,bridge,release,provenance,matrix,sbom]=await Promise.all([
|
|
12
|
+
json('package.json'),json('docs/R184-package-lock.json'),json('bridge/package.json'),json('RELEASE.json'),
|
|
13
|
+
json('PROVENANCE.json'),json('TEST-MATRIX.json'),json('SBOM.cdx.json')
|
|
14
|
+
]);
|
|
15
|
+
assert.equal(pkg.version,'0.15.75');
|
|
16
|
+
assert.equal(lock.version,'0.15.75');
|
|
17
|
+
assert.equal(lock.packages[''].version,'0.15.75');
|
|
18
|
+
assert.equal(bridge.version,'0.15.75');
|
|
19
|
+
assert.equal(release.version,'0.15.75');assert.equal(release.train,'R206');
|
|
20
|
+
assert.equal(release.source_predecessor.train,'R205');
|
|
21
|
+
assert.equal(release.source_predecessor.manifest_sha256,'166eebde18d53a9ec46f738383c289733f9cd37918b3572b7515d1ab9b122c92');
|
|
22
|
+
assert.equal(release.runtime_predecessor.release_id,'v0.15.74-e84bf4eed16e9443');
|
|
23
|
+
assert.equal(provenance.builder,'deadbyte-r206-release-closure');
|
|
24
|
+
assert.equal(provenance.source_predecessor_train,'R205');
|
|
25
|
+
assert.equal(provenance.source_predecessor_manifest_sha256,'166eebde18d53a9ec46f738383c289733f9cd37918b3572b7515d1ab9b122c92');
|
|
26
|
+
assert.equal(matrix.train,'R206');
|
|
27
|
+
assert.equal(sbom.serialNumber,'urn:cdx:deadbyte-mcp:r206:0.15.75');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('R206 release contract keeps public surface stable and deferred work truthful',async()=>{
|
|
31
|
+
const release=await json('RELEASE.json');
|
|
32
|
+
assert.equal(release.capabilities.compact_tool_count,43);
|
|
33
|
+
assert.equal(release.capabilities.full_tool_count,117);
|
|
34
|
+
assert.equal(release.r206.public_tool_delta,0);
|
|
35
|
+
assert.equal(release.r206.authority_scope_delta,0);
|
|
36
|
+
assert.equal(release.r206.selector_release_authority,false);
|
|
37
|
+
assert.deepEqual(release.r206.deferred_unverified,[
|
|
38
|
+
'REMOTE_TWO_MACHINE_E2E','NATIVE_UIA_APPLICATION_EFFECT','BOUNDED_CDP_HARNESS2','REA_WINDOWS_209_TOOL_PARITY'
|
|
39
|
+
]);
|
|
40
|
+
assert.equal(release.r206.npm_publish,'NOT_RUN');
|
|
41
|
+
assert.equal(release.r206.production_activation,'NOT_RUN');
|
|
42
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {assertR206NpmPublishReady,assertR206PackedIdentity} from '../scripts/npm-release-readiness-r206.mjs';
|
|
4
|
+
|
|
5
|
+
const release={
|
|
6
|
+
version:'0.15.75',train:'R206',
|
|
7
|
+
parent:{version:'0.15.74',release_id:'v0.15.74-e84bf4eed16e9443',
|
|
8
|
+
manifest_sha256:'e84bf4eed16e9443fbd0bf9f0f562e26b01db778647aefcc8b12fd7f48636b47',
|
|
9
|
+
generation_manifest_sha256:'598f896be9ce65e6b0c618e8c8f6eaa2a9d91617a76a146bf3deb385a56d19e7'},
|
|
10
|
+
source_predecessor:{train:'R205',manifest_sha256:'166eebde18d53a9ec46f738383c289733f9cd37918b3572b7515d1ab9b122c92'},
|
|
11
|
+
r206:{scope:'EVIDENCE_BOUND_CANDIDATE_SELECTION_RELEASE_CLOSURE',public_tool_delta:0,authority_scope_delta:0,selector_release_authority:false}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
test('R206 publication refuses an absent exact-byte closure before pack or registry effects',async()=>{
|
|
15
|
+
await assert.rejects(assertR206NpmPublishReady(release,{root:'D:\\candidate'}),/PUBLISH_BLOCKED R206.*closure root absent/);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('R206 packed identity refuses any artifact differing from the protected closure',()=>{
|
|
19
|
+
assert.throws(()=>assertR206PackedIdentity({
|
|
20
|
+
tarball_sha256:'a'.repeat(64),tarball_integrity:'sha512-expected',tarball_shasum:'b'.repeat(40),
|
|
21
|
+
observedSha256:'c'.repeat(64),observedIntegrity:'sha512-other',observedShasum:'d'.repeat(40)
|
|
22
|
+
}),/PUBLISH_BLOCKED R206.*repacked tarball/);
|
|
23
|
+
});
|
|
@@ -83,7 +83,7 @@ test('R72 compact baseline permits only R64 read offset and authorized coding-on
|
|
|
83
83
|
assert.deepEqual(diff.added,[]);
|
|
84
84
|
assert.deepEqual(diff.removed,[]);
|
|
85
85
|
const packageVersion=JSON.parse(await readFile(path.join(repoRoot,'package.json'),'utf8')).version;
|
|
86
|
-
if(['0.15.41','0.15.42','0.15.43','0.15.44','0.15.45','0.15.46','0.15.47','0.15.48','0.15.49','0.15.50','0.15.51','0.15.52','0.15.53','0.15.54','0.15.55','0.15.56','0.15.57','0.15.58','0.15.59','0.15.60','0.15.61','0.15.62','0.15.63','0.15.64','0.15.65','0.15.66','0.15.67','0.15.68','0.15.69','0.15.70','0.15.71','0.15.72','0.15.73','0.15.74'].includes(packageVersion)){
|
|
86
|
+
if(['0.15.41','0.15.42','0.15.43','0.15.44','0.15.45','0.15.46','0.15.47','0.15.48','0.15.49','0.15.50','0.15.51','0.15.52','0.15.53','0.15.54','0.15.55','0.15.56','0.15.57','0.15.58','0.15.59','0.15.60','0.15.61','0.15.62','0.15.63','0.15.64','0.15.65','0.15.66','0.15.67','0.15.68','0.15.69','0.15.70','0.15.71','0.15.72','0.15.73','0.15.74','0.15.75'].includes(packageVersion)){
|
|
87
87
|
assert.deepEqual([...diff.schema_changed].sort(),['machine_fs','native_diff_preview']);
|
|
88
88
|
assert.equal(diff.unchanged.length,41);
|
|
89
89
|
const prior=baseline.tools.find(tool=>tool.name==='machine_fs');
|
|
@@ -9,6 +9,7 @@ import {tmpdir} from 'node:os';
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import {loadCodingPolicy} from '../src/coding-plane.mjs';
|
|
11
11
|
import {createMachineFsRuntime} from '../src/machine-fs.mjs';
|
|
12
|
+
import {readRangeFromHandle} from '../src/machine-fs-handle-read-r196.mjs';
|
|
12
13
|
import {generateTrustKeypair} from '../src/trust.mjs';
|
|
13
14
|
import {machineFsGatewayInputSchema} from '../src/compact-gateway-mcp-tools.mjs';
|
|
14
15
|
import {registerMachineFsTools} from '../src/machine-fs-mcp-tools.mjs';
|
|
@@ -106,31 +107,44 @@ test('R64 large-offset read retains bounded worker cancellation',async t=>{
|
|
|
106
107
|
await assert.rejects(()=>runtime.read({rootId:'project',relativePath:'large.bin',offset:PAGE+1,maxBytes:1,timeoutMs:100}),/timed out after 100ms/);
|
|
107
108
|
});
|
|
108
109
|
|
|
109
|
-
test('R64 default hash stream
|
|
110
|
+
test('R64 default hash stream uses the retained handle and remains abort-bound',async()=>{
|
|
110
111
|
const source=await readFile(new URL('../src/machine-fs.mjs',import.meta.url),'utf8');
|
|
111
|
-
|
|
112
|
+
const handleSource=await readFile(new URL('../src/machine-fs-handle-read-r196.mjs',import.meta.url),'utf8');
|
|
113
|
+
assert.ok(/readRangeFromHandle\(handle,/.test(source),'read must use opened handle');
|
|
114
|
+
assert.ok(/handle\.createReadStream\(\{start:0,autoClose:false,signal\}\)/.test(handleSource),
|
|
115
|
+
'hash must stream through the retained FileHandle with abort signal');
|
|
112
116
|
assert.ok(/abortController\.abort\(\)/.test(source),'read timeout must abort its hash stream');
|
|
113
117
|
});
|
|
114
118
|
|
|
115
|
-
test('R64
|
|
119
|
+
test('R64 fallback FileHandle hash stream obeys abort independently of the new native backend',async t=>{
|
|
116
120
|
const f=await fixture(t);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
+
const file=path.join(f.project,'large.bin');
|
|
122
|
+
await writeFile(file,Buffer.alloc(PAGE+16));
|
|
123
|
+
const handle=await fs.promises.open(file,'r');
|
|
124
|
+
const proto=Object.getPrototypeOf(handle);
|
|
125
|
+
const original=proto.createReadStream;
|
|
126
|
+
let started,called=false,observedAbort=false;
|
|
127
|
+
const entered=new Promise(resolve=>started=resolve);
|
|
128
|
+
proto.createReadStream=function(options={}){
|
|
129
|
+
called=true;
|
|
121
130
|
const stream=new EventEmitter();
|
|
122
131
|
options.signal?.addEventListener('abort',()=>{
|
|
123
132
|
observedAbort=true;
|
|
124
133
|
queueMicrotask(()=>stream.emit('error',new Error('aborted')));
|
|
125
134
|
},{once:true});
|
|
135
|
+
started();
|
|
126
136
|
return stream;
|
|
127
137
|
};
|
|
128
|
-
|
|
138
|
+
const controller=new AbortController();
|
|
129
139
|
try{
|
|
130
|
-
|
|
131
|
-
|
|
140
|
+
const operation=readRangeFromHandle(handle,{offset:PAGE+1,length:1,hashFile:true,signal:controller.signal});
|
|
141
|
+
await entered;
|
|
142
|
+
controller.abort();
|
|
143
|
+
await assert.rejects(operation,/aborted/);
|
|
144
|
+
assert.equal(called,true,'fallback handle hash was not started');
|
|
145
|
+
assert.equal(observedAbort,true,'fallback hash stream ignored abort');
|
|
132
146
|
}finally{
|
|
133
|
-
|
|
134
|
-
|
|
147
|
+
proto.createReadStream=original;
|
|
148
|
+
await handle.close();
|
|
135
149
|
}
|
|
136
150
|
});
|