397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
60 lines
3.0 KiB
JavaScript
60 lines
3.0 KiB
JavaScript
// tools/router-task-id.test.mjs
|
|
import { describe, it, expect } from 'vitest';
|
|
import { deriveTaskId } from './router-task-id.mjs';
|
|
|
|
describe('deriveTaskId (✅O17)', () => {
|
|
it('первый план → присвоить из firstPlanHash', () => {
|
|
expect(deriveTaskId({ existingTaskId: null, firstPlanHash: 'abc123' })).toBe('task:abc123');
|
|
});
|
|
it('существующий task-id СТАБИЛЕН через re-issue (новый хеш НЕ сбрасывает)', () => {
|
|
const id = deriveTaskId({ existingTaskId: 'task:abc123', firstPlanHash: 'NEWHASH999' });
|
|
expect(id).toBe('task:abc123'); // персист, не сброс
|
|
});
|
|
it('разные задачи (разный firstPlanHash, нет existing) → разные id', () => {
|
|
expect(deriveTaskId({ existingTaskId: null, firstPlanHash: 'p1' }))
|
|
.not.toBe(deriveTaskId({ existingTaskId: null, firstPlanHash: 'p2' }));
|
|
});
|
|
it('пустой existing и пустой firstPlanHash → null (нечего якорить)', () => {
|
|
expect(deriveTaskId({ existingTaskId: '', firstPlanHash: '' })).toBe(null);
|
|
});
|
|
});
|
|
|
|
// loadTaskId/saveTaskId — персист-обвязка (инъектируемый fs, guard sessionId)
|
|
import { loadTaskId, saveTaskId } from './router-task-id.mjs';
|
|
|
|
function memFs(files = {}, calls = []) {
|
|
return {
|
|
readFileSync(p) {
|
|
if (!(p in files)) { const e = new Error('ENOENT'); e.code = 'ENOENT'; throw e; }
|
|
return files[p];
|
|
},
|
|
writeFileSync(p, data) { calls.push(['write', p]); files[p] = data; },
|
|
renameSync(from, to) { calls.push(['rename', from, to]); files[to] = files[from]; delete files[from]; },
|
|
_files: files,
|
|
_calls: calls,
|
|
};
|
|
}
|
|
|
|
describe('loadTaskId/saveTaskId (персист first-plan-anchor)', () => {
|
|
it('save → load возвращает тот же task-id', () => {
|
|
const fsImpl = memFs();
|
|
saveTaskId({ taskId: 'task:abc', sessionId: 'sess1', runtimeDir: '/rt', fsImpl });
|
|
expect(loadTaskId({ sessionId: 'sess1', runtimeDir: '/rt', fsImpl })).toBe('task:abc');
|
|
});
|
|
it('F-C1 (sharp-edges): запись атомарна — temp→rename, НЕ прямой write в финальный путь (зеркало plan-lock writeAtomic)', () => {
|
|
const fsImpl = memFs();
|
|
saveTaskId({ taskId: 'task:abc', sessionId: 'sess1', runtimeDir: '/rt', fsImpl });
|
|
const FINAL = '/rt/router-task-id-sess1.txt';
|
|
expect(fsImpl._calls.some((c) => c[0] === 'rename' && c[2] === FINAL)).toBe(true);
|
|
expect(fsImpl._calls.filter((c) => c[0] === 'write').every((c) => c[1] !== FINAL)).toBe(true);
|
|
expect(Object.keys(fsImpl._files)).toEqual([FINAL]); // без .tmp-осадка
|
|
});
|
|
it('нет файла → null', () => {
|
|
expect(loadTaskId({ sessionId: 'sess1', runtimeDir: '/rt', fsImpl: memFs() })).toBe(null);
|
|
});
|
|
it('кривой sessionId (path-injection) → throw guard', () => {
|
|
expect(() => loadTaskId({ sessionId: '../evil', runtimeDir: '/rt', fsImpl: memFs() }))
|
|
.toThrow(/sessionId/);
|
|
});
|
|
});
|