| 1 | // Real Node host/wire; fake Core authority. Production cleanup regression receipts. |
| 2 | import test from 'node:test' |
| 3 | import assert from 'node:assert/strict' |
| 4 | import { mkdtempSync, writeFileSync, rmSync } from 'node:fs' |
| 5 | import { tmpdir } from 'node:os' |
| 6 | import { join } from 'node:path' |
| 7 | import { startHost, owner, sha256File } from './harness.mjs' |
| 8 | function entry(dir, name, source) { const path = join(dir, name + '.mjs'); writeFileSync(path, source); return { path, sha256: sha256File(path) } } |
| 9 | async function setup(t) { const dir = mkdtempSync(join(tmpdir(), 'cw-activation-cleanup-')); const host = await startHost(); t.after(async () => { await host.stop(); rmSync(dir, {recursive:true,force:true}) }); return {dir,host,ref:owner('native-cleanup')} } |
| 10 | const scoped = (ref, path) => ({owner:ref,plugin_name:'native-cleanup',entry:path,scope:path,config:{}}) |
| 11 | |
| 12 | test('scoped failure preserves original refusal and a working sibling', async t => { |
| 13 | const {dir,host,ref}=await setup(t) |
| 14 | const good=entry(dir,'good',`export const inject=['tools']; export function apply(ctx){ctx.tools.register({name:'sibling',description:'live',parameters:{type:'object'},execute:()=> 'sibling live'})}`) |
| 15 | const bad=entry(dir,'bad',`export function apply(ctx){ctx.provide('approval',{})}`) |
| 16 | assert.equal((await host.call('ext/activate',scoped(ref,good))).status,'ok') |
| 17 | const result=await host.call('ext/activate',scoped(ref,bad)); assert.equal(result.status,'failed'); assert.match(result.diagnostic,/approval/) |
| 18 | assert.deepEqual(await host.call('ext/deactivate',{owner:ref,entry:bad}),{disposed:true,leaked:[]}) |
| 19 | const registration=host.registry.find(row=>row.op==='register' && row.spec.name==='sibling') |
| 20 | const answer=await host.call('tool/call',{handle:registration.handle,call_id:'sibling-call',input:{},deadline_ms:1000}) |
| 21 | assert.equal(answer.content[0].text,'sibling live') |
| 22 | }) |
| 23 | test('failed scoped activation retains incomplete disposer for teardown receipt', {timeout:15000}, async t => { |
| 24 | const {dir,host,ref}=await setup(t) |
| 25 | const bad=entry(dir,'dirty',`export function apply(ctx){ctx.effect(()=>()=>new Promise(()=>{}),'unfinished failed activation');ctx.provide('approval',{})}`) |
| 26 | const result=await host.call('ext/activate',scoped(ref,bad)); assert.equal(result.status,'failed') |
| 27 | const receipt=await host.call('ext/deactivate',{owner:ref,entry:bad}); assert.equal(receipt.disposed,false,'a missing owner must not manufacture clean teardown') |
| 28 | assert.deepEqual(await host.call('ext/deactivate',{owner:ref}),{disposed:true,leaked:[]}) |
| 29 | }) |
| 30 | test('withdrawal during async activation cannot return late success', async t => { |
| 31 | const {dir,host,ref}=await setup(t) |
| 32 | const late=entry(dir,'late',`export async function apply(ctx){ctx.logger.info('late activation reached await');await new Promise(resolve=>setTimeout(resolve,100));}`) |
| 33 | const begun=host.request('ext/activate',scoped(ref,late)) |
| 34 | await host.waitFor(message=>message.method==='log' && JSON.stringify(message.params).includes('late activation reached await')) |
| 35 | await host.call('ext/deactivate',{owner:ref,entry:late}) |
| 36 | const answer=await begun.promise; assert.equal(answer.status,'failed'); assert.match(answer.diagnostic,/withdrawn/) |
| 37 | }) |
| 38 |