27 lines
3.9 KiB
JavaScript
27 lines
3.9 KiB
JavaScript
/**
|
|
* 导入源文件退休元数据的不可变更新与身份保护测试。已取消 job 的源文件仍需完整引用扫描才能进入回收。
|
|
* 这里只验证标记、归属及归档演进,不实际删除文件字节。
|
|
*/
|
|
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';
|
|
// 建立已取消 native-ndjson 导入任务及空资源表;coverage 默认返回完整且无 pin 的扫描结论。
|
|
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()});
|
|
// 新增退休标记只生成新 runtime/job 并递增 generation;相同标记是 no-op,重新被引用或扫描不完整时清除退休资格。
|
|
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);
|
|
});
|
|
// 允许源文件向 previousFiles 归档,但拒绝丢失归属、与资源重名、重复 job 共享身份或回退已 purge 元数据;非法版本/时间也拒绝。
|
|
// coverage 返回的完整/无 pin 只是扫描替身结论;这些断言只检查元数据演进,不调用真实文件仓库清理。
|
|
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/);
|
|
});
|