import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import {execFile} from 'node:child_process'; import {promisify} from 'node:util'; import {nativePatchPreflight} from '../scripts/native-patch-preflight.mjs'; const exec=promisify(execFile); async function fixture(t){ const root=await fs.mkdtemp(path.join(os.tmpdir(),'safetynet-native-preflight-'));t.after(()=>fs.rm(root,{recursive:true,force:true})); const config=path.join(root,'config.json'),patchDir=path.join(root,'patches');await fs.mkdir(patchDir);await fs.writeFile(path.join(patchDir,'.patches'),'one.patch\n');await fs.writeFile(path.join(patchDir,'one.patch'),'synthetic'); await fs.writeFile(config,JSON.stringify([{repo:'src',patch_dir:'patches'}])); return {root,config,options:{checkoutRoot:root,configPath:config}}; } async function init(repo){await fs.mkdir(repo,{recursive:true});await exec('git',['init',repo]);await fs.writeFile(path.join(repo,'tracked.txt'),'baseline');await exec('git',['-C',repo,'add','tracked.txt']);await exec('git',['-C',repo,'-c','user.name=Fixture','-c','user.email=fixture@example.invalid','-c','commit.gpgsign=false','commit','-m','baseline']);} test('accepts an independent clean checkout and leaves HEAD unchanged',async t=>{ const f=await fixture(t),repo=path.join(f.root,'src');await init(repo);const before=(await exec('git',['-C',repo,'rev-parse','HEAD'])).stdout.trim();const r=await nativePatchPreflight(f.options);assert.equal(r.passed,true);assert.equal(r.required[0].head,before);assert.equal(r.required[0].patchCount,1);assert.equal((await exec('git',['-C',repo,'rev-parse','HEAD'])).stdout.trim(),before); }); test('rejects existing child directory whose Git resolves to ancestor',async t=>{ const f=await fixture(t);await init(f.root);await fs.mkdir(path.join(f.root,'src'));const r=await nativePatchPreflight(f.options);assert.equal(r.passed,false);assert.deepEqual(r.errors[0].errors,['missing-own-git-marker']); }); test('missing required target fails even when patch directory exists',async t=>{ const f=await fixture(t);const r=await nativePatchPreflight(f.options);assert.equal(r.passed,false);assert.equal(r.requiredCount,1);assert.deepEqual(r.errors[0].errors,['missing-required-path']); }); test('rejects changed tracked files',async t=>{ const f=await fixture(t);await init(path.join(f.root,'src'));await fs.writeFile(path.join(f.root,'src/tracked.txt'),'changed');const r=await nativePatchPreflight(f.options);assert.equal(r.passed,false);assert.deepEqual(r.errors[0].errors,['tracked-worktree-not-clean']); }); test('rejects residual git-am state despite clean tracked files',async t=>{ const f=await fixture(t),repo=path.join(f.root,'src');await init(repo);await fs.mkdir(path.join(repo,'.git/rebase-apply'));const r=await nativePatchPreflight(f.options);assert.equal(r.passed,false);assert.deepEqual(r.errors[0].errors,['git-am-in-progress']);assert.equal((await exec('git',['-C',repo,'status','--porcelain=v1','--untracked-files=no'])).stdout.trim(),''); }); test('rejects checkout symlink or junction escaping supplied root',async t=>{ const f=await fixture(t),outside=await fs.mkdtemp(path.join(os.tmpdir(),'safetynet-preflight-outside-'));t.after(()=>fs.rm(outside,{recursive:true,force:true}));await init(outside);await fs.symlink(outside,path.join(f.root,'src'),process.platform==='win32'?'junction':'dir');const r=await nativePatchPreflight(f.options);assert.equal(r.passed,false);assert.deepEqual(r.errors[0].errors,['path-outside-checkout']); }); test('rejects escaping config and missing listed patch',async t=>{ const f=await fixture(t);await init(path.join(f.root,'src'));await fs.rm(path.join(f.root,'patches/one.patch'));assert.equal((await nativePatchPreflight(f.options)).passed,false);const r=await nativePatchPreflight({...f.options,configPath:path.join(f.root,'../outside.json')});assert.equal(r.passed,false); }); test('fontconfig exclusion is opt-in, Windows-only, exact path and DEPS condition checked',async t=>{ const f=await fixture(t);await init(path.join(f.root,'src'));await fs.mkdir(path.join(f.root,'src/third_party/fontconfig/src'),{recursive:true});await fs.writeFile(path.join(f.root,'src/DEPS'),"deps = {'src/third_party/fontconfig/src': {'url':'fixture','condition':'checkout_linux'}}");await fs.writeFile(f.config,JSON.stringify([{repo:'src/third_party/fontconfig/src',patch_dir:'patches'}])); assert.equal((await nativePatchPreflight(f.options)).passed,false); const options={...f.options,excludeWindowsFontconfig:true,platform:'win32'};const good=await nativePatchPreflight(options);assert.equal(good.passed,true);assert.equal(good.excludedCount,1);assert.equal(good.requiredCount,0); assert.equal((await nativePatchPreflight({...options,platform:'linux'})).passed,false); await fs.writeFile(path.join(f.root,'src/DEPS'),"deps={'src/third_party/fontconfig/src':{'condition':'checkout_win'}}");assert.equal((await nativePatchPreflight(options)).passed,false); }); test('rejects listed patch traversal outside its patch directory',async t=>{ const f=await fixture(t);await init(path.join(f.root,'src'));await fs.writeFile(path.join(f.root,'other.patch'),'synthetic');await fs.writeFile(path.join(f.root,'patches/.patches'),'../other.patch\n');const r=await nativePatchPreflight(f.options);assert.equal(r.passed,false);assert.deepEqual(r.errors[0].errors,['patch-outside-patch-directory']); }); test('CLI requires explicit roots and returns failing status',async()=>{ await assert.rejects(exec(process.execPath,['scripts/native-patch-preflight.mjs']),error=>error.code===1&&JSON.parse(error.stdout).passed===false); });