Files
stack/agents/darkwing/work/queue-188/build.patch
T
jason.woltje 158205c5cc lead: commit darkwing's untracked evidence (348 files: queue-137 queue-141 queue-150 queue-162 queue-165 queue-166 queue-167 queue-169 queue-179 queue-180 queue-181 queue-182 queue-185 queue-186 queue-187 queue-188 queue-190 queue-93)
Unclaimed files left in the canonical checkout. Inventory: canonical-cleanup-inventory.md (commit-as-evidence groups). detect-secrets scan clean on 2026-11-02: hits are sha256 pins, git object ids, test sentinels already in tracked tests and unit-file lines.
2026-11-02 15:05:14 -06:00

86 lines
6.0 KiB
Diff

diff --git a/packages/cli/tests/stack-release.test.mjs b/packages/cli/tests/stack-release.test.mjs
index 1a2d5c5ba..b5f9c2119 100644
--- a/packages/cli/tests/stack-release.test.mjs
+++ b/packages/cli/tests/stack-release.test.mjs
@@ -4,7 +4,7 @@
import test, {after} from 'node:test';
import assert from 'node:assert/strict';
import {execFileSync,spawn,spawnSync} from 'node:child_process';
-import {mkdtempSync,mkdirSync,writeFileSync,readFileSync,appendFileSync,cpSync,rmSync,chmodSync,existsSync,statSync,lstatSync,symlinkSync,readdirSync,realpathSync} from 'node:fs';
+import {mkdtempSync,mkdirSync,writeFileSync,readFileSync,appendFileSync,cpSync,rmSync,chmodSync,existsSync,statSync,lstatSync,symlinkSync,linkSync,readdirSync,realpathSync} from 'node:fs';
import {createServer} from 'node:http';
import {createHash} from 'node:crypto';
import {DatabaseSync} from 'node:sqlite';
@@ -1449,9 +1449,9 @@ print(n,flush=True)`;
// A subtest that fails before noLeak leaves its watches running; they end with the file.
const WATCHERS=new Set();
after(()=>{for(const ch of WATCHERS)ch.kill('SIGKILL')});
-// A proof file under e.root, outside the data root, with its read watch running.
-async function proofFile(e,name='proof.187',mode=0o600){
- const f=join(e.root,name);writeFileSync(f,SENTINEL+'\n',{mode});chmodSync(f,mode);
+// A proof file under dir (e.root, outside the data root, unless given), with its read watch running.
+async function proofFile(e,name='proof.187',mode=0o600,dir=e.root){
+ const f=join(dir,name);writeFileSync(f,SENTINEL+'\n',{mode});chmodSync(f,mode);
const ch=spawn('python3',['-I','-c',WATCH,f],{stdio:['pipe','pipe','inherit']});
let out='';ch.stdout.on('data',(d)=>{out+=d});
await new Promise((res,rej)=>{ch.stdout.on('data',()=>{if(out.startsWith('ready\n'))res()});ch.on('error',rej);ch.on('close',(c)=>rej(new Error(`inotify watch exited ${c}`)))});
@@ -1650,3 +1650,58 @@ test('T53 row 187: activate sets, carries and drops the proof pair',async(t)=>{
await noLeak(e);
});
});
+
+// Row 188 (#1686, P19 409 and 410): the checks on a carried pair, and a proof file inside REPO.
+// A file gone from its path keeps a hard link under e.root, so the inode and its read watch
+// outlive the path: inotify sends IN_IGNORED, which the watch would count, when a watched inode goes.
+const carried=['proof: carried, header x-console-proof'];
+test('T54 row 188: a carried pair gets the proof file checks, and a proof file inside the repository refuses',async(t)=>{
+ const changes=[
+ ['chmod 644',(e,f)=>{chmodSync(f,0o644);return 'has mode 644, not 600 or 400'}],
+ ['a symlink to a mode-600 file',async(e,f)=>{const g=await proofFile(e,'swap.188');linkSync(f,join(e.root,'kept.188'));rmSync(f);symlinkSync(g,f);return 'is a symlink'}],
+ ['removal',(e,f)=>{linkSync(f,join(e.root,'kept.188'));rmSync(f);return 'cannot be found'}],
+ ];
+ for(const [name,change] of changes){
+ await t.test(`test 1: an activate that carries the pair refuses after ${name}, in the plan and the run, before anything stops`,async(t)=>{
+ const e=await mk(t),base=await seedPreview(e),f=await proofFile(e);
+ const r=await prun(e,'activate',B,'acme',...pair(f));assert.equal(r.code,0,r.out+r.err);
+ const why=`--proof-file ${f} ${await change(e,f)}`;
+ const out=await planOnly(e,3,'activate',C1,'acme');
+ assert.deepEqual(proofLines(out),carried);assert.deepEqual(out.filter((l)=>l.startsWith('refuses ')),[`refuses proof-file-invalid: ${why}`]);
+ await refused(e,['activate',C1,'acme'],'proof-file-invalid',why);
+ assert.equal(pointer(e).copy,B);assert.deepEqual(pointer(e).console,[...base,...pair(f)]);
+ await noLeak(e);
+ });
+ }
+ await t.test('test 2: a rollback that carries the pair refuses after chmod 644, in the plan and the run, before anything stops',async(t)=>{
+ const e=await mk(t),base=await seedPreview(e),f=await proofFile(e);
+ const r=await prun(e,'activate',C1,'acme',...pair(f));assert.equal(r.code,0,r.out+r.err);
+ chmodSync(f,0o644);const why=`--proof-file ${f} has mode 644, not 600 or 400`;
+ const out=await planOnly(e,3,'rollback','acme');
+ assert.ok(out.includes(`to: ${B}`),out.join('\n'));
+ assert.deepEqual(proofLines(out),carried);assert.deepEqual(out.filter((l)=>l.startsWith('refuses ')),[`refuses proof-file-invalid: ${why}`]);
+ await refused(e,['rollback','acme'],'proof-file-invalid',why);
+ assert.equal(pointer(e).copy,C1);assert.deepEqual(pointer(e).console,[...base,...pair(f)]);
+ await noLeak(e);
+ });
+ // Tests 3 and 4: a mode-600 file of this uid, so only the realpath check can refuse it.
+ const inRepo=async(t,e,dir)=>{t.after(()=>rmSync(dir,{recursive:true,force:true}));mkdirSync(dir);return proofFile(e,'proof.188',0o600,dir)};
+ await t.test('test 3: a proof file inside the repository refuses, in the plan and the run, before anything stops',async(t)=>{
+ const e=await mk(t);await seedPreview(e);const f=await inRepo(t,e,join(repo,'proof-r188'));
+ const why=`--proof-file ${f} is inside the repository or the copies`;
+ const out=await planOnly(e,3,'activate',C1,'acme',...pair(f));
+ assert.deepEqual(out.filter((l)=>l.startsWith('refuses ')),[`refuses proof-file-invalid: ${why}`]);
+ await refused(e,['activate',C1,'acme',...pair(f)],'proof-file-invalid',why);
+ await noLeak(e);
+ });
+ await t.test('test 4: a path outside the repository through a directory symlink into it refuses on its realpath',async(t)=>{
+ const e=await mk(t);await seedPreview(e);await inRepo(t,e,join(repo,'proof-r188'));
+ const link=join(e.root,'repo-link.188');symlinkSync(join(repo,'proof-r188'),link);
+ const f=join(link,'proof.188');assert.ok(!lstatSync(f).isSymbolicLink());assert.ok(!f.startsWith(`${repo}/`));
+ const why=`--proof-file ${f} is inside the repository or the copies`;
+ const out=await planOnly(e,3,'activate',C1,'acme',...pair(f));
+ assert.deepEqual(out.filter((l)=>l.startsWith('refuses ')),[`refuses proof-file-invalid: ${why}`]);
+ await refused(e,['activate',C1,'acme',...pair(f)],'proof-file-invalid',why);
+ await noLeak(e);
+ });
+});