Some checks failed
test / test (push) Failing after 24s
Реализация целевого дизайна (docs/architecture/target-design.md): - src/: модульная структура (config, logger, errors, http, core: auth/history/jobs/engines/comfy/codex/images) - Очередь заданий: FIFO, кониурентность codex=1 / comfy-upscale=2, MAX_QUEUED_JOBS=5, JSONL-журнал и восстановление после рестарта (running → interrupted, upscale requeue) - Персистентные сессии (sha256-хеши токенов), scrypt-хеш пароля, rate-limit входа, Origin-проверка, magic-byte валидация аплоадов - Атомарная запись манифеста истории, каскадное удаление, URL с фактическим расширением + 302-алиас /image.jpg, миграция history/ → data/history - Даунскейл sharp до 1024px перед Codex + масштабирование области референса - SSE /api/events с фолбэком на polling, фронтенд переведён на ES-модули (public/js/) - Тесты node:test + supertest (27), CI workflow test.yml, Docker/деплой: том kadr-data - Документация: README, docs/architecture/
131 lines
4.5 KiB
JavaScript
131 lines
4.5 KiB
JavaScript
"use strict";
|
|
|
|
const { test, describe, beforeEach, afterEach } = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const os = require("os");
|
|
const { HistoryStore } = require("../src/core/history/history-store");
|
|
const { HistoryService, migrateLegacyHistory } = require("../src/core/history/history-service");
|
|
|
|
describe("history-store", () => {
|
|
let tempDir;
|
|
|
|
beforeEach(() => {
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "kadr-history-test-"));
|
|
});
|
|
|
|
afterEach(() => {
|
|
try {
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
} catch {
|
|
// ignore
|
|
}
|
|
});
|
|
|
|
test("appends versions atomically and handles pruning", async () => {
|
|
const store = new HistoryStore({ dir: tempDir, limit: 3 });
|
|
const service = new HistoryService({ store });
|
|
|
|
const dummyImg = Buffer.from("dummy image content");
|
|
|
|
const v1 = await service.createVersion({
|
|
kind: "original",
|
|
label: "V1",
|
|
buffer: dummyImg,
|
|
ext: "jpg",
|
|
width: 100,
|
|
height: 100,
|
|
});
|
|
const v2 = await service.createVersion({
|
|
kind: "edit",
|
|
label: "V2",
|
|
parentId: v1.id,
|
|
buffer: dummyImg,
|
|
ext: "jpg",
|
|
width: 100,
|
|
height: 100,
|
|
});
|
|
const v3 = await service.createVersion({
|
|
kind: "edit",
|
|
label: "V3",
|
|
parentId: v2.id,
|
|
buffer: dummyImg,
|
|
ext: "jpg",
|
|
width: 100,
|
|
height: 100,
|
|
});
|
|
|
|
assert.equal(store.load().length, 3);
|
|
assert.ok(fs.existsSync(store.filePath(v1)));
|
|
assert.ok(fs.existsSync(store.filePath(v2)));
|
|
assert.ok(fs.existsSync(store.filePath(v3)));
|
|
|
|
// Appending 4th version should prune v1
|
|
const v4 = await service.createVersion({
|
|
kind: "upscale",
|
|
label: "V4",
|
|
parentId: v3.id,
|
|
buffer: dummyImg,
|
|
ext: "png",
|
|
width: 400,
|
|
height: 400,
|
|
});
|
|
|
|
const history = store.load();
|
|
assert.equal(history.length, 3);
|
|
assert.deepEqual(history.map((v) => v.id), [v2.id, v3.id, v4.id]);
|
|
|
|
// v1 file should be deleted on disk
|
|
assert.equal(fs.existsSync(store.filePath(v1)), false);
|
|
assert.ok(fs.existsSync(store.filePath(v4)));
|
|
|
|
// Check no leftover .tmp files
|
|
const tmpFiles = fs.readdirSync(tempDir).filter((f) => f.endsWith(".tmp"));
|
|
assert.equal(tmpFiles.length, 0);
|
|
});
|
|
|
|
test("cascade deletion removes version and all its children", async () => {
|
|
const store = new HistoryStore({ dir: tempDir, limit: 10 });
|
|
const service = new HistoryService({ store });
|
|
const dummyImg = Buffer.from("dummy image content");
|
|
|
|
const root = await service.createVersion({ kind: "original", label: "Root", buffer: dummyImg });
|
|
const child1 = await service.createVersion({ kind: "edit", label: "Child 1", parentId: root.id, buffer: dummyImg });
|
|
const child2 = await service.createVersion({ kind: "upscale", label: "Child 2", parentId: child1.id, buffer: dummyImg });
|
|
const independent = await service.createVersion({ kind: "original", label: "Independent", buffer: dummyImg });
|
|
|
|
assert.equal(store.load().length, 4);
|
|
|
|
// Delete child1 -> should delete child1 and child2, but keep root and independent
|
|
const result = await service.removeCascade(child1.id);
|
|
assert.equal(result.removed.length, 2);
|
|
assert.equal(result.remaining.length, 2);
|
|
assert.deepEqual(result.remaining.map((v) => v.id), [root.id, independent.id]);
|
|
|
|
// Verify deleted files are gone from disk
|
|
assert.ok(fs.existsSync(store.filePath(root)));
|
|
assert.ok(fs.existsSync(store.filePath(independent)));
|
|
assert.equal(fs.existsSync(store.filePath(child1)), false);
|
|
assert.equal(fs.existsSync(store.filePath(child2)), false);
|
|
});
|
|
|
|
test("migrateLegacyHistory copies files without deleting original", () => {
|
|
const legacyDir = path.join(tempDir, "legacy");
|
|
const targetDir = path.join(tempDir, "target");
|
|
|
|
fs.mkdirSync(legacyDir, { recursive: true });
|
|
const mockManifest = [{ id: "v-legacy-1", kind: "original", label: "Legacy" }];
|
|
fs.writeFileSync(path.join(legacyDir, "history.json"), JSON.stringify(mockManifest));
|
|
fs.writeFileSync(path.join(legacyDir, "v-legacy-1.jpg"), "fake jpg content");
|
|
|
|
migrateLegacyHistory(legacyDir, targetDir);
|
|
|
|
assert.ok(fs.existsSync(path.join(targetDir, "history.json")));
|
|
assert.ok(fs.existsSync(path.join(targetDir, "v-legacy-1.jpg")));
|
|
// Legacy files still exist
|
|
assert.ok(fs.existsSync(path.join(legacyDir, "history.json")));
|
|
assert.ok(fs.existsSync(path.join(legacyDir, "v-legacy-1.jpg")));
|
|
});
|
|
});
|