import test from 'node:test'; import assert from 'node:assert/strict'; import {spawn} from 'node:child_process'; import {mkdtempSync,rmSync,existsSync,readFileSync} from 'node:fs'; import {tmpdir} from 'node:os'; import {join} from 'node:path'; // All fault injection is confined to a disposable child process and temp volume. async function killedDeletion(phase,dir) { const moduleUrl=new URL('../server/user-store.mjs',import.meta.url).href; const code=` import fs from 'node:fs'; import {DatabaseSync} from 'node:sqlite'; const store=await import(${JSON.stringify(moduleUrl)}); const email='crash@example.test'; const bytes=Buffer.from('Kesinti sırasında korunacak gerçek belge baytları.'); const id=store.saveDocumentFile(email,{ad:'crash.txt'},bytes); const path=store.BELGE_DIR+'/'+store.getDocument(email,id).dosya; const pause=()=>{process.send({phase:${JSON.stringify(phase)},id,path});Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0);}; if(${JSON.stringify(phase)}==='before-commit') { const original=DatabaseSync.prototype.prepare; DatabaseSync.prototype.prepare=function(sql){ const statement=original.call(this,sql); if(sql==='DELETE FROM belgeler WHERE id=? AND email=?') { const run=statement.run.bind(statement); statement.run=(...args)=>{const result=run(...args);pause();return result;}; } return statement; }; } else { const original=fs.unlinkSync; fs.unlinkSync=function(candidate){if(candidate===path)pause();return original(candidate);}; } store.deleteDocument(email,id); process.exit(3); `; const child=spawn(process.execPath,['--input-type=module','-e',code],{ env:{...process.env,IHALEIST_USER_DATA_DIR:dir},stdio:['ignore','ignore','pipe','ipc'], }); let stderr='';child.stderr.on('data',chunk=>{stderr+=chunk;}); let timer; try { const message=await new Promise((resolve,reject)=>{ child.once('message',resolve); child.once('error',reject); child.once('exit',(code,signal)=>reject(new Error(`Child exited before checkpoint: ${code}/${signal} ${stderr}`))); timer=setTimeout(()=>reject(new Error('Child did not reach deletion checkpoint')),10000); }); clearTimeout(timer); const stopped=new Promise(resolve=>child.once('exit',(code,signal)=>resolve({code,signal}))); child.kill('SIGKILL'); assert.equal((await stopped).signal,'SIGKILL'); return message; } finally {clearTimeout(timer);if(child.exitCode===null && child.signalCode===null)child.kill('SIGKILL');} } for(const phase of ['before-commit','after-commit']) { test(`document deletion recovers after real SIGKILL ${phase}`,async()=>{ const dir=mkdtempSync(join(tmpdir(),'ihaleist-crash-')); try { const checkpoint=await killedDeletion(phase,dir); assert.equal(existsSync(checkpoint.path),true,'file must still exist at the forced crash checkpoint'); // Inspect recovery in another process: no cached connection can mask WAL recovery. const code=` const store=await import(${JSON.stringify(new URL('../server/user-store.mjs',import.meta.url).href)}); const record=store.getDocument('crash@example.test',${JSON.stringify(checkpoint.id)}); const db=store.openDb(); const pending=db.prepare('SELECT COUNT(*) n FROM document_file_cleanup').get().n; const integrity=db.prepare('PRAGMA integrity_check').get().integrity_check; db.close();console.log(JSON.stringify({exists:!!record,pending,integrity})); `; const child=spawn(process.execPath,['--input-type=module','-e',code],{env:{...process.env,IHALEIST_USER_DATA_DIR:dir},stdio:['ignore','pipe','pipe']}); let output='',errors='';child.stdout.on('data',chunk=>output+=chunk);child.stderr.on('data',chunk=>errors+=chunk); const exit=await new Promise((resolve,reject)=>{child.once('error',reject);child.once('exit',resolve);}); assert.equal(exit,0,errors); const result=JSON.parse(output); assert.equal(result.integrity,'ok');assert.equal(result.pending,0); assert.equal(result.exists,phase==='before-commit'); assert.equal(existsSync(checkpoint.path),phase==='before-commit'); if(phase==='before-commit')assert.equal(readFileSync(checkpoint.path,'utf8'),'Kesinti sırasında korunacak gerçek belge baytları.'); } finally {rmSync(dir,{recursive:true,force:true});} }); }