import path from "node:path"; import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { eq } from "drizzle-orm"; import { migrate } from "drizzle-orm/better-sqlite3/migrator"; import { createDatabaseContext, type DatabaseContext, } from "../src/db/database-context.js"; import { ProjectRevisionConflictError, ProjectRevisionRepository, } from "../src/db/repositories/project-revision.repository.js"; import { projectChangeSets } from "../src/db/schema/project-change-sets.js"; import { projectRevisions } from "../src/db/schema/project-revisions.js"; import { projects } from "../src/db/schema/projects.js"; function createTestDatabase(): DatabaseContext { const context = createDatabaseContext(":memory:"); migrate(context.db, { migrationsFolder: path.resolve("src", "db", "migrations"), }); context.db.insert(projects).values({ id: "project-1", name: "Test project" }).run(); return context; } function appendTestRevision( repository: ProjectRevisionRepository, expectedRevision: number ) { return repository.append({ projectId: "project-1", expectedRevision, source: "user", description: "Stromkreis bearbeiten", actorId: "test-user", forward: { schemaVersion: 1, type: "circuit.update", payload: { circuitId: "circuit-1", displayName: "Neu" }, }, inverse: { schemaVersion: 1, type: "circuit.update", payload: { circuitId: "circuit-1", displayName: "Alt" }, }, }); } describe("project revision repository", () => { it("appends immutable revision metadata and payloads with a monotonic number", () => { const context = createTestDatabase(); try { const repository = new ProjectRevisionRepository(context.db); assert.equal(repository.getCurrentRevision("project-1"), 0); const appended = appendTestRevision(repository, 0); assert.equal(appended.projectId, "project-1"); assert.equal(appended.revisionNumber, 1); assert.equal(repository.getCurrentRevision("project-1"), 1); const revision = context.db .select() .from(projectRevisions) .where(eq(projectRevisions.id, appended.revisionId)) .get(); const changeSet = context.db .select() .from(projectChangeSets) .where(eq(projectChangeSets.id, appended.changeSetId)) .get(); assert.deepEqual(revision, { id: appended.revisionId, projectId: "project-1", revisionNumber: 1, createdAtIso: appended.createdAtIso, actorId: "test-user", source: "user", description: "Stromkreis bearbeiten", }); assert.deepEqual(changeSet, { id: appended.changeSetId, projectRevisionId: appended.revisionId, commandType: "circuit.update", payloadSchemaVersion: 1, forwardPayloadJson: JSON.stringify({ schemaVersion: 1, type: "circuit.update", payload: { circuitId: "circuit-1", displayName: "Neu", }, }), inversePayloadJson: JSON.stringify({ schemaVersion: 1, type: "circuit.update", payload: { circuitId: "circuit-1", displayName: "Alt", }, }), }); const next = appendTestRevision(repository, 1); assert.equal(next.revisionNumber, 2); assert.equal(repository.getCurrentRevision("project-1"), 2); } finally { context.close(); } }); it("rejects a stale expected revision without writing history", () => { const context = createTestDatabase(); try { const repository = new ProjectRevisionRepository(context.db); appendTestRevision(repository, 0); assert.throws( () => appendTestRevision(repository, 0), (error) => error instanceof ProjectRevisionConflictError && error.expectedRevision === 0 && error.actualRevision === 1 ); assert.equal(repository.getCurrentRevision("project-1"), 1); assert.equal(context.db.select().from(projectRevisions).all().length, 1); assert.equal(context.db.select().from(projectChangeSets).all().length, 1); } finally { context.close(); } }); it("rolls back the project counter and revision when change-set storage fails", () => { const context = createTestDatabase(); try { context.sqlite.exec(` CREATE TRIGGER fail_project_change_set_insert BEFORE INSERT ON project_change_sets BEGIN SELECT RAISE(ABORT, 'forced change-set failure'); END; `); const repository = new ProjectRevisionRepository(context.db); assert.throws( () => appendTestRevision(repository, 0), /forced change-set failure/ ); assert.equal(repository.getCurrentRevision("project-1"), 0); assert.equal(context.db.select().from(projectRevisions).all().length, 0); assert.equal(context.db.select().from(projectChangeSets).all().length, 0); } finally { context.close(); } }); });