packages/plugins holds the kind registry and the loader for in-repo plug-in modules (REQ-PLUG-1). The first two kinds are usage-source and limit-check (REQ-LIMIT-4). The business file's plugins key turns modules on per kind, through plugin-settings.mjs and step 5's write path (REQ-ROLE-3). A refused load is logged and keeps the kind's old modules. Any filesystem error other than ENOENT or ENOTDIR refuses as unreadable. The organization writer validates the bytes it writes as well as the document. stack-copy ships packages/plugins. Nothing is wired into a host process yet. Candidate 466c835f7dce79d20c7ab74c197a52b70e7fdeb0efae0456222dc6d0a633f219, Dewey packet80797a539(round 2), base0bdd573ff. Filbert approved round 2 in 28568 and Darkwing in 28570. Landing gate atf7d249634: 24 suites green, 0 failed. 14 package suites passed 1586 tests (plugins 21, business 92, bus 171, cli 195, webui 112). The 10 script suites passed. Follow-up: P19 task 293, coded writer refusals. Co-Authored-By: Claude Opus 5.5 <[email protected]>
213 lines
21 KiB
JavaScript
213 lines
21 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {execFileSync,spawnSync,spawn} from 'node:child_process';
|
|
import {symlinkSync,unlinkSync,mkdtempSync,mkdirSync,writeFileSync,readFileSync,cpSync,rmSync,chmodSync,existsSync,statSync} from 'node:fs';
|
|
import {tmpdir} from 'node:os';
|
|
import {join,dirname} from 'node:path';
|
|
import {fileURLToPath} from 'node:url';
|
|
import {once} from 'node:events';
|
|
import {pathToFileURL} from 'node:url';
|
|
import {fixture,notifyConfig,BUSINESSES} from './helpers.mjs';
|
|
const ROOT=fileURLToPath(new URL('../../..',import.meta.url)).replace(/\/$/,'');
|
|
const git=(cwd,...args)=>execFileSync('git',['-c','user.name=fixture','-c','user.email=fixture@invalid','-c','commit.gpgsign=false','-c','core.hooksPath=/dev/null','-C',cwd,...args],{encoding:'utf8'}).trim();
|
|
export function setup(t, realPin=false){
|
|
const root=mkdtempSync(join(tmpdir(),'stack-copy-')),repo=join(root,'repo'),copies=join(root,'copies'),bin=join(root,'bin');
|
|
t.after(()=>{execFileSync('chmod',['-R','u+w',root]);rmSync(root,{recursive:true,force:true})});
|
|
mkdirSync(repo);mkdirSync(bin);
|
|
const tracked=execFileSync('git',['-C',ROOT,'ls-files','-co','--exclude-standard','--','scripts','packages','adapters','roles','RELEASE','package.json','package-lock.json',':!packages/*/tests',':!scripts/foundation'],{encoding:'utf8'}).trim().split('\n');
|
|
for(const f of tracked)if(existsSync(join(ROOT,f)))cpSync(join(ROOT,f),join(repo,f));
|
|
// Lightweight npm-ci fixture; real pin/host/proof integration is tested separately.
|
|
const pkg={name:'copy-fixture',version:'1.0.0'};
|
|
if(!realPin){writeFileSync(join(repo,'package.json'),JSON.stringify(pkg));
|
|
writeFileSync(join(repo,'package-lock.json'),JSON.stringify({name:pkg.name,version:pkg.version,lockfileVersion:3,packages:{'':pkg}}));}
|
|
else {for(const f of ['package.json','package-lock.json'])cpSync(join(ROOT,f),join(repo,f));}
|
|
git(repo,'init','-q','-b','refactor');git(repo,'config','gc.auto','0');git(repo,'add','.');git(repo,'commit','-qm','fixture');
|
|
const id=git(repo,'rev-parse','HEAD');git(repo,'update-ref','refs/remotes/origin/refactor',id);
|
|
const env={PATH:`${bin}:${dirname(process.execPath)}:/usr/bin:/bin`,HOME:root,XDG_DATA_HOME:join(root,'data'),XDG_CONFIG_HOME:join(root,'config')};
|
|
const search=join(root,'unit-search');mkdirSync(search);
|
|
const analyze=body=>{writeFileSync(join(bin,'systemd-analyze'),'#!/bin/sh\n'+body);chmodSync(join(bin,'systemd-analyze'),0o755);};
|
|
analyze(`printf '%s\\n' '${search}'\n`);
|
|
const run=(script,args=[])=>spawnSync(join(repo,'scripts',script),args,{env,encoding:'utf8',timeout:30000});
|
|
const fake=body=>{writeFileSync(join(bin,'systemctl'),'#!/bin/bash\n'+body);chmodSync(join(bin,'systemctl'),0o755)};
|
|
return {root,repo,copies,bin,id,env,run,fake,search,analyze};
|
|
}
|
|
test('shared copy: npm ci, pushed reachability, readonly/existing copy, webui delegation',t=>{
|
|
const s=setup(t),r=s.run('stack-copy.sh',['copy',s.id,'--copies',s.copies]);assert.equal(r.status,0,r.stderr);
|
|
const copy=join(s.copies,s.id);assert.equal(statSync(copy).mode&0o777,0o555);
|
|
for(const f of ['scripts/mosaic','adapters/pi/adapter.sh','packages/queue/src/cli.mjs','package.json','package-lock.json','packages/tasks/requirements.json','packages/plugins/src/kinds.mjs','packages/plugins/src/loader.mjs'])assert.ok(existsSync(join(copy,f)),f);
|
|
const npm=join(s.bin,'npm');writeFileSync(npm,'#!/bin/sh\nexit 99\n');chmodSync(npm,0o755);
|
|
const existing=s.run('webui-service.sh',['copy',s.id,'--copies',s.copies]);assert.equal(existing.status,0);assert.equal(existing.stdout,`exists: ${copy}\n`);unlinkSync(npm);
|
|
git(s.repo,'update-ref','refs/remotes/origin/refactor',git(s.repo,'hash-object','-w','package.json'));
|
|
const denied=s.run('stack-copy.sh',['copy',s.id,'--copies',s.copies]);assert.notEqual(denied.status,0);assert.match(denied.stderr,/origin\/refactor/);
|
|
});
|
|
test('wrapper preserves Q81 lease and marker, rejects foreign/symlink; check uses effective roots',t=>{
|
|
const s=setup(t);assert.equal(s.run('stack-copy.sh',['copy',s.id,'--copies',s.copies]).status,0);
|
|
const wrapper=join(s.bin,'mosaic'),marker='# mosaic-cli wrapper (Q81, agents/sage/work/q81/mosaic-cli.sh)';
|
|
const lease=`[ "\${1:-}" = __lease-capability ] && exec "${s.bin}/mosaic-old" "$@"`;
|
|
writeFileSync(wrapper,`#!/usr/bin/env bash\n${marker}\n${lease}\nexec "/old/scripts/mosaic" "$@"\n`);
|
|
assert.equal(s.run('stack-copy.sh',['wrapper',s.id,'--copies',s.copies,'--bin',s.bin]).status,0);
|
|
const lines=readFileSync(wrapper,'utf8').trimEnd().split('\n');
|
|
assert.equal(lines[1],marker);
|
|
assert.equal(lines[2],`# CLI runs from pinned copy: ${join(s.copies,s.id)}`);
|
|
assert.equal(lines[3],'# The Q81 lease probe still answers at mosaic-old.');
|
|
assert.equal(lines.at(-1),`exec "${join(s.copies,s.id)}/scripts/mosaic" "$@"`);
|
|
assert.match(readFileSync(wrapper,'utf8'),new RegExp(s.id));assert.ok(readFileSync(wrapper,'utf8').includes(lease));
|
|
const old=join(s.bin,'mosaic-old');writeFileSync(old,'#!/bin/sh\nprintf \"legacy:%s\\n\" \"$1\"\n');chmodSync(old,0o755);
|
|
assert.equal(spawnSync(wrapper,['__lease-capability'],{env:s.env,encoding:'utf8'}).stdout,'legacy:__lease-capability\n');
|
|
const copy=join(s.copies,s.id);s.fake(`case "$*" in *mosaic-bus*) echo 'LoadState=loaded'; echo 'WorkingDirectory=${copy}'; echo 'ExecStart={ path=${copy}/scripts/mosaic ; argv[]=${copy}/scripts/mosaic bus start mosaic-stack --pm ; }';; *) echo 'LoadState=loaded'; echo 'WorkingDirectory=${copy}'; echo 'ExecStart={ path=/usr/bin/node ; argv[]=/usr/bin/node ${copy}/packages/webui/src/cli.mjs serve ; }';; esac\n`);
|
|
assert.equal(s.run('stack-copy.sh',['check','--copies',s.copies,'--bin',s.bin]).status,0);
|
|
s.fake(`echo 'LoadState=loaded'; echo 'WorkingDirectory=${copy}';case \"$*\" in *mosaic-bus*)echo 'ExecStart={ path=/other/scripts/mosaic ; argv[]=/other/scripts/mosaic bus start demo ; }';; *)echo 'ExecStart={ path=/usr/bin/node ; argv[]=/usr/bin/node ${copy}/packages/webui/src/cli.mjs serve ; }';;esac\n`);
|
|
assert.equal(s.run('stack-copy.sh',['check','--copies',s.copies,'--bin',s.bin]).status,1,'effective ExecStart mismatch');
|
|
s.fake(`echo 'LoadState=loaded'; echo 'WorkingDirectory=/other';\n`);assert.equal(s.run('stack-copy.sh',['check','--copies',s.copies,'--bin',s.bin]).status,1);
|
|
const foreign=`#!/usr/bin/env bash\n# foreign wrapper\n${lease}\nexec \"${copy}/scripts/mosaic\" \"$@\"\n`;writeFileSync(wrapper,foreign);
|
|
// Restore matching effective roots: only the foreign marker refuses check.
|
|
s.fake(`echo 'LoadState=loaded';echo 'WorkingDirectory=${copy}';case "$*" in *mosaic-bus*) echo 'ExecStart={ path=${copy}/scripts/mosaic ; argv[]=${copy}/scripts/mosaic bus start demo ; }';; *)echo 'ExecStart={ path=/usr/bin/node ; argv[]=/usr/bin/node ${copy}/packages/webui/src/cli.mjs serve ; }';;esac\n`);
|
|
assert.equal(s.run('stack-copy.sh',['check','--copies',s.copies,'--bin',s.bin]).status,1);
|
|
assert.notEqual(s.run('stack-copy.sh',['wrapper',s.id,'--copies',s.copies,'--bin',s.bin]).status,0);assert.equal(readFileSync(wrapper,'utf8'),foreign);
|
|
const backing=join(s.bin,'backing');writeFileSync(backing,`#!/usr/bin/env bash\n${marker}\n${lease}\nexec \"/old/scripts/mosaic\" \"$@\"\n`);unlinkSync(wrapper);symlinkSync(backing,wrapper);assert.notEqual(s.run('stack-copy.sh',['wrapper',s.id,'--copies',s.copies,'--bin',s.bin]).status,0);
|
|
});
|
|
test('bus unit copy/pm pins, install refuses all busy states and failed reads, drop-in refusal',t=>{
|
|
const s=setup(t);assert.equal(s.run('stack-copy.sh',['copy',s.id,'--copies',s.copies]).status,0);
|
|
const args=['--copy',s.id,'--copies',s.copies,'--pm'];
|
|
const render=s.run('bus-service.sh',['render',...args]);assert.equal(render.status,0,render.stderr);assert.match(render.stdout,new RegExp(`WorkingDirectory=${s.copies}/${s.id}`));assert.match(render.stdout,/bus start %i --pm/);
|
|
const units=join(s.root,'units');
|
|
for(const state of ['active','activating','deactivating','reloading']){s.fake(`echo '[email protected] loaded ${state} running fixture';\n`);const r=s.run('bus-service.sh',['install',...args,'--dir',units,'--no-reload']);assert.notEqual(r.status,0,state);assert.ok(!existsSync(units));}
|
|
s.fake('exit 1\n');assert.notEqual(s.run('bus-service.sh',['install',...args,'--dir',units,'--no-reload']).status,0);assert.ok(!existsSync(units));
|
|
const dropdir=join(s.search,'[email protected]');mkdirSync(dropdir);const drop=join(dropdir,'pm.conf');writeFileSync(drop,'[Service]\nExecStart=elsewhere\n');
|
|
s.fake(`case "$*" in *list-units*) echo '[email protected] loaded inactive dead fixture';; *)exit 99;; esac\n`);
|
|
const refused=s.run('bus-service.sh',['install',...args,'--dir',units,'--no-reload']);assert.notEqual(refused.status,0);assert.ok(refused.stderr.includes(drop));assert.ok(!existsSync(units));
|
|
writeFileSync(drop,'[Service]\nEnvironment=SAFE=1\n');assert.equal(s.run('bus-service.sh',['install',...args,'--dir',units,'--no-reload']).status,0);
|
|
});
|
|
|
|
|
|
test('missing package manifest refuses before npm; npm failure removes the unpublished temp copy', t=>{
|
|
const s=setup(t);git(s.repo,'rm','package.json');git(s.repo,'commit','-qm','missing package');const id=git(s.repo,'rev-parse','HEAD');git(s.repo,'update-ref','refs/remotes/origin/refactor',id);
|
|
const npm=join(s.bin,'npm'),called=join(s.root,'npm-called');writeFileSync(npm,`#!/bin/bash\necho called > '${called}'\nexit 1\n`);chmodSync(npm,0o755);
|
|
const r=s.run('stack-copy.sh',['copy',id,'--copies',s.copies]);assert.notEqual(r.status,0);assert.match(r.stderr,/package.json/);assert.ok(!existsSync(called));assert.ok(!existsSync(join(s.copies,id)));
|
|
git(s.repo,'reset','--hard',s.id);git(s.repo,'update-ref','refs/remotes/origin/refactor',s.id);
|
|
const tar=join(s.bin,'tar');writeFileSync(tar,'#!/bin/sh\n/usr/bin/tar \"$@\" || exit $?\nfor a; do dest=\"$a\"; done\nrm \"$dest/package.json\"\n');chmodSync(tar,0o755);
|
|
const absent=s.run('stack-copy.sh',['copy',s.id,'--copies',s.copies]);assert.notEqual(absent.status,0);assert.match(absent.stderr,/package.json.*missing/);assert.ok(!existsSync(called));unlinkSync(tar);
|
|
const fail=s.run('stack-copy.sh',['copy',s.id,'--copies',s.copies]);assert.notEqual(fail.status,0);assert.match(fail.stderr,/npm ci failed/);assert.ok(existsSync(called));assert.deepEqual(execFileSync('ls',['-A',s.copies],{encoding:'utf8'}),'');
|
|
});
|
|
|
|
|
|
async function until(fn,ms=10000){const end=Date.now()+ms;while(Date.now()<end){if(fn())return;await new Promise(r=>setTimeout(r,25));}throw Error('fixture startup timed out');}
|
|
async function childResult(command,args,env,input){
|
|
const p=spawn(command,args,{env,stdio:['pipe','pipe','pipe']});let stdout='',stderr='';
|
|
p.stdout.on('data',c=>stdout+=c);p.stderr.on('data',c=>stderr+=c);p.stdin.end(input);
|
|
const timer=setTimeout(()=>p.kill('SIGKILL'),15000);
|
|
try{const [status]=await once(p,'close');return {status,stdout,stderr};}finally{clearTimeout(timer)}
|
|
}
|
|
test('real pinned install loads bus and Console; same-copy mosaic talk passes proof, other trees refuse', {timeout:180000},async t=>{
|
|
const s=setup(t,true);
|
|
const copied=s.run('stack-copy.sh',['copy',s.id,'--copies',s.copies]);assert.equal(copied.status,0,copied.stderr);
|
|
const root=join(s.copies,s.id),url=f=>pathToFileURL(join(root,f)).href;
|
|
const {checkEnginePin}=await import(url('packages/conversation/src/pi-pin.mjs'));assert.equal(checkEnginePin(root).version,'0.85.1');
|
|
const f=fixture(join(s.root,'home'));notifyConfig(f.dataRoot,'acme',null);
|
|
// Only synthetic configuration, no inherited credentials/agent environment.
|
|
const env={...s.env,MOSAIC_CONFIG:f.config};
|
|
const host=spawn(join(root,'scripts/mosaic'),['bus','start','acme'],{env,stdio:['ignore','pipe','pipe']});let hostOut='',hostErr='';
|
|
host.stdout.on('data',c=>hostOut+=c);host.stderr.on('data',c=>hostErr+=c);
|
|
try{await until(()=>hostOut.includes('bus host up')||host.exitCode!==null);assert.match(hostOut,/bus host up/,hostErr);}
|
|
finally{if(host.exitCode===null&&host.signalCode===null){const done=once(host,'close');host.kill('SIGTERM');await done;}}
|
|
const web=spawn(process.execPath,[join(root,'packages/webui/src/cli.mjs'),'serve','--port','0','--business','acme','--board','http://127.0.0.1:9'],{env,stdio:['ignore','pipe','pipe']});let webOut='',webErr='';
|
|
web.stdout.on('data',c=>webOut+=c);web.stderr.on('data',c=>webErr+=c);
|
|
try{await until(()=>/Mosaic Console: http:/.test(webOut)||web.exitCode!==null);const port=webOut.match(/127\.0\.0\.1:(\d+)/)?.[1];assert.ok(port,webErr);assert.equal((await fetch(`http://127.0.0.1:${port}/healthz`)).status,200);}
|
|
finally{if(web.exitCode===null&&web.signalCode===null){const done=once(web,'close');web.kill();await done;}}
|
|
// The production runtime derives cliPath. Only agent ancestry is mocked;
|
|
// readProcess passes through for tester.mjs (row 110).
|
|
// real pid/start/argv/nonce and the runtime's entry-path binding remain intact.
|
|
const proofScript=join(s.root,'proof-runtime.mjs');
|
|
writeFileSync(proofScript,`
|
|
import {mock} from 'node:test';
|
|
const humanURL=${JSON.stringify(url('packages/bus/src/human.mjs'))};
|
|
const real=await import(humanURL);
|
|
mock.module(humanURL,{namedExports:{readProcess:real.readProcess,verifyHuman:(proof,options)=>real.verifyHuman(proof,{...options,readProcess:pid=>({...real.readProcess(pid),ppid:1})})}});
|
|
const {startBroker}=await import(${JSON.stringify(url('packages/bus/src/runtime.mjs'))});
|
|
const runtime=await startBroker({dataRoot:${JSON.stringify(f.dataRoot)},businesses:${JSON.stringify(BUSINESSES)}});
|
|
const broker=runtime.broker;
|
|
const pm=broker.bindLaunch({business:'demo',role:'pm',run:'pm-fixture',harness:'pi'});broker.request(pm,{verb:'role.claim'});
|
|
const request=broker.request.bind(broker);broker.request=(cap,r)=>{const result=request(cap,r);if(r.verb==='message.send'&&r.args.to==='pm')request(pm,{verb:'message.send',args:{to:'human',body:'same-copy reply',in_reply_to:result.id}});return result;};
|
|
process.on('SIGTERM',async()=>{await runtime.close();process.exit(0)});
|
|
console.log('proof runtime ready');
|
|
`);
|
|
const proof=spawn(process.execPath,['--experimental-test-module-mocks',proofScript],{env,stdio:['ignore','pipe','pipe']});let proofOut='',proofErr='';
|
|
proof.stdout.on('data',c=>proofOut+=c);proof.stderr.on('data',c=>proofErr+=c);
|
|
const socket=join(f.dataRoot,'bus','broker.sock');
|
|
try{
|
|
await until(()=>proofOut.includes('proof runtime ready')||proof.exitCode!==null);assert.match(proofOut,/proof runtime ready/,proofErr);
|
|
const result=await childResult(join(root,'scripts/mosaic'),['talk','pm','hello','--business','demo','--wait','1'],env);
|
|
assert.equal(result.status,0,result.stderr);assert.match(result.stdout,/sent .* to pm/);assert.match(result.stdout,/same-copy reply/);
|
|
for(const other of [ROOT,join(s.root,'other-copy')]){
|
|
if(other!==ROOT)cpSync(join(root,'packages'),join(other,'packages'),{recursive:true});
|
|
const result=await childResult(process.execPath,[join(other,'packages/bus/src/human-cli.mjs'),socket],env,JSON.stringify({business:'demo',verb:'inbox'})+'\n');
|
|
assert.notEqual(result.status,0);assert.equal(result.stderr.trim(),'human-required');
|
|
}
|
|
}finally{if(proof.exitCode===null&&proof.signalCode===null){const done=once(proof,'close');proof.kill('SIGTERM');await done;}}
|
|
});
|
|
|
|
|
|
test('bus status reads effective copy/pm/drop-ins without writes; check refuses an overridden ExecStart',t=>{
|
|
const s=setup(t);assert.equal(s.run('stack-copy.sh',['copy',s.id,'--copies',s.copies]).status,0);
|
|
const copy=join(s.copies,s.id),drop=join(s.root,'pm.conf');
|
|
const rows=`LoadState=loaded\nWorkingDirectory=${copy}\nExecStart={ path=${copy}/scripts/mosaic ; argv[]=${copy}/scripts/mosaic bus start demo --pm ; }\nDropInPaths=${drop}\n`;
|
|
s.fake(`case "$*" in *show*) cat <<'ROWS'\n${rows}ROWS\n;; *is-active*) echo inactive;; *is-enabled*) echo disabled;; *)exit 99;;esac\n`);
|
|
const status=s.run('bus-service.sh',['status','demo']);assert.equal(status.status,0,status.stderr);assert.match(status.stdout,new RegExp(`copy: ${s.id}`));assert.match(status.stdout,/pm: on/);assert.ok(status.stdout.includes(drop));
|
|
});
|
|
|
|
|
|
test('drop-ins in either override field and malformed metadata refuse; uninstall also checks --no-reload',t=>{
|
|
const s=setup(t),units=join(s.root,'units'),dropdir=join(s.search,'[email protected]');mkdirSync(dropdir);const drop=join(dropdir,'override.conf');
|
|
for(const field of ['ExecStart','WorkingDirectory']){
|
|
writeFileSync(drop,`[Service]\n${field}=override\n`);
|
|
s.fake(`case "$*" in *list-units*) echo '[email protected] loaded inactive dead fixture';; *)exit 99;;esac\n`);
|
|
const r=s.run('bus-service.sh',['install','--dir',units,'--no-reload']);assert.notEqual(r.status,0);assert.ok(r.stderr.includes(drop));assert.ok(!existsSync(units));
|
|
}
|
|
s.fake('exit 0\n');s.analyze('exit 0\n');const unknown=s.run('bus-service.sh',['install','--dir',units,'--no-reload']);assert.notEqual(unknown.status,0);assert.match(unknown.stderr,/unknown/);
|
|
mkdirSync(units);const file=join(units,'[email protected]');writeFileSync(file,'unchanged');
|
|
s.fake("echo '[email protected] loaded active running fixture'\n");assert.notEqual(s.run('bus-service.sh',['uninstall','--dir',units,'--no-reload']).status,0);assert.equal(readFileSync(file,'utf8'),'unchanged');
|
|
});
|
|
|
|
|
|
test('install never shows a template name on systemd 261; template drop-ins still refuse',t=>{
|
|
const s=setup(t),units=join(s.root,'units'),calls=join(s.root,'systemctl-calls');
|
|
s.fake(`echo "$*" >> '${calls}'\ncase "$*" in *show*[email protected]*) echo 'neither a valid invocation ID nor unit name' >&2; exit 1;; *list-units*) echo '[email protected] loaded inactive dead fixture';; *)exit 99;; esac\n`);
|
|
const result=s.run('bus-service.sh',['install','--dir',units,'--no-reload']);assert.equal(result.status,0,result.stderr);
|
|
assert.doesNotMatch(readFileSync(calls,'utf8'),/show mosaic-bus@\.service/);
|
|
// This rejection arm is deliberately exercised; it is not a dead show reply.
|
|
assert.equal(spawnSync(join(s.bin,'systemctl'),['--user','show','[email protected]'],{env:s.env}).status,1);
|
|
const dropdir=join(s.search,'[email protected]');mkdirSync(dropdir);const file=join(dropdir,'pm.conf');writeFileSync(file,'[Service]\nExecStart=override\n');
|
|
const before=readFileSync(join(units,'[email protected]'),'utf8');
|
|
const denied=s.run('bus-service.sh',['install','--dir',units,'--no-reload']);assert.notEqual(denied.status,0);assert.ok(denied.stderr.includes(file));assert.equal(readFileSync(join(units,'[email protected]'),'utf8'),before);
|
|
});
|
|
test('unlisted instance drop-in is found in another unit lookup directory; path discovery errors refuse',t=>{
|
|
const s=setup(t),units=join(s.root,'units'),second=join(s.root,'second-search');mkdirSync(second);
|
|
const dropdir=join(second,'[email protected]');mkdirSync(dropdir);const file=join(dropdir,'cwd.conf');writeFileSync(file,'[Service]\nWorkingDirectory=/other\n');
|
|
s.analyze(`printf '%s\\n' '${s.search}' '${second}'\n`);
|
|
s.fake("case \"$*\" in *list-units*) echo '[email protected] loaded inactive dead fixture';; *)exit 99;; esac\n");
|
|
const denied=s.run('bus-service.sh',['install','--dir',units,'--no-reload']);assert.notEqual(denied.status,0);assert.ok(denied.stderr.includes(file));assert.ok(!existsSync(units));
|
|
s.analyze('exit 1\n');const failed=s.run('bus-service.sh',['install','--dir',units,'--no-reload']);assert.notEqual(failed.status,0);assert.match(failed.stderr,/unit-paths failed/);assert.ok(!existsSync(units));
|
|
});
|
|
|
|
// Row 108: inherited drop-ins apply even without a loaded bus instance.
|
|
for(const name of ['mosaic-.service.d','service.d'])test(`inherited ${name} override refuses without changing the unit`,t=>{
|
|
const s=setup(t),units=join(s.root,'units');mkdirSync(units);
|
|
const target=join(units,'[email protected]');writeFileSync(target,'previous unit bytes');
|
|
s.fake('exit 0\n');
|
|
const dropdir=join(s.search,name);mkdirSync(dropdir);const file=join(dropdir,'override.conf');
|
|
for(const field of ['WorkingDirectory','ExecStart']){
|
|
writeFileSync(file,`[Service]\n${field}=/other\n`);
|
|
const denied=s.run('bus-service.sh',['install','--dir',units,'--no-reload']);
|
|
assert.equal(denied.status,1,denied.stderr);assert.ok(denied.stderr.includes(file));assert.equal(readFileSync(target,'utf8'),'previous unit bytes');
|
|
}
|
|
writeFileSync(file,'[Service]\nEnvironment=SAFE=1\n');
|
|
const allowed=s.run('bus-service.sh',['install','--dir',units,'--no-reload']);assert.equal(allowed.status,0,allowed.stderr);
|
|
});
|
|
test('inherited drop-in unreadable directory or file refuses before writing',t=>{
|
|
const s=setup(t),units=join(s.root,'units');mkdirSync(units);const target=join(units,'[email protected]');writeFileSync(target,'previous unit bytes');s.fake('exit 0\n');
|
|
const dropdir=join(s.search,'service.d');symlinkSync(join(s.root,'absent-dir'),dropdir);
|
|
const dir=s.run('bus-service.sh',['install','--dir',units,'--no-reload']);assert.equal(dir.status,1);assert.ok(dir.stderr.includes(dropdir));assert.equal(readFileSync(target,'utf8'),'previous unit bytes');
|
|
unlinkSync(dropdir);mkdirSync(dropdir);const file=join(dropdir,'broken.conf');symlinkSync(join(s.root,'absent-file'),file);
|
|
const broken=s.run('bus-service.sh',['install','--dir',units,'--no-reload']);assert.equal(broken.status,1);assert.ok(broken.stderr.includes(file));assert.equal(readFileSync(target,'utf8'),'previous unit bytes');
|
|
});
|