205 lines
6.8 KiB
TypeScript
205 lines
6.8 KiB
TypeScript
import { existsSync, rmSync } from "node:fs";
|
|
import { mkdtemp } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
import { genericDashboardFixture } from "$lib/model/fixtures/generic";
|
|
import type { DashboardDocument } from "$lib/model";
|
|
import {
|
|
DashboardPersistenceValidationError,
|
|
createDashboardStore,
|
|
type DashboardStore,
|
|
} from "./dashboard-store";
|
|
|
|
const stores: DashboardStore[] = [];
|
|
const tempRoots: string[] = [];
|
|
const originalCwd = process.cwd();
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
process.chdir(originalCwd);
|
|
stores.splice(0).forEach((store) => store.close());
|
|
tempRoots.splice(0).forEach((path) => rmSync(path, { force: true, recursive: true }));
|
|
});
|
|
|
|
describe("dashboard persistence store", () => {
|
|
test("seeds the first active dashboard and creates the sqlite file", async () => {
|
|
const { dbPath, store } = await createTestStore();
|
|
|
|
const seed = store.seedDashboardIfEmpty(genericDashboardFixture, {
|
|
actor: "test-seed",
|
|
message: "initial fixture",
|
|
});
|
|
|
|
expect(existsSync(dbPath)).toBe(true);
|
|
expect(seed.operation).toBe("seed");
|
|
expect(seed.actor).toBe("test-seed");
|
|
expect(seed.schemaVersion).toBe(genericDashboardFixture.schemaVersion);
|
|
expect(seed.document.metadata.title).toBe("Operations Console");
|
|
|
|
const active = store.getActiveDashboard();
|
|
expect(active?.currentRevisionId).toBe(seed.id);
|
|
expect(active?.document.metadata.title).toBe("Operations Console");
|
|
expect(store.listRevisions()).toHaveLength(1);
|
|
});
|
|
|
|
test("commits validated updates and records revision metadata", async () => {
|
|
const { store } = await createTestStore();
|
|
const seed = store.seedDashboardIfEmpty(genericDashboardFixture, {
|
|
actor: "test-seed",
|
|
});
|
|
const updated = cloneDashboard({
|
|
...genericDashboardFixture,
|
|
metadata: {
|
|
...genericDashboardFixture.metadata,
|
|
title: "Operations Console Updated",
|
|
},
|
|
});
|
|
|
|
const revision = store.commitDashboard(updated, {
|
|
actor: "agent",
|
|
message: "rename dashboard",
|
|
});
|
|
|
|
expect(revision.operation).toBe("commit");
|
|
expect(revision.actor).toBe("agent");
|
|
expect(revision.message).toBe("rename dashboard");
|
|
expect(revision.document.metadata.title).toBe("Operations Console Updated");
|
|
expect(store.getActiveDashboard()?.currentRevisionId).toBe(revision.id);
|
|
expect(store.getRevision(seed.id)?.document.metadata.title).toBe("Operations Console");
|
|
expect(store.listRevisions().map((item) => item.id)).toEqual([
|
|
revision.id,
|
|
seed.id,
|
|
]);
|
|
});
|
|
|
|
test("rejects invalid writes without changing the active revision", async () => {
|
|
const { store } = await createTestStore();
|
|
const seed = store.seedDashboardIfEmpty(genericDashboardFixture, {
|
|
actor: "test-seed",
|
|
});
|
|
const invalid = cloneDashboard({
|
|
...genericDashboardFixture,
|
|
layout: {
|
|
...genericDashboardFixture.layout,
|
|
telemetry: ["missing-card"],
|
|
},
|
|
});
|
|
|
|
expect(() =>
|
|
store.commitDashboard(invalid, {
|
|
actor: "agent",
|
|
message: "invalid update",
|
|
}),
|
|
).toThrow(DashboardPersistenceValidationError);
|
|
|
|
const active = store.getActiveDashboard();
|
|
expect(active?.currentRevisionId).toBe(seed.id);
|
|
expect(active?.document.layout.telemetry).toEqual(genericDashboardFixture.layout.telemetry);
|
|
expect(store.listRevisions()).toHaveLength(1);
|
|
});
|
|
|
|
test("loads specific revisions and rolls back to a prior valid document", async () => {
|
|
const { store } = await createTestStore();
|
|
const seed = store.seedDashboardIfEmpty(genericDashboardFixture, {
|
|
actor: "test-seed",
|
|
});
|
|
const updated = cloneDashboard({
|
|
...genericDashboardFixture,
|
|
metadata: {
|
|
...genericDashboardFixture.metadata,
|
|
title: "Changed Dashboard",
|
|
},
|
|
});
|
|
const change = store.commitDashboard(updated, {
|
|
actor: "agent",
|
|
message: "change title",
|
|
});
|
|
|
|
const rollback = store.rollbackToRevision(seed.id, {
|
|
actor: "operator",
|
|
message: "restore seed",
|
|
});
|
|
|
|
expect(store.getRevision(change.id)?.document.metadata.title).toBe("Changed Dashboard");
|
|
expect(rollback.operation).toBe("rollback");
|
|
expect(rollback.sourceRevisionId).toBe(seed.id);
|
|
expect(rollback.document.metadata.title).toBe("Operations Console");
|
|
expect(store.getActiveDashboard()?.currentRevisionId).toBe(rollback.id);
|
|
expect(store.getActiveDashboard()?.document.metadata.title).toBe("Operations Console");
|
|
expect(store.listRevisions().map((item) => item.operation)).toEqual([
|
|
"rollback",
|
|
"commit",
|
|
"seed",
|
|
]);
|
|
});
|
|
|
|
test("assigns monotonic revision timestamps for stable history ordering", async () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date("2026-06-18T12:00:00.000Z"));
|
|
const { store } = await createTestStore();
|
|
const seed = store.seedDashboardIfEmpty(genericDashboardFixture, {
|
|
actor: "test-seed",
|
|
});
|
|
const updated = cloneDashboard({
|
|
...genericDashboardFixture,
|
|
metadata: {
|
|
...genericDashboardFixture.metadata,
|
|
title: "Changed Dashboard",
|
|
},
|
|
});
|
|
|
|
const commit = store.commitDashboard(updated, { actor: "agent" });
|
|
const rollback = store.rollbackToRevision(seed.id, { actor: "operator" });
|
|
|
|
expect([
|
|
seed.createdAt.toISOString(),
|
|
commit.createdAt.toISOString(),
|
|
rollback.createdAt.toISOString(),
|
|
]).toEqual([
|
|
"2026-06-18T12:00:00.000Z",
|
|
"2026-06-18T12:00:00.001Z",
|
|
"2026-06-18T12:00:00.002Z",
|
|
]);
|
|
expect(store.listRevisions().map((item) => item.id)).toEqual([
|
|
rollback.id,
|
|
commit.id,
|
|
seed.id,
|
|
]);
|
|
});
|
|
|
|
test("applies migrations when launched outside the repository root", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "dimensionlab-dashboard-cwd-"));
|
|
tempRoots.push(root);
|
|
process.chdir(root);
|
|
vi.resetModules();
|
|
const { createDashboardStore: createStore } = await import("./dashboard-store");
|
|
const store = createStore({
|
|
databaseUrl: `file:${join(root, "dashboard.sqlite")}`,
|
|
});
|
|
stores.push(store);
|
|
|
|
const seed = store.seedDashboardIfEmpty(genericDashboardFixture, {
|
|
actor: "test-seed",
|
|
});
|
|
|
|
expect(seed.document.metadata.title).toBe("Operations Console");
|
|
expect(store.getActiveDashboard()?.currentRevisionId).toBe(seed.id);
|
|
});
|
|
});
|
|
|
|
async function createTestStore() {
|
|
const root = await mkdtemp(join(tmpdir(), "dimensionlab-dashboard-store-"));
|
|
tempRoots.push(root);
|
|
const dbPath = join(root, "nested", "dashboard.sqlite");
|
|
const store = createDashboardStore({
|
|
databaseUrl: `file:${dbPath}`,
|
|
});
|
|
stores.push(store);
|
|
|
|
return { dbPath, store };
|
|
}
|
|
|
|
function cloneDashboard(document: DashboardDocument): DashboardDocument {
|
|
return structuredClone(document);
|
|
}
|