import {test} from 'node:test'; import assert from 'node:assert/strict'; import {createImportJob,validateImportJob} from '../../src/lib/canvas-runtime/imports.ts'; import {markImportFiles,importFiles,prepareImportFileRetention} from '../../src/lib/canvas-runtime/import-file-retention.ts'; const job={...createImportJob({id:'job',projectId:'P',sourceFingerprint:'source',parserVersion:1,source:{kind:'native-ndjson',fileId:'source',byteLength:10,targetCanvasId:'P'}}),status:'cancelled'},runtime={imports:new Map([['job',job]]),resources:new Map()},coverage=()=>({complete:true,pins:new Map()}); test('source retention marks are immutable metadata with monotonic job generations and per-file clearing',()=>{ const next=markImportFiles(runtime,new Map([['source',{version:1,unreferencedAt:100}]]));assert.notEqual(next,runtime);assert.equal(job.source.retirement,undefined);assert.equal(next.imports.get('job').generation,1);assert.ok(Object.isFrozen(next.imports.get('job').source.retirement));validateImportJob(next.imports.get('job'));assert.equal(markImportFiles(next,new Map([['source',{version:1,unreferencedAt:100}]])),next); const pinned=prepareImportFileRetention(next,next,()=>({complete:true,pins:new Map([['source',new Set(['export'])]])}),new Set());assert.equal(pinned.imports.get('job').source.retirement,undefined);assert.equal(pinned.imports.get('job').generation,2);assert.equal(next.imports.get('job').source.retirement.unreferencedAt,100); const incomplete=prepareImportFileRetention(next,next,()=>({complete:false,pins:new Map()}),new Set());assert.equal(incomplete.imports.get('job').source.retirement,undefined); }); test('source ownership, archive direction and completed purge metadata cannot be silently rewritten',()=>{ const archived={...job,source:{...job.source,fileId:'next',previousFiles:[{fileId:'source',byteLength:10}]}};assert.equal(importFiles(new Map([['job',archived]])).size,2);assert.equal(prepareImportFileRetention(runtime,{...runtime,imports:new Map([['job',archived]])},coverage,new Set()).imports.get('job'),archived); assert.throws(()=>prepareImportFileRetention(runtime,{...runtime,imports:new Map()},coverage,new Set()),/import_file_identity_conflict/);assert.throws(()=>prepareImportFileRetention(runtime,{...runtime,resources:new Map([['source',{}]])},coverage,new Set()),/import_file_identity_conflict/); const retired=markImportFiles(runtime,new Map([['source',{version:1,unreferencedAt:100,purgedAt:200}]]));assert.throws(()=>prepareImportFileRetention(retired,runtime,coverage,new Set()),/import_source_retired/); assert.throws(()=>importFiles(new Map([['job',job],['other',{...job,id:'other'}]])),/import_file_identity_conflict/); const resumed={...job,status:'running',source:{...job.source,retirement:{version:1,unreferencedAt:100,purgedAt:200}}};assert.throws(()=>validateImportJob(resumed),/invalid_import_file_retirement/); for(const retirement of [{version:2,unreferencedAt:1},{version:1,unreferencedAt:-1},{version:1,unreferencedAt:2,purgedAt:1}])assert.throws(()=>validateImportJob({...job,source:{...job.source,retirement}}),/invalid_import_file_retirement/); });