101 lines
4.8 KiB
JavaScript
101 lines
4.8 KiB
JavaScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||
import { mkdtempSync, rmSync, readdirSync, writeFileSync, readFileSync } from 'node:fs';
|
||
import { tmpdir } from 'node:os';
|
||
import { join } from 'node:path';
|
||
import { writeJsonAtomic, readJson, readJsonOrBroken, appendLine, readLines } from './state.mjs';
|
||
|
||
let dir;
|
||
beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'night-state-')); });
|
||
afterEach(() => { rmSync(dir, { recursive: true, force: true }); });
|
||
|
||
describe('writeJsonAtomic', () => {
|
||
it('пишет файл и читает обратно тем же значением', () => {
|
||
const p = join(dir, 'a', 'b', 'state.json');
|
||
writeJsonAtomic(p, { status: 'working', n: 1 });
|
||
expect(readJson(p)).toEqual({ status: 'working', n: 1 });
|
||
});
|
||
|
||
it('не оставляет временных файлов после успешной записи', () => {
|
||
const p = join(dir, 'state.json');
|
||
writeJsonAtomic(p, { a: 1 });
|
||
expect(readdirSync(dir)).toEqual(['state.json']);
|
||
});
|
||
|
||
it('несколько записей в один каталог подряд не мешают друг другу', () => {
|
||
// 🪤 Надзиратель в одном круге пишет в каталог работника несколько файлов состояния
|
||
// подряд — от одного процесса и с одним временем на часах. Собери имя временного файла
|
||
// только из времени и номера процесса — два таких имени совпадут, и одна запись
|
||
// переименует чужую недописанную половину. Убери случайный хвост — покраснеет здесь.
|
||
const names = ['a.json', 'b.json', 'c.json', 'd.json'];
|
||
for (const n of names) writeJsonAtomic(join(dir, n), { имя: n });
|
||
for (const n of names) expect(readJson(join(dir, n))).toEqual({ имя: n });
|
||
expect(readdirSync(dir).sort()).toEqual([...names].sort()); // ни одного хвоста .tmp
|
||
});
|
||
|
||
it('старое значение уцелело, если новое не сериализуется', () => {
|
||
const p = join(dir, 'state.json');
|
||
writeJsonAtomic(p, { good: true });
|
||
const loop = {}; loop.self = loop;
|
||
expect(() => writeJsonAtomic(p, loop)).toThrow();
|
||
expect(readJson(p)).toEqual({ good: true });
|
||
});
|
||
});
|
||
|
||
describe('readJson', () => {
|
||
it('нет файла — отдаёт null, а не падает', () => {
|
||
expect(readJson(join(dir, 'нет.json'))).toBeNull();
|
||
});
|
||
|
||
it('файл порван на середине — отдаёт null, а не мусор', () => {
|
||
const p = join(dir, 'broken.json');
|
||
writeFileSync(p, '{"status":"wor');
|
||
expect(readJson(p)).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('readJsonOrBroken', () => {
|
||
// 🆕 Общий прибор затеи (решение диспетчера 01.08). Три ответа вместо двух у readJson:
|
||
// файла нет · прочитан · есть, но порван — кусок 3 и кусок 6 отличают эти три случая
|
||
// друг от друга, а readJson такого различия не даёт вовсе.
|
||
it('нет файла — found: false, broken: false, value: null', () => {
|
||
const p = join(dir, 'нет.json');
|
||
expect(readJsonOrBroken(p)).toEqual({ found: false, broken: false, value: null });
|
||
});
|
||
|
||
it('файл цел — found: true, broken: false, value с содержимым', () => {
|
||
const p = join(dir, 'state.json');
|
||
writeJsonAtomic(p, { item: 3, saved_at: '2026-08-01T00:00:00.000Z' });
|
||
expect(readJsonOrBroken(p)).toEqual({
|
||
found: true,
|
||
broken: false,
|
||
value: { item: 3, saved_at: '2026-08-01T00:00:00.000Z' },
|
||
});
|
||
});
|
||
|
||
it('файл есть, но порван на середине — found: true, broken: true, value: null', () => {
|
||
const p = join(dir, 'broken.json');
|
||
writeFileSync(p, '{"item":3,"saved_at":"202');
|
||
expect(readJsonOrBroken(p)).toEqual({ found: true, broken: true, value: null });
|
||
});
|
||
});
|
||
|
||
describe('appendLine / readLines', () => {
|
||
it('дописывает строки и читает их по порядку', () => {
|
||
const p = join(dir, 'events.jsonl');
|
||
appendLine(p, { e: 'start' });
|
||
appendLine(p, { e: 'commit' });
|
||
expect(readLines(p)).toEqual([{ e: 'start' }, { e: 'commit' }]);
|
||
});
|
||
|
||
it('порванная последняя строка пропускается, целые остаются', () => {
|
||
const p = join(dir, 'events.jsonl');
|
||
appendLine(p, { e: 'start' });
|
||
writeFileSync(p, readFileSync(p, 'utf8') + '{"e":"comm');
|
||
expect(readLines(p)).toEqual([{ e: 'start' }]);
|
||
});
|
||
|
||
it('нет файла — пустой список', () => {
|
||
expect(readLines(join(dir, 'нет.jsonl'))).toEqual([]);
|
||
});
|
||
});
|