// #656/1745 STATIC-only authored tests; no measured outcome until separately cleared. // Generic fallback retains its current behavior; absent cache is NOT a universal refusal. import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import { dirname, join } from 'node:path'; import { revocationFixture, outcome, id } from './model-revocation-fixture.mjs'; import { Credentials } from '../src/credentials.mjs'; const timeout=30000; async function prepared(f){f.seed({matchingTmp:false});await f.prepare();} async function finished(f){await prepared(f);f.complete();} function proof(f){assert.equal(f.proofs.length,1,'real controller minted exact opaque model completion proof');return f.proofs[0];} test('656 generic remove after exact completion skips only redundant model unlink and keeps metadata',{timeout},async t=>{ const f=await revocationFixture(t);await finished(f);const before=f.credentials.status(),count=f.counts.unlinkFinal; const result=f.credentials.remove(f.instance);assert.equal(f.counts.unlinkFinal,count,'generic remove does not perform SECOND model unlink after exact completion');assert.deepEqual(result.services,['model']);assert.deepEqual(f.credentials.status(),before,'reference date and revoked status metadata unchanged');assert.equal(f.exists(f.file)||f.exists(f.temporary),false);f.valueFree(); }); for(const entry of ['model','model.tmp'])test('656 '+entry+' appearing after completed proof is preserved by unchanged unknown fence',{timeout},async t=>{ const f=await revocationFixture(t);await finished(f);const file=join(dirname(f.file),entry),body='owned unrecognized\n';fs.writeFileSync(file,body,{mode:0o600,flag:'wx'});const count=f.counts.unlinkFinal,result=await outcome(()=>f.credentials.remove(f.instance)); assert.equal(f.counts.unlinkFinal,count,'finished proof never authorizes another model pathname unlink');assert.equal(result.ok,false);assert.equal(result.code,'credential-file');assert.equal(id(fs.readFileSync(file)),id(body),'postcompletion unknown entry preserved');f.valueFree(); }); for(const state of ['valid','revoking'])test('656 '+state+' cached unconfirmed model keeps existing generic removal guard',{timeout},async t=>{ const f=await revocationFixture(t);f.seed({matchingTmp:false,valid:state==='valid'});const before=id(fs.readFileSync(f.file)),count=f.counts.unlinkFinal,result=await outcome(()=>f.credentials.remove(f.instance));assert.equal(result.ok,false);assert.equal(result.code,'credential-revoke-failed');assert.equal(f.counts.unlinkFinal,count);assert.equal(id(fs.readFileSync(f.file)),before);assert.equal(f.record()?.state,state);f.valueFree(); }); test('656 unconfirmed prepared model still cannot use finished-proof skip',{timeout},async t=>{ const f=await revocationFixture(t);await prepared(f);const count=f.counts.unlinkFinal,result=await outcome(()=>f.credentials.remove(f.instance));assert.equal(result.ok,false);assert.equal(result.code,'credential-revoke-failed');assert.equal(f.counts.unlinkFinal,count);assert.equal(f.record()?.state,'revoking');f.valueFree(); }); test('656 no-proof absent-cache model retains existing generic file removal behavior',{timeout},async t=>{ const f=await revocationFixture(t);f.seed({matchingTmp:false});f.credentials.hold(f.instance,'model');const count=f.counts.unlinkFinal,result=f.credentials.remove(f.instance);assert.deepEqual(result.services,['model']);assert.equal(f.counts.unlinkFinal-count,1,'ordinary generic no-cache behavior remains allowed');assert.equal(f.exists(f.file),false);f.valueFree(); }); test('656 no-record no-proof generic remove retains existing ENOENT behavior',{timeout},async t=>{ const f=await revocationFixture(t),count=f.counts.unlinkFinal,result=f.credentials.remove(f.instance);assert.deepEqual(result.services,['model']);assert.equal(f.counts.unlinkFinal-count,1,'no proof does not invent completed-proof skip');f.valueFree(); }); for(const action of ['register','unregister','reload','hold','claim','revoke','rewrite','close'])test('656 '+action+' invalidates latest completed proof without granting fallback authority',{timeout},async t=>{ const f=await revocationFixture(t);await finished(f);const token=proof(f),c=f.credentials;let signal; if(action==='register')c.register(f.instance,'model',{store:true,file:f.file,rotateBy:'2099-01-01'}); if(action==='unregister')c.unregister(f.instance,'model'); if(action==='reload'){const result=await outcome(()=>c.reload(f.instance,'model','2099-01-01'));assert.equal(result.ok,false);assert.equal(result.code,'credential-file');} if(action==='hold')c.hold(f.instance,'model'); if(action==='claim')signal=c.claim(f.instance,'model'); if(action==='revoke')c.revoke(f.instance,'model'); if(action==='rewrite')f.seed({matchingTmp:false,valid:true}); if(action==='close')c.close(); const result=await outcome(()=>c.finishModelRevocation(f.instance,token));assert.equal(result.ok,false);assert.equal(result.code,'credential-revoke-failed');if(signal)c.release(f.instance,'model',signal);if(action==='rewrite')assert.equal(f.exists(f.file),true);f.valueFree(); }); test('656 new snapshot invalidates old token even if later capture refuses on absent final',{timeout},async t=>{ const f=await revocationFixture(t);await finished(f);const token=proof(f),capture=await outcome(()=>f.credentials.captureModelRevocation(f.instance));assert.equal(capture.ok,false);assert.equal(capture.code,'credential-revoke-failed');const result=await outcome(()=>f.credentials.finishModelRevocation(f.instance,token));assert.equal(result.ok,false);assert.equal(result.code,'credential-revoke-failed');f.valueFree(); }); test('656 wrong instance cannot reuse a completed token',{timeout},async t=>{ const f=await revocationFixture(t);await finished(f);const token=proof(f),count=f.counts.unlinkFinal,result=await outcome(()=>f.credentials.finishModelRevocation('demo/other',token));assert.equal(result.ok,false);assert.equal(result.code,'credential-revoke-failed');assert.equal(f.counts.unlinkFinal,count);f.credentials.finishModelRevocation(f.instance,token);assert.equal(f.counts.unlinkFinal,count);f.valueFree(); }); test('656 nonmodel memory-first failure and retry remain unchanged after model completion',{timeout},async t=>{ const f=await revocationFixture(t);f.seed({matchingTmp:false});const file=join(dirname(f.file),'gitea'),value='fixture-gitea-key';f.remember(value);f.credentials.register(f.instance,'gitea',{store:true,file,rotateBy:'2099-01-01'});f.credentials.issue(f.instance,'gitea',value,'2099-01-01');await f.prepare();f.complete();const count=f.counts.unlinkFinal;f.faults.unlinkService=true; const failure=await outcome(()=>f.credentials.remove(f.instance));assert.equal(f.counts.unlinkFinal,count,'nonmodel failure does not repeat model unlink');assert.equal(failure.ok,false);assert.equal(failure.code,'credential-file');assert.equal(f.credentials.status().find(s=>s.instance===f.instance&&s.service==='gitea').state,'revoked','nonmodel keeps memory-first semantics');assert.equal(f.exists(file),true,'failed nonmodel unlink remains honestly present');f.faults.unlinkService=false; const result=f.credentials.remove(f.instance);assert.deepEqual([...result.services].sort(),['gitea','model']);assert.equal(f.counts.unlinkFinal,count);assert.equal(f.counts.unlinkService,2,'one failed then one successful service unlink; model remains completed');f.valueFree(); }); test('656 replaced private agent directory cannot reuse exact historical completion proof',{timeout},async t=>{ const f=await revocationFixture(t);await finished(f);const token=proof(f),directory=dirname(f.file);fs.renameSync(directory,join(f.root,'former-owned-agent-dir'));fs.mkdirSync(directory,{mode:0o700});const count=f.counts.unlinkFinal,result=await outcome(()=>f.credentials.finishModelRevocation(f.instance,token));assert.equal(result.ok,false);assert.equal(result.code,'credential-revoke-failed');assert.equal(f.counts.unlinkFinal,count);f.valueFree(); }); test('656 another Credentials epoch cannot use a former objects completed token',{timeout},async t=>{ const f=await revocationFixture(t);await finished(f);const token=proof(f);let other;t.after(()=>other?.close());other=new Credentials({storeRoot:f.storeRoot,dataRoot:f.dataRoot,modelAgents:[f.instance]});const count=f.counts.unlinkFinal,result=await outcome(()=>other.finishModelRevocation(f.instance,token));assert.equal(result.ok,false);assert.equal(result.code,'credential-revoke-failed');assert.equal(f.counts.unlinkFinal,count);f.credentials.finishModelRevocation(f.instance,token);assert.equal(f.counts.unlinkFinal,count);f.valueFree(); });