v2: модульный монолит — очередь, персистентность, безопасность, SSE, тесты
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/
This commit is contained in:
2026-08-27 12:52:27 +07:00
parent 1c36a21e0c
commit 401969cfdb
68 changed files with 9368 additions and 4085 deletions

130
test/history-store.test.js Normal file
View File

@@ -0,0 +1,130 @@
"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")));
});
});