deadbyte-mcp 0.15.34 → 0.15.36
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 +100 -38
- package/PROVENANCE.json +87 -41
- package/README.txt +72 -14
- package/RELEASE.json +187 -150
- package/SBOM.cdx.json +25 -3
- package/TEST-MATRIX.json +258 -151
- package/bridge/package.json +13 -0
- package/bridge/src/bridge-health-r59.mjs +52 -0
- package/bridge/src/bridge.mjs +487 -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 +206 -0
- package/controller/deadbyte-controller.ps1 +152 -18
- package/controller/deadbyte-process-policy.json +4 -4
- package/docs/R59-TASK-CONTRACT.md +26 -0
- package/package.json +7 -3
- package/proof/r59-full-regression-current.json +13 -0
- package/proof/r59-full-regression-current.stderr.log +0 -0
- package/proof/r59-full-regression-current.stdout.log +1268 -0
- 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/gate-windows-r59-prepublish.ps1 +41 -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/r59-full-regression-capture.mjs +35 -0
- package/scripts/windows-gate-evidence-r58.mjs +93 -0
- package/src/capability-resolver.mjs +25 -7
- package/src/capability-snapshot.mjs +52 -0
- package/src/compact-gateway-mcp-tools.mjs +10 -11
- 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/mcp-server.mjs +179 -118
- package/src/r40-compact-disarmed-probe.mjs +1 -1
- package/src/r44-compact-disarmed-probe.mjs +1 -1
- package/src/r45-compact-disarmed-probe.mjs +1 -1
- package/src/release-generation.mjs +1 -0
- package/src/release-slot.mjs +2 -1
- package/src/remote-mcp-r40-parity-client.mjs +1 -1
- package/src/remote-mcp-r44-parity-client.mjs +1 -1
- package/src/remote-mcp-r45-parity-client.mjs +1 -1
- package/src/remote-mcp-smoke-client.mjs +1 -1
- package/src/remote-session-guardian.mjs +8 -1
- package/src/runtime-plane-registry.mjs +74 -0
- package/src/runtime-update.mjs +85 -2
- package/src/version.mjs +1 -1
- package/test/bridge-runtime-r59.test.mjs +60 -0
- package/test/capability-resolver.test.mjs +16 -0
- package/test/capability-snapshot-r59.test.mjs +39 -0
- package/test/compact-gateway-r36.test.mjs +22 -2
- package/test/compact-max-catalog-r44.test.mjs +1 -1
- package/test/compact-max-catalog-r45.test.mjs +1 -1
- package/test/contained.test.mjs +2 -1
- package/test/controller-policy-preflight-r59.test.mjs +39 -0
- package/test/controller-r211.test.mjs +4 -1
- package/test/controller.test.mjs +2 -1
- package/test/fixtures/r59-compact-surface-v0.15.35.json +222 -0
- package/test/helpers/r58-doctor-fixture.mjs +26 -0
- package/test/observation-integration.test.mjs +2 -3
- package/test/optional-plane-degraded-r59.test.mjs +44 -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 +24 -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 +98 -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 +50 -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 +32 -0
- package/test/r59-release-contract.test.mjs +61 -0
- package/test/r59-tool-surface-compat.test.mjs +90 -0
- package/test/release-generation.test.mjs +4 -1
- package/test/remote-session-cli.test.mjs +55 -2
- package/test/runtime-plane-registry-r59.test.mjs +40 -0
|
@@ -127,7 +127,7 @@ try {
|
|
|
127
127
|
if (compactProfile) {
|
|
128
128
|
const manifest = gateway(await client.callTool({ name: 'capability_manifest', arguments: {} }), 'capability_manifest');
|
|
129
129
|
const manifestV1 = manifest.schema === 'deadbyte.capability-manifest.v1' && exactR36Compact;
|
|
130
|
-
const manifestV2 = manifest.
|
|
130
|
+
const manifestV2 = ['deadbyte.capability-manifest.v2','deadbyte.capability-manifest.v3'].includes(manifest.schema) && (exactR40Compact || exactR44Compact);
|
|
131
131
|
if ((!manifestV1 && !manifestV2) || manifest.profile !== 'compact' ||
|
|
132
132
|
!Array.isArray(manifest.capabilities) || !/^[0-9a-f]{64}$/.test(String(manifest.ledger_sha256 ?? ''))) {
|
|
133
133
|
throw new Error('compact capability manifest malformed');
|
|
@@ -8,6 +8,10 @@ import { windowsPowerShellEnvironment } from './runtime-update.mjs';
|
|
|
8
8
|
import { resolveStateRoot } from './state-root.mjs';
|
|
9
9
|
|
|
10
10
|
const CLEANUP=['disarmautonomous','disarmcoding','disarmprocess','disarmdesktop','disarmhost','park'];
|
|
11
|
+
// This inherited IPC channel is an OS-owned parent lifetime handle, not a PID
|
|
12
|
+
// poll. A reused PID cannot keep a new foreground guardian alive.
|
|
13
|
+
let ownerDisconnected=false;
|
|
14
|
+
process.once('disconnect',()=>{ownerDisconnected=true;});
|
|
11
15
|
|
|
12
16
|
function parseArgs(argv){
|
|
13
17
|
const out={};
|
|
@@ -51,7 +55,10 @@ async function main(){
|
|
|
51
55
|
const root=resolveStateRoot(args['state-root']);
|
|
52
56
|
const ownerPid=Number(args['owner-pid']);
|
|
53
57
|
const nonce=String(args['session-nonce']||'');
|
|
58
|
+
const ipcOwner=args['owner-channel']==='ipc';
|
|
59
|
+
if(ipcOwner&&typeof process.send!=='function')throw new Error('guardian owner IPC channel missing');
|
|
54
60
|
if(!Number.isInteger(ownerPid)||ownerPid<1) throw new Error('guardian owner pid invalid');
|
|
61
|
+
if(ipcOwner&&process.ppid!==ownerPid)throw new Error('guardian IPC parent does not match declared owner');
|
|
55
62
|
if(!/^[0-9a-f]{64}$/.test(nonce)) throw new Error('guardian session nonce invalid');
|
|
56
63
|
|
|
57
64
|
const launcher=await requireRegular(path.join(root,'bin','DEADBYTE-MCP.ps1'),'stable DEADBYTE launcher');
|
|
@@ -63,7 +70,7 @@ async function main(){
|
|
|
63
70
|
for(;;){
|
|
64
71
|
const marker=await readMarker(markerPath);
|
|
65
72
|
if(!marker||!markerMatches(marker,{ownerPid,nonce})) return;
|
|
66
|
-
if(pidAlive(ownerPid)){
|
|
73
|
+
if(ipcOwner?!ownerDisconnected:pidAlive(ownerPid)){
|
|
67
74
|
await sleep(250);
|
|
68
75
|
continue;
|
|
69
76
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const PLANE_ID=/^[a-z][a-z0-9_-]{0,63}$/;
|
|
2
|
+
|
|
3
|
+
function boundedReason(error){
|
|
4
|
+
const text=error instanceof Error?error.message:String(error);
|
|
5
|
+
return text.replace(/[\r\n\t]+/g,' ').slice(0,1024);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class RuntimePlaneUnavailableError extends Error {
|
|
9
|
+
constructor(plane,diagnostic={}){
|
|
10
|
+
super(`${plane} runtime unavailable: ${String(diagnostic.reason??diagnostic.code??'not ready')}`);
|
|
11
|
+
this.name='RuntimePlaneUnavailableError';
|
|
12
|
+
this.code='DEADBYTE_RUNTIME_UNAVAILABLE';
|
|
13
|
+
this.plane=plane;
|
|
14
|
+
this.diagnostic=diagnostic;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createUnavailableRuntime(plane,diagnostic={}){
|
|
19
|
+
if(typeof plane!=='string'||!PLANE_ID.test(plane)) throw new Error('runtime plane id invalid');
|
|
20
|
+
const fail=()=>Promise.reject(new RuntimePlaneUnavailableError(plane,diagnostic));
|
|
21
|
+
return new Proxy(Object.freeze({plane,unavailable:true,diagnostic}),{
|
|
22
|
+
get(target,property){
|
|
23
|
+
if(property in target) return target[property];
|
|
24
|
+
if(property==='then') return undefined;
|
|
25
|
+
return fail;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createRuntimePlaneRegistry(){
|
|
31
|
+
const planes=new Map();
|
|
32
|
+
let epoch=0;
|
|
33
|
+
function update(id,next){
|
|
34
|
+
planes.set(id,Object.freeze({...next}));
|
|
35
|
+
epoch++;
|
|
36
|
+
return next.runtime??null;
|
|
37
|
+
}
|
|
38
|
+
function define(id,{configured,required=false,dependencies=[]}={}){
|
|
39
|
+
if(typeof id!=='string'||!PLANE_ID.test(id)||planes.has(id)) throw new Error(`runtime plane definition invalid: ${String(id)}`);
|
|
40
|
+
if(!Array.isArray(dependencies)||dependencies.some(dep=>typeof dep!=='string'||!PLANE_ID.test(dep))) throw new Error(`runtime plane dependencies invalid: ${id}`);
|
|
41
|
+
planes.set(id,Object.freeze({id,configured:configured===true,required:required===true,dependencies:[...new Set(dependencies)],state:configured===true?'STARTING':'DISABLED',ready:false,runtime:null,diagnostic:configured===true?null:{code:'RUNTIME_DISABLED',reason:'runtime is not configured',recoverable:false}}));
|
|
42
|
+
}
|
|
43
|
+
async function initialize(id,initializer,{policySha256=null}={}){
|
|
44
|
+
const current=planes.get(id);
|
|
45
|
+
if(!current) throw new Error(`runtime plane not defined: ${id}`);
|
|
46
|
+
if(!current.configured) return null;
|
|
47
|
+
for(const dependency of current.dependencies){
|
|
48
|
+
const dep=planes.get(dependency);
|
|
49
|
+
if(!dep||!dep.ready){
|
|
50
|
+
return update(id,{...current,state:'UNAVAILABLE',ready:false,runtime:null,policy_sha256:policySha256,
|
|
51
|
+
diagnostic:{code:'DEPENDENCY_UNAVAILABLE',reason:`dependency '${dependency}' is not ready`,recoverable:true}});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
try{
|
|
55
|
+
const runtime=await initializer();
|
|
56
|
+
if(!runtime) throw new Error('initializer returned no runtime');
|
|
57
|
+
return update(id,{...current,state:'READY',ready:true,runtime,policy_sha256:policySha256,diagnostic:null});
|
|
58
|
+
}catch(error){
|
|
59
|
+
if(current.required) throw error;
|
|
60
|
+
return update(id,{...current,state:'DEGRADED',ready:false,runtime:null,policy_sha256:policySha256,
|
|
61
|
+
diagnostic:{code:'INITIALIZATION_FAILED',reason:boundedReason(error),recoverable:true}});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function state(id){
|
|
65
|
+
const value=planes.get(id);
|
|
66
|
+
if(!value) throw new Error(`runtime plane not defined: ${id}`);
|
|
67
|
+
const {runtime,...publicState}=value;
|
|
68
|
+
return Object.freeze(publicState);
|
|
69
|
+
}
|
|
70
|
+
function runtime(id){return planes.get(id)?.runtime??null;}
|
|
71
|
+
function snapshot(){return Object.freeze({epoch,planes:Object.freeze(Object.fromEntries([...planes].map(([id])=>[id,state(id)])))});}
|
|
72
|
+
return Object.freeze({define,initialize,state,runtime,snapshot});
|
|
73
|
+
}
|
|
74
|
+
|
package/src/runtime-update.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, createPrivateKey, createPublicKey, randomBytes, sign as cryptoSign, verify as cryptoVerify } from 'node:crypto';
|
|
2
2
|
import { spawn, spawnSync } from 'node:child_process';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
|
-
import { lstat, mkdir, open, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { lstat, mkdir, open, readFile, readdir, realpath, rename, rm, writeFile } from 'node:fs/promises';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
@@ -306,6 +306,69 @@ async function normalizeComponentIdentities(config){
|
|
|
306
306
|
return next;
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
function bridgeDependencyName(relativePath,usedNames){
|
|
310
|
+
const normalized=String(relativePath).replaceAll('\\','/').replaceAll('/','_');
|
|
311
|
+
let name=normalized;
|
|
312
|
+
if(!/^[A-Za-z0-9._-]{1,32}$/.test(name)||usedNames.has(name)){
|
|
313
|
+
name=`dep-${sha256Bytes(Buffer.from(String(relativePath),'utf8')).slice(0,20)}`;
|
|
314
|
+
}
|
|
315
|
+
if(usedNames.has(name)) throw new Error(`bridge dependency name collision: ${relativePath}`);
|
|
316
|
+
usedNames.add(name);
|
|
317
|
+
return name;
|
|
318
|
+
}
|
|
319
|
+
function bridgeStaticRelativeImports(source,file){
|
|
320
|
+
const found=[];
|
|
321
|
+
for(const [index,line] of String(source).split(/\r?\n/).entries()){
|
|
322
|
+
if(/\bimport\s*\(\s*['"]\.\.?\//.test(line)) throw new Error(`bridge dynamic relative import is forbidden: ${file}:${index+1}`);
|
|
323
|
+
const importStart=/^\s*import\b/.test(line);
|
|
324
|
+
const exportStart=/^\s*export\b/.test(line)&&/\bfrom\b/.test(line);
|
|
325
|
+
if(!importStart&&!exportStart) continue;
|
|
326
|
+
const match=importStart
|
|
327
|
+
? /^\s*import\s+(?:(?:[^'"]+?)\s+from\s*)?['"]([^'"]+)['"]\s*;?\s*$/.exec(line)
|
|
328
|
+
: /^\s*export\s+[^'"]*?\s+from\s*['"]([^'"]+)['"]\s*;?\s*$/.exec(line);
|
|
329
|
+
if(!match) throw new Error(`bridge import syntax unsupported for dependency binding: ${file}:${index+1}`);
|
|
330
|
+
if(match[1].startsWith('.')) found.push(match[1]);
|
|
331
|
+
}
|
|
332
|
+
return found;
|
|
333
|
+
}
|
|
334
|
+
async function discoverBridgeLocalDependencies(root,entryScript){
|
|
335
|
+
const absoluteRoot=path.resolve(root),realRoot=await realpath(absoluteRoot);
|
|
336
|
+
const srcRoot=path.join(absoluteRoot,'src');
|
|
337
|
+
const queue=[path.resolve(entryScript)],visited=new Set(),files=[];
|
|
338
|
+
while(queue.length){
|
|
339
|
+
const current=queue.shift();
|
|
340
|
+
const currentReal=await realpath(current);
|
|
341
|
+
const relativeReal=path.relative(realRoot,currentReal);
|
|
342
|
+
if(relativeReal.startsWith('..')||path.isAbsolute(relativeReal)) throw new Error(`bridge dependency escapes bridge root: ${current}`);
|
|
343
|
+
if(visited.has(currentReal)) continue;
|
|
344
|
+
visited.add(currentReal);
|
|
345
|
+
if(visited.size>16) throw new Error('bridge local dependency closure exceeds 16 files');
|
|
346
|
+
const stat=await lstat(current);
|
|
347
|
+
if(!stat.isFile()||stat.isSymbolicLink()) throw new Error(`bridge dependency must be a regular file: ${current}`);
|
|
348
|
+
const source=await readFile(current,'utf8');
|
|
349
|
+
const specifiers=bridgeStaticRelativeImports(source,current);
|
|
350
|
+
if(currentReal!==await realpath(entryScript)) files.push(currentReal);
|
|
351
|
+
for(const specifier of specifiers){
|
|
352
|
+
if(!specifier.startsWith('./')) throw new Error(`bridge relative import escapes bridge root: ${specifier}`);
|
|
353
|
+
if(specifier.includes('?')||specifier.includes('#')) throw new Error(`bridge relative import query/fragment unsupported: ${specifier}`);
|
|
354
|
+
const resolved=path.resolve(path.dirname(current),specifier);
|
|
355
|
+
const relative=path.relative(absoluteRoot,resolved);
|
|
356
|
+
if(relative.startsWith('..')||path.isAbsolute(relative)) throw new Error(`bridge dependency escapes bridge root: ${specifier}`);
|
|
357
|
+
if(!/\.(?:mjs|js)$/i.test(resolved)) throw new Error(`bridge local dependency must be .mjs/.js: ${specifier}`);
|
|
358
|
+
queue.push(resolved);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const unique=[...new Set(files)].sort((a,b)=>a.localeCompare(b));
|
|
362
|
+
const usedNames=new Set();
|
|
363
|
+
const dependencies=[];
|
|
364
|
+
for(const file of unique){
|
|
365
|
+
const relative=path.relative(srcRoot,file).replaceAll('\\','/');
|
|
366
|
+
if(relative.startsWith('..')||path.isAbsolute(relative)) throw new Error(`bridge dependency must remain under src: ${file}`);
|
|
367
|
+
dependencies.push({Name:bridgeDependencyName(relative,usedNames),Path:file,Sha256:await sha256File(file)});
|
|
368
|
+
}
|
|
369
|
+
return dependencies;
|
|
370
|
+
}
|
|
371
|
+
|
|
309
372
|
export async function prepareBridgeSpec(config,bootstrap){
|
|
310
373
|
if(!bootstrap) return structuredClone(config);
|
|
311
374
|
if(typeof bootstrap.root!=='string'||!path.isAbsolute(bootstrap.root)||!HEX64.test(String(bootstrap.scriptSha256??''))) {
|
|
@@ -327,6 +390,7 @@ export async function prepareBridgeSpec(config,bootstrap){
|
|
|
327
390
|
next.Bridge.Arguments='src\\bridge.mjs';
|
|
328
391
|
next.Bridge.ScriptPath=script;
|
|
329
392
|
next.Bridge.ScriptSha256=actual;
|
|
393
|
+
next.Bridge.Dependencies=await discoverBridgeLocalDependencies(root,script);
|
|
330
394
|
return next;
|
|
331
395
|
}
|
|
332
396
|
|
|
@@ -1334,6 +1398,23 @@ function handoffPriorPointer(body){
|
|
|
1334
1398
|
function handoffNewPointer(body){
|
|
1335
1399
|
return body.new_pointer?parseActivePointer(JSON.stringify(body.new_pointer)):null;
|
|
1336
1400
|
}
|
|
1401
|
+
async function verifyCompletedUpdateReceipt(body,state,statePath,key){
|
|
1402
|
+
const candidate=handoffNewPointer(body),prior=handoffPriorPointer(body);
|
|
1403
|
+
if(!candidate||!samePublicPointer(candidate,statePointer(state,'new_pointer'))||!samePublicPointer(prior,statePointer(state,'prior_pointer'))) throw new Error('completed update handoff pointer binding mismatch');
|
|
1404
|
+
if(state.expected_version!==candidate.version||state.expected_manifest_sha256!==candidate.manifest_sha256) throw new Error('completed update journal subject mismatch');
|
|
1405
|
+
const expectedPath=path.join(path.dirname(statePath),'receipt.json');
|
|
1406
|
+
if(typeof state.receipt_path!=='string'||!path.isAbsolute(state.receipt_path)||path.resolve(state.receipt_path)!==expectedPath||!HEX64.test(String(state.receipt_sha256??''))) throw new Error('completed update receipt reference invalid');
|
|
1407
|
+
const info=await lstat(expectedPath);
|
|
1408
|
+
if(!info.isFile()||info.isSymbolicLink()) throw new Error('completed update receipt must be a regular file');
|
|
1409
|
+
const receipt=await readJson(expectedPath);
|
|
1410
|
+
if(!verifySlotUpdateEnvelope(receipt,key.publicKey)||receipt.receipt_sha256!==state.receipt_sha256) throw new Error('completed update receipt signature or digest mismatch');
|
|
1411
|
+
const subject=receipt.body;
|
|
1412
|
+
if(subject.schema!==UPDATE_SCHEMA||subject.rollback_performed!==false||subject.update_id!==body.update_id||subject.release_version!==candidate.version||subject.candidate_manifest_sha256!==candidate.manifest_sha256||
|
|
1413
|
+
!samePublicPointer(statePointer(subject,'active_pointer'),candidate)||!samePublicPointer(statePointer(subject,'prior_pointer'),prior)||
|
|
1414
|
+
subject.release_slot?.release_id!==candidate.release_id||subject.release_slot?.release_root!==candidate.release_root||subject.release_slot?.manifest_sha256!==candidate.manifest_sha256||
|
|
1415
|
+
subject.generation?.generation_root!==candidate.generation_root||subject.generation?.generation_manifest_sha256!==candidate.generation_manifest_sha256||subject.launcher?.sha256!==candidate.launcher_sha256) throw new Error('completed update receipt subject binding mismatch');
|
|
1416
|
+
return {receipt_path:expectedPath,receipt_sha256:receipt.receipt_sha256};
|
|
1417
|
+
}
|
|
1337
1418
|
async function resumeSlotUpdate({handoffPath}){
|
|
1338
1419
|
const absoluteHandoff=path.resolve(handoffPath),probe=await readJson(absoluteHandoff);
|
|
1339
1420
|
const stateRoot=path.resolve(String(probe?.body?.state_root??path.dirname(path.dirname(path.dirname(path.dirname(absoluteHandoff))))));
|
|
@@ -1342,8 +1423,9 @@ async function resumeSlotUpdate({handoffPath}){
|
|
|
1342
1423
|
if(releaseState.schema!=='deadbyte.r27-slot-update-state.v1'||releaseState.update_id!==body.update_id) throw new Error('handoff state binding mismatch');
|
|
1343
1424
|
let phase=String(releaseState.phase??'');
|
|
1344
1425
|
if(phase==='completed'){
|
|
1426
|
+
const verifiedReceipt=await verifyCompletedUpdateReceipt(body,releaseState,statePath,key);
|
|
1345
1427
|
return {ok:true,resumed:false,completed:true,update_id:body.update_id,state_path:statePath,
|
|
1346
|
-
|
|
1428
|
+
...verifiedReceipt,historical_receipt_verified:true,current_runtime_verified:false};
|
|
1347
1429
|
}
|
|
1348
1430
|
if(phase.startsWith('failed')) throw new Error('slot update already failed at phase='+phase);
|
|
1349
1431
|
const priorPointer=handoffPriorPointer(body),candidatePointer=handoffNewPointer(body);
|
|
@@ -1735,6 +1817,7 @@ export async function runSlotUpdateCli(argv=process.argv.slice(2),{stateRoot=DEF
|
|
|
1735
1817
|
console.log('UPDATE_ID '+result.update_id);
|
|
1736
1818
|
console.log('STATE_PATH '+result.state_path);
|
|
1737
1819
|
if(result.receipt_sha256) console.log('RECEIPT_SHA256 '+result.receipt_sha256);
|
|
1820
|
+
if(result.historical_receipt_verified===true) console.log('VERIFICATION_SCOPE historical-signed-receipt; current-runtime-not-observed');
|
|
1738
1821
|
return result;
|
|
1739
1822
|
}
|
|
1740
1823
|
if(argv.includes('--recover-interrupted')){
|
package/src/version.mjs
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createBridgeGovernor } from '../bridge/src/r40-governor.mjs';
|
|
4
|
+
import { createBridgeHealth } from '../bridge/src/bridge-health-r59.mjs';
|
|
5
|
+
|
|
6
|
+
const limits={
|
|
7
|
+
normal:{max_active:1,max_queue:2,queue_timeout_ms:1000},
|
|
8
|
+
autonomous:{max_active:1,max_queue:1,queue_timeout_ms:1000},
|
|
9
|
+
desktop_observe:{max_active:1,max_queue:1,queue_timeout_ms:1000},
|
|
10
|
+
desktop_action:{max_active:1,max_queue:1,queue_timeout_ms:1000}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
test('R59 bridge governor removes cancelled queued work and drains without replay',async()=>{
|
|
14
|
+
const governor=createBridgeGovernor({limits});
|
|
15
|
+
const first=await governor.acquire('machine_status');
|
|
16
|
+
const abort=new AbortController();
|
|
17
|
+
const queued=governor.acquire('machine_ping',{signal:abort.signal});
|
|
18
|
+
abort.abort(new Error('caller disconnected'));
|
|
19
|
+
await assert.rejects(queued,/caller disconnected|aborted/);
|
|
20
|
+
assert.equal(governor.snapshot().normal.queued,0);
|
|
21
|
+
assert.equal(governor.snapshot().normal.cancelled,1);
|
|
22
|
+
const queuedDuringRecovery=governor.acquire('machine_ping');
|
|
23
|
+
assert.equal(governor.reconcile('core generation changed'),1);
|
|
24
|
+
await assert.rejects(queuedDuringRecovery,/core generation changed/);
|
|
25
|
+
first.release();
|
|
26
|
+
const after=await governor.acquire('machine_status');
|
|
27
|
+
after.release();
|
|
28
|
+
const drain=governor.close('bridge reconciling');
|
|
29
|
+
await assert.rejects(governor.acquire('machine_status'),/closed/);
|
|
30
|
+
await drain;
|
|
31
|
+
assert.equal(governor.snapshot().normal.active,0);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('R59 bridge health requires fresh child negotiation catalog and v3 capability attestation',()=>{
|
|
35
|
+
let now=100_000;
|
|
36
|
+
const health=createBridgeHealth({clock:()=>now,maxCapabilityAgeMs:15_000});
|
|
37
|
+
assert.equal(health.snapshot().http_status,503);
|
|
38
|
+
health.coreConnected({childIdentity:'core-abc',protocolEra:'modern',catalogSha256:'a'.repeat(64)});
|
|
39
|
+
assert.equal(health.snapshot().http_status,503);
|
|
40
|
+
health.capabilityObserved({schema:'deadbyte.capability-manifest.v3',snapshot_id:'b'.repeat(64),
|
|
41
|
+
state_epoch:3,observed_at_utc:new Date(now).toISOString(),valid_until_utc:new Date(now+5000).toISOString(),
|
|
42
|
+
bindings:{release_manifest_sha256:'c'.repeat(64),generation_manifest_sha256:'d'.repeat(64)}});
|
|
43
|
+
assert.equal(health.snapshot().state,'READY');
|
|
44
|
+
assert.equal(health.snapshot().http_status,200);
|
|
45
|
+
health.capabilityObserved({schema:'deadbyte.capability-manifest.v3',snapshot_id:'e'.repeat(64),state_epoch:4,
|
|
46
|
+
observed_at_utc:new Date(now).toISOString(),valid_until_utc:new Date(now+5000).toISOString(),
|
|
47
|
+
bindings:{release_manifest_sha256:'c'.repeat(64),generation_manifest_sha256:'d'.repeat(64)},
|
|
48
|
+
planes:{DEADBYTE_CODING_RUNTIME:{state:'DEGRADED',diagnostic:{code:'INITIALIZATION_FAILED'}}}});
|
|
49
|
+
assert.equal(health.snapshot().state,'DEGRADED');
|
|
50
|
+
assert.equal(health.snapshot().http_status,503);
|
|
51
|
+
health.capabilityObserved({schema:'deadbyte.capability-manifest.v3',snapshot_id:'b'.repeat(64),state_epoch:5,
|
|
52
|
+
observed_at_utc:new Date(now).toISOString(),valid_until_utc:new Date(now+5000).toISOString(),bindings:{}});
|
|
53
|
+
now+=15_001;
|
|
54
|
+
assert.equal(health.snapshot().state,'DEGRADED');
|
|
55
|
+
assert.equal(health.snapshot().http_status,503);
|
|
56
|
+
health.reconciling('core transport closed after dispatch');
|
|
57
|
+
const degraded=health.snapshot();
|
|
58
|
+
assert.equal(degraded.state,'RECONCILING');
|
|
59
|
+
assert.doesNotMatch(JSON.stringify(degraded),/token|secret|C:\\/i);
|
|
60
|
+
});
|
|
@@ -18,6 +18,22 @@ test('capability resolver enforces 5 canonical states', () => {
|
|
|
18
18
|
assert.equal(CAPABILITY_STATES.UNAVAILABLE, 'UNAVAILABLE');
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
+
test('R59 live plane state can only reduce signed capability availability', async()=>{
|
|
22
|
+
const ledger={entries:[{
|
|
23
|
+
id:'coding.search',compact_tool:'coding_inspect',full_tool:'coding_search',compact_op:'search',
|
|
24
|
+
authority:'coding',mutating:false,production:true,runtime_flag:'DEADBYTE_CODING_RUNTIME'
|
|
25
|
+
}]};
|
|
26
|
+
const result=await resolveCapabilityStates({
|
|
27
|
+
ledger,toolCatalog:['coding_inspect'],profile:'compact',env:{DEADBYTE_CODING_RUNTIME:'1'},
|
|
28
|
+
codingRuntime:{sessionStatus:async()=>({armed:true})},
|
|
29
|
+
runtimePlanes:{DEADBYTE_CODING_RUNTIME:{state:'DEGRADED',ready:false,diagnostic:{code:'INITIALIZATION_FAILED',reason:'missing root',recoverable:true}}}
|
|
30
|
+
});
|
|
31
|
+
assert.equal(result.capabilities[0].state,CAPABILITY_STATES.DEGRADED);
|
|
32
|
+
assert.equal(result.capabilities[0].ready,false);
|
|
33
|
+
assert.equal(result.capabilities[0].diagnostic.code,'INITIALIZATION_FAILED');
|
|
34
|
+
assert.deepEqual(result.effective_capabilities,[]);
|
|
35
|
+
});
|
|
36
|
+
|
|
21
37
|
test('deriveCapabilityRequirements maps dependencies accurately', () => {
|
|
22
38
|
const reqCodingRead = deriveCapabilityRequirements({
|
|
23
39
|
id: 'coding.search',
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createCapabilitySnapshotTracker } from '../src/capability-snapshot.mjs';
|
|
4
|
+
|
|
5
|
+
const H=value=>String(value).repeat(64).slice(0,64);
|
|
6
|
+
const ledger={schema:'deadbyte.capability-ledger.v1',ledger_sha256:H('a'),entries:[{
|
|
7
|
+
id:'machine.status',full_tool:'machine_status',compact_tool:'machine_observe',compact_op:'machine_status',
|
|
8
|
+
runtime_flag:'DEADBYTE_MACHINE_RUNTIME',authority:'none',mutating:false,receipt_class:'machine',production:true,host_only:false
|
|
9
|
+
}]};
|
|
10
|
+
const resolution={capabilities:[{...ledger.entries[0],tool:'machine_observe',operations:['machine_status'],requires:['DEADBYTE_MACHINE_RUNTIME'],state:'AVAILABLE',ready:true,diagnostic:null}],effective_capabilities:['machine.status'],unavailable_capabilities:[],degraded_capabilities:[]};
|
|
11
|
+
|
|
12
|
+
test('R59 capability snapshot v3 is a backward-compatible content-bound live overlay', () => {
|
|
13
|
+
let now=Date.parse('2026-10-02T12:00:00.000Z');
|
|
14
|
+
const tracker=createCapabilitySnapshotTracker({clock:()=>now,ttlMs:5000});
|
|
15
|
+
const input={profile:'compact',ledger,toolCatalog:['machine_observe'],catalogSha256:H('b'),resolution,
|
|
16
|
+
bindings:{release_id:'v0.15.36-test',release_manifest_sha256:H('c'),generation_manifest_sha256:H('d')},
|
|
17
|
+
planes:{machine:{state:'READY',ready:true,policy_sha256:null,diagnostic:null}}};
|
|
18
|
+
const first=tracker.snapshot(input);
|
|
19
|
+
assert.equal(first.schema,'deadbyte.capability-manifest.v3');
|
|
20
|
+
assert.equal(first.ledger_sha256,ledger.ledger_sha256);
|
|
21
|
+
assert.deepEqual(first.tool_catalog,['machine_observe']);
|
|
22
|
+
assert.equal(first.state_epoch,1);
|
|
23
|
+
assert.match(first.snapshot_id,/^[0-9a-f]{64}$/);
|
|
24
|
+
assert.equal(first.capabilities[0].selected_route.tool,'machine_observe');
|
|
25
|
+
now+=1000;
|
|
26
|
+
const same=tracker.snapshot(input);
|
|
27
|
+
assert.equal(same.snapshot_id,first.snapshot_id);
|
|
28
|
+
assert.equal(same.state_epoch,1);
|
|
29
|
+
assert.notEqual(same.observed_at_utc,first.observed_at_utc);
|
|
30
|
+
const changed=tracker.snapshot({...input,planes:{machine:{state:'DEGRADED',ready:false,policy_sha256:null,diagnostic:{code:'DOWN',reason:'probe failed'}}}});
|
|
31
|
+
assert.equal(changed.state_epoch,2);
|
|
32
|
+
assert.notEqual(changed.snapshot_id,first.snapshot_id);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('R59 capability snapshot rejects routes outside the signed ledger', () => {
|
|
36
|
+
const tracker=createCapabilitySnapshotTracker();
|
|
37
|
+
assert.throws(()=>tracker.snapshot({profile:'compact',ledger,toolCatalog:['invented'],catalogSha256:H('b'),
|
|
38
|
+
resolution:{...resolution,capabilities:[{...resolution.capabilities[0],id:'invented.capability'}]},bindings:{},planes:{}}),/outside signed ledger/);
|
|
39
|
+
});
|
|
@@ -72,7 +72,7 @@ test('R36 coding_inspect gateway calls runtime directly and never MCP-recurses',
|
|
|
72
72
|
assert.equal(result.structuredContent.result.path,'src/a.mjs');
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
test('
|
|
75
|
+
test('R59 capability manifest v3 preserves exact compact catalog attestation',async()=>{
|
|
76
76
|
const server=fakeServer();
|
|
77
77
|
registerCompactGatewayTools(server,{
|
|
78
78
|
profile:'compact',ledger:{schema:'deadbyte.capability-ledger.v1',entries:[],ledger_sha256:'a'.repeat(64)},
|
|
@@ -81,13 +81,33 @@ test('R40 capability manifest v2 attests the exact registered compact catalog',a
|
|
|
81
81
|
});
|
|
82
82
|
const result=await server.tools.get('capability_manifest').handler({});
|
|
83
83
|
const manifest=result.structuredContent.result;
|
|
84
|
-
assert.equal(manifest.schema,'deadbyte.capability-manifest.
|
|
84
|
+
assert.equal(manifest.schema,'deadbyte.capability-manifest.v3');
|
|
85
85
|
assert.equal(manifest.profile,'compact');
|
|
86
86
|
assert.deepEqual(manifest.tool_catalog,[...server.tools.keys()].sort());
|
|
87
87
|
assert.equal(manifest.tool_count,server.tools.size);
|
|
88
88
|
assert.match(manifest.catalog_sha256,/^[0-9a-f]{64}$/);
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
+
test('R59 capability manifest v3 preserves v2 catalog fields and adds live bindings',async()=>{
|
|
92
|
+
const server=fakeServer();
|
|
93
|
+
registerCompactGatewayTools(server,{
|
|
94
|
+
profile:'compact',ledger:{schema:'deadbyte.capability-ledger.v1',entries:[],ledger_sha256:'a'.repeat(64)},
|
|
95
|
+
codingRuntime:null,semanticRuntime:null,machineFsRuntime:null,processRuntime:null,observationRuntime:null,machineRuntime:null,desktopRuntime:null,
|
|
96
|
+
runtimePlanes:{DEADBYTE_CODING_RUNTIME:{state:'DEGRADED',ready:false,diagnostic:{code:'INITIALIZATION_FAILED',reason:'missing root',recoverable:true}}},
|
|
97
|
+
releaseBindingProvider:async()=>({release_id:'v0.15.36-test',release_manifest_sha256:'b'.repeat(64),generation_manifest_sha256:'c'.repeat(64)}),
|
|
98
|
+
catalogProvider:()=>[...server.tools.keys()]
|
|
99
|
+
});
|
|
100
|
+
const result=await server.tools.get('capability_manifest').handler({});
|
|
101
|
+
const manifest=result.structuredContent.result;
|
|
102
|
+
assert.equal(manifest.schema,'deadbyte.capability-manifest.v3');
|
|
103
|
+
assert.equal(manifest.tool_count,server.tools.size);
|
|
104
|
+
assert.deepEqual(manifest.tool_catalog,[...server.tools.keys()].sort());
|
|
105
|
+
assert.match(manifest.snapshot_id,/^[0-9a-f]{64}$/);
|
|
106
|
+
assert.equal(manifest.state_epoch,1);
|
|
107
|
+
assert.equal(manifest.bindings.release_id,'v0.15.36-test');
|
|
108
|
+
assert.equal(manifest.planes.DEADBYTE_CODING_RUNTIME.state,'DEGRADED');
|
|
109
|
+
});
|
|
110
|
+
|
|
91
111
|
|
|
92
112
|
test('R40 desktop_control is stable when runtime is absent and routes typed read-only operations when present',async()=>{
|
|
93
113
|
const server=fakeServer();
|
|
@@ -81,7 +81,7 @@ test('R44 real max-runtime compact stdio catalog is exactly 43 with verified ski
|
|
|
81
81
|
const manifest=await client.callTool({name:'capability_manifest',arguments:{}});
|
|
82
82
|
assert.equal(manifest.isError,false);
|
|
83
83
|
const m=manifest.structuredContent.result;
|
|
84
|
-
assert.equal(m.schema,'deadbyte.capability-manifest.
|
|
84
|
+
assert.equal(m.schema,'deadbyte.capability-manifest.v3');
|
|
85
85
|
assert.equal(m.profile,'compact');
|
|
86
86
|
assert.equal(m.tool_count,43);
|
|
87
87
|
assert.deepEqual(m.tool_catalog,names);
|
|
@@ -81,7 +81,7 @@ test('R45 real max-runtime compact stdio catalog is exactly 43 with verified ski
|
|
|
81
81
|
const manifest=await client.callTool({name:'capability_manifest',arguments:{}});
|
|
82
82
|
assert.equal(manifest.isError,false);
|
|
83
83
|
const m=manifest.structuredContent.result;
|
|
84
|
-
assert.equal(m.schema,'deadbyte.capability-manifest.
|
|
84
|
+
assert.equal(m.schema,'deadbyte.capability-manifest.v3');
|
|
85
85
|
assert.equal(m.profile,'compact');
|
|
86
86
|
assert.equal(m.tool_count,43);
|
|
87
87
|
assert.deepEqual(m.tool_catalog,names);
|
package/test/contained.test.mjs
CHANGED
|
@@ -714,7 +714,8 @@ test('MCP contained_transform schema is a compile-time operation enum with no ex
|
|
|
714
714
|
assert.ok(containedStart >= 0 && containedEnd > containedStart);
|
|
715
715
|
assert.match(contained, /z\.enum\(CONTAINED_TRANSFORM_OPERATIONS\)/);
|
|
716
716
|
assert.match(contained, /inputSchema: z\.strictObject\(\{/);
|
|
717
|
-
assert.match(server, /if \(
|
|
717
|
+
assert.match(server, /if \(planeRegistry\.state\('host'\)\.configured\)/);
|
|
718
|
+
assert.match(server, /requirePlane\('host',hostPolicy\)/);
|
|
718
719
|
for (const name of ['host_file_read', 'host_file_write', 'host_exec']) assert.match(server, new RegExp(`'${name}'`));
|
|
719
720
|
assert.doesNotMatch(contained, /inputSchema: z\.object\(\{/);
|
|
720
721
|
assert.doesNotMatch(contained, /executable:\s*z\.|argv:\s*z\.|environment:\s*z\.|challenge:\s*z\.|request_digest:\s*z\.|worker_id:\s*z\./);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
const controllerPath=new URL('../controller/deadbyte-controller.ps1',import.meta.url);
|
|
9
|
+
function quote(value){return `'${String(value).replaceAll("'","''")}'`;}
|
|
10
|
+
function functionBlock(source,name,nextName){
|
|
11
|
+
const start=source.indexOf(`function ${name}`),end=source.indexOf(`function ${nextName}`,start+1);
|
|
12
|
+
assert.ok(start>=0&&end>start,`missing ${name}`);
|
|
13
|
+
return source.slice(start,end);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test('R59 controller refuses to arm process or coding policy with a missing root',async()=>{
|
|
17
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r59-controller-preflight-'));
|
|
18
|
+
try{
|
|
19
|
+
const runtime=path.join(root,'runtime');
|
|
20
|
+
const missing=path.join(root,'missing');
|
|
21
|
+
const coding=path.join(root,'coding.json'),processPolicy=path.join(root,'process.json');
|
|
22
|
+
await writeFile(coding,JSON.stringify({schema:'deadbyte.coding-policy.v1',enabled:true,
|
|
23
|
+
lease_file:path.join(runtime,'coding-grant.json'),roots:{project:{path:missing}}}));
|
|
24
|
+
await writeFile(processPolicy,JSON.stringify({schema:'deadbyte.process-policy.v1',enabled:true,
|
|
25
|
+
grant_file:path.join(runtime,'process-grant.json'),roots:{project:{path:missing}},profiles:{node:{}}}));
|
|
26
|
+
const source=await readFile(controllerPath,'utf8');
|
|
27
|
+
const blocks=[functionBlock(source,'Assert-PolicyRootsAvailable','Get-HostPolicyMetadata'),
|
|
28
|
+
functionBlock(source,'Get-ProcessPolicyMetadata','Get-ProcessGrantStatus'),
|
|
29
|
+
functionBlock(source,'Get-CodingPolicyMetadata','Get-CodingLeaseStatus')].join('\n');
|
|
30
|
+
const script=`$ErrorActionPreference='Stop';$RuntimeDir=${quote(runtime)};function Fail([string]$m){throw $m};function Get-ExecutableHash($p){return '${'a'.repeat(64)}'};${blocks};`+
|
|
31
|
+
`$errors=@();foreach($case in @(@{kind='process';cfg=@{ProcessPolicyFile=${quote(processPolicy)}}},@{kind='coding';cfg=@{CodingPolicyFile=${quote(coding)}}})){try{if($case.kind -eq 'process'){Get-ProcessPolicyMetadata $case.cfg|Out-Null}else{Get-CodingPolicyMetadata $case.cfg|Out-Null};throw 'unexpected pass'}catch{$errors+=$_.Exception.Message}};$errors|ConvertTo-Json -Compress`;
|
|
32
|
+
const result=spawnSync('powershell',['-NoLogo','-NoProfile','-NonInteractive','-Command',script],{encoding:'utf8'});
|
|
33
|
+
assert.equal(result.status,0,result.stderr);
|
|
34
|
+
const errors=JSON.parse(result.stdout.trim());
|
|
35
|
+
assert.equal(errors.length,2);
|
|
36
|
+
assert.match(errors[0],/process policy root 'project' missing/);
|
|
37
|
+
assert.match(errors[1],/coding policy root 'project' missing/);
|
|
38
|
+
}finally{await rm(root,{recursive:true,force:true});}
|
|
39
|
+
});
|
|
@@ -17,5 +17,8 @@ test('R21.1 binds controller execution to CoreRoot controller directory', async
|
|
|
17
17
|
|
|
18
18
|
test('R21.1 validates controller root before creating command runtime directories', async () => {
|
|
19
19
|
const source = await readFile(controller, 'utf8');
|
|
20
|
-
|
|
20
|
+
const dispatch = source.slice(source.indexOf('function Run-CommandCore('), source.indexOf('function Run-Command(', source.indexOf('function Run-CommandCore(')));
|
|
21
|
+
assert.match(dispatch, /\$cfg = Get-Config\s+Assert-ControllerRootBound \$cfg\s+\$readOnly = \$Name -in/);
|
|
22
|
+
assert.match(dispatch, /if \(-not \$readOnly\) \{ Ensure-Directory \$RuntimeDir; Ensure-Directory \$LogDir \}/);
|
|
23
|
+
assert.ok(dispatch.indexOf('Assert-ControllerRootBound $cfg') < dispatch.indexOf('Ensure-Directory $RuntimeDir'));
|
|
21
24
|
});
|
package/test/controller.test.mjs
CHANGED
|
@@ -12,7 +12,8 @@ test('controller derives runtime state from observed identities and readiness',
|
|
|
12
12
|
assert.match(source, /'OFFLINE'/);
|
|
13
13
|
assert.match(source, /'PARKED'/);
|
|
14
14
|
assert.match(source, /'ONLINE'/);
|
|
15
|
-
assert.match(source, /'
|
|
15
|
+
assert.match(source, /'STARTING'/);
|
|
16
|
+
assert.match(source, /'FAILED'/);
|
|
16
17
|
assert.doesNotMatch(source, /state\.txt/i);
|
|
17
18
|
});
|
|
18
19
|
|