diff --git a/AGENTS.md b/AGENTS.md index 9c6726e..7807fca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -227,6 +227,9 @@ and CircuitDeviceRow writes execute persistent project commands. Applying a sorted view across multiple sections is one atomic `circuit.reorder-sections` command and one undo step. Explicit renumbering uses the collision-safe `circuit.renumber-section` command and is never triggered implicitly. +Immutable revision metadata is available through the paginated +`GET /api/projects/:projectId/history/revisions` endpoint; it does not expose +stored command payloads. Insertions and generated move targets use client-generated stable UUIDs, and undo restores the same ids from complete server snapshots. The tree response supplies the optimistic `currentRevision`; obsolete direct field PATCH, diff --git a/docs/circuit-list-editor-api.md b/docs/circuit-list-editor-api.md index 1ef6108..8ce5e47 100644 --- a/docs/circuit-list-editor-api.md +++ b/docs/circuit-list-editor-api.md @@ -14,6 +14,16 @@ this value for optimistic concurrency checks. - `GET /projects/:projectId/history` - returns `currentRevision`, `undoDepth`, `redoDepth` and the current top change-set id for both persistent stacks +- `GET /projects/:projectId/history/revisions` + - returns revision metadata in descending `revisionNumber` order + - optional query: `limit` (default `25`, maximum `100`) and exclusive + `beforeRevision` + - returns `projectId`, `currentRevision`, `revisions` and + `nextBeforeRevision`; use the latter as the next page's + `beforeRevision` + - each revision contains ids, number, timestamp, actor, source, description, + command type and payload schema version + - stored forward/inverse command payloads are intentionally not exposed - `POST /projects/:projectId/commands` - executes a supported versioned command as a new user revision - body: `{ "expectedRevision": 0, "command": { ... } }` diff --git a/docs/circuit-list-editor-known-limitations.md b/docs/circuit-list-editor-known-limitations.md index 4dc3c12..25992a8 100644 --- a/docs/circuit-list-editor-known-limitations.md +++ b/docs/circuit-list-editor-known-limitations.md @@ -4,8 +4,9 @@ persistent project commands and project-wide toolbar undo/redo, including moves, atomic multi-section reorders and explicit renumbering. Undo/redo eligibility is restored from server history after a page reload. -- There is no browsable revision timeline yet; the editor currently exposes - only the next eligible Undo/Redo actions. +- A paginated revision-metadata API exists, but there is no browsable timeline + UI yet; the editor currently exposes only the next eligible Undo/Redo + actions. - Named logical project snapshots and restore are not implemented yet. - No Revit/CSV/IFCGUID import and export workflow is implemented yet. - Persistence currently targets local SQLite; PostgreSQL is an architectural option, not an implemented runtime. diff --git a/docs/current-architecture.md b/docs/current-architecture.md index 1db4dcf..722b2c2 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -78,7 +78,9 @@ Gerätezeilen-Kommandos bewahren dabei auch lokale ProjectDevice-Overrides und prüfen Projektzugehörigkeit von Verknüpfungen und Räumen. Projektweite, persistente Undo-/Redo-Stacks verwalten die zulässige LIFO-Reihenfolge und verwerfen den Redo-Zweig bei einem neuen Benutzerkommando. Ihr Status ist über -`GET /api/projects/:projectId/history` lesbar. Ein zentraler Dispatcher führt +`GET /api/projects/:projectId/history` lesbar. Die unveränderliche, +absteigend paginierte Revisions-Timeline ist ohne Befehls-Payloads über +`GET /api/projects/:projectId/history/revisions` lesbar. Ein zentraler Dispatcher führt die unterstützten Typen `circuit.update` und `circuit-device-row.update` über öffentliche Command-, Undo- und Redo-Endpunkte aus. Zusätzlich sind `circuit-device-row.insert` und `circuit-device-row.delete` als atomare diff --git a/docs/project-history-and-external-model-architecture.md b/docs/project-history-and-external-model-architecture.md index 71e856e..8056495 100644 --- a/docs/project-history-and-external-model-architecture.md +++ b/docs/project-history-and-external-model-architecture.md @@ -170,6 +170,9 @@ Completed foundation: a forced stack failure rolls all of them back - `GET /api/projects/:projectId/history` exposes current revision, stack depths and top change-set ids +- `GET /api/projects/:projectId/history/revisions` exposes a descending, + cursor-paginated metadata timeline without leaking persisted forward or + inverse command payloads - a central application dispatcher executes the supported `circuit.update` and `circuit-device-row.update` envelopes without coupling the domain service to SQLite @@ -226,7 +229,7 @@ Completed foundation: Remaining constraints before completing project version history: -- add a browsable revision timeline plus named logical snapshots and restore +- add a browsable timeline UI plus named logical snapshots and restore - extend project-scoped commands only when further project mutations are deliberately added to history; the generic versioned envelope is complete - do not model IFCGUID as an overloaded circuit equipment identifier diff --git a/docs/spec/07-implementation-phases-todo.md b/docs/spec/07-implementation-phases-todo.md index f2bbf13..91f5303 100644 --- a/docs/spec/07-implementation-phases-todo.md +++ b/docs/spec/07-implementation-phases-todo.md @@ -364,6 +364,8 @@ Tasks: - [x] persist undo/redo eligibility on the server - [x] expose public command, undo and redo endpoints for the first supported update command types +- [x] expose a cursor-paginated, read-only revision metadata timeline without + command payloads - add named and periodic logical project snapshots - restore a historical snapshot as a new revision - keep database backups separate from logical history @@ -387,6 +389,8 @@ Implemented foundation: late-failure rollback coverage - a read-only project history endpoint exposes revision, stack depths and top change-set ids +- a separate read-only timeline endpoint exposes immutable revision metadata + in descending, cursor-paginated pages without forward/inverse payloads - a central application dispatcher reconstructs persisted commands and exposes optimistic command, undo and redo endpoints for Circuit and CircuitDeviceRow field updates diff --git a/package.json b/package.json index 8abf881..ab8fa6c 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ "build:api": "tsc -p tsconfig.json", "build:web": "next build", "start": "node dist/server/index.js", - "test": "tsx --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.repository.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts", - "test:watch": "tsx --watch --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.repository.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts", + "test": "tsx --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.repository.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-history-timeline.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts", + "test:watch": "tsx --watch --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.repository.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-history-timeline.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", "db:backup": "tsx scripts/db-backup.ts", diff --git a/src/db/repositories/project-history.repository.ts b/src/db/repositories/project-history.repository.ts index 7c6024c..daea7e3 100644 --- a/src/db/repositories/project-history.repository.ts +++ b/src/db/repositories/project-history.repository.ts @@ -1,11 +1,14 @@ -import { and, asc, desc, eq } from "drizzle-orm"; +import { and, asc, desc, eq, lt } from "drizzle-orm"; import { deserializeProjectCommand } from "../../domain/models/project-command.model.js"; import type { ProjectHistoryCommand, ProjectHistoryDirection, + ListProjectRevisionsInput, + ProjectRevisionPage, ProjectHistoryState, ProjectHistoryStore, } from "../../domain/ports/project-history.store.js"; +import type { ProjectRevisionSource } from "../../domain/ports/project-revision.store.js"; import type { AppDatabase } from "../database-context.js"; import { projectChangeSets } from "../schema/project-change-sets.js"; import { projectHistoryStackEntries } from "../schema/project-history-stack-entries.js"; @@ -47,6 +50,74 @@ export class ProjectHistoryRepository implements ProjectHistoryStore { }; } + listRevisions( + input: ListProjectRevisionsInput + ): ProjectRevisionPage | null { + validateRevisionPageInput(input); + const project = this.database + .select({ currentRevision: projects.currentRevision }) + .from(projects) + .where(eq(projects.id, input.projectId)) + .get(); + if (!project) { + return null; + } + + const projectCondition = eq( + projectRevisions.projectId, + input.projectId + ); + const rows = this.database + .select({ + revisionId: projectRevisions.id, + changeSetId: projectChangeSets.id, + revisionNumber: projectRevisions.revisionNumber, + createdAtIso: projectRevisions.createdAtIso, + actorId: projectRevisions.actorId, + source: projectRevisions.source, + description: projectRevisions.description, + commandType: projectChangeSets.commandType, + payloadSchemaVersion: projectChangeSets.payloadSchemaVersion, + }) + .from(projectRevisions) + .innerJoin( + projectChangeSets, + eq( + projectChangeSets.projectRevisionId, + projectRevisions.id + ) + ) + .where( + input.beforeRevision === undefined + ? projectCondition + : and( + projectCondition, + lt( + projectRevisions.revisionNumber, + input.beforeRevision + ) + ) + ) + .orderBy(desc(projectRevisions.revisionNumber)) + .limit(input.limit + 1) + .all(); + const hasMore = rows.length > input.limit; + const revisions = rows.slice(0, input.limit).map((row) => ({ + ...row, + source: row.source as ProjectRevisionSource, + })); + + return { + projectId: input.projectId, + currentRevision: project.currentRevision, + revisions, + nextBeforeRevision: + hasMore && revisions.length > 0 + ? revisions.at(-1)!.revisionNumber + : null, + }; + } + getNextCommand( projectId: string, direction: ProjectHistoryDirection @@ -93,3 +164,22 @@ export class ProjectHistoryRepository implements ProjectHistoryStore { }; } } + +function validateRevisionPageInput(input: ListProjectRevisionsInput) { + if ( + !Number.isSafeInteger(input.limit) || + input.limit < 1 || + input.limit > 100 + ) { + throw new Error("Project revision page limit must be between 1 and 100."); + } + if ( + input.beforeRevision !== undefined && + (!Number.isSafeInteger(input.beforeRevision) || + input.beforeRevision < 1) + ) { + throw new Error( + "Project revision cursor must be a positive integer." + ); + } +} diff --git a/src/domain/ports/project-history.store.ts b/src/domain/ports/project-history.store.ts index 98a40b3..fd59654 100644 --- a/src/domain/ports/project-history.store.ts +++ b/src/domain/ports/project-history.store.ts @@ -1,4 +1,5 @@ import type { SerializedProjectCommand } from "../models/project-command.model.js"; +import type { ProjectRevisionSource } from "./project-revision.store.js"; export type ProjectHistoryDirection = "undo" | "redo"; @@ -16,8 +17,36 @@ export interface ProjectHistoryCommand { command: SerializedProjectCommand; } +export interface ProjectRevisionSummary { + revisionId: string; + changeSetId: string; + revisionNumber: number; + createdAtIso: string; + actorId: string | null; + source: ProjectRevisionSource; + description: string | null; + commandType: string; + payloadSchemaVersion: number; +} + +export interface ProjectRevisionPage { + projectId: string; + currentRevision: number; + revisions: ProjectRevisionSummary[]; + nextBeforeRevision: number | null; +} + +export interface ListProjectRevisionsInput { + projectId: string; + limit: number; + beforeRevision?: number; +} + export interface ProjectHistoryStore { getState(projectId: string): ProjectHistoryState | null; + listRevisions( + input: ListProjectRevisionsInput + ): ProjectRevisionPage | null; getNextCommand( projectId: string, direction: ProjectHistoryDirection diff --git a/src/server/controllers/project-history.controller.ts b/src/server/controllers/project-history.controller.ts index 5e8df18..0cf13bc 100644 --- a/src/server/controllers/project-history.controller.ts +++ b/src/server/controllers/project-history.controller.ts @@ -1,4 +1,5 @@ import type { Request, Response } from "express"; +import { listProjectRevisionsQuerySchema } from "../../shared/validation/project-history.schemas.js"; import { projectHistoryStore } from "../composition/project-command-stores.js"; export function getProjectHistory(req: Request, res: Response) { @@ -13,3 +14,23 @@ export function getProjectHistory(req: Request, res: Response) { } return res.json(state); } + +export function listProjectRevisions(req: Request, res: Response) { + const { projectId } = req.params; + if (typeof projectId !== "string" || !projectId.trim()) { + return res.status(400).json({ error: "Invalid projectId" }); + } + const parsed = listProjectRevisionsQuerySchema.safeParse(req.query); + if (!parsed.success) { + return res.status(400).json({ error: parsed.error.flatten() }); + } + + const page = projectHistoryStore.listRevisions({ + projectId, + ...parsed.data, + }); + if (!page) { + return res.status(404).json({ error: "Project not found" }); + } + return res.json(page); +} diff --git a/src/server/routes/project.routes.ts b/src/server/routes/project.routes.ts index f9083b3..201d6b4 100644 --- a/src/server/routes/project.routes.ts +++ b/src/server/routes/project.routes.ts @@ -13,7 +13,10 @@ import { listCircuitListsByProject } from "../controllers/circuit-list.controlle import { createFloor, listFloorsByProject } from "../controllers/floor.controller.js"; import { createRoom, listRoomsByProject } from "../controllers/room.controller.js"; import { getCircuitTree } from "../controllers/circuit-tree.controller.js"; -import { getProjectHistory } from "../controllers/project-history.controller.js"; +import { + getProjectHistory, + listProjectRevisions, +} from "../controllers/project-history.controller.js"; import { executeProjectCommand, redoProjectCommand, @@ -26,6 +29,7 @@ projectRouter.get("/", listProjects); projectRouter.post("/", createProject); projectRouter.get("/:projectId", getProject); projectRouter.get("/:projectId/history", getProjectHistory); +projectRouter.get("/:projectId/history/revisions", listProjectRevisions); projectRouter.post("/:projectId/commands", executeProjectCommand); projectRouter.post("/:projectId/history/undo", undoProjectCommand); projectRouter.post("/:projectId/history/redo", redoProjectCommand); diff --git a/src/shared/validation/project-history.schemas.ts b/src/shared/validation/project-history.schemas.ts new file mode 100644 index 0000000..2c00872 --- /dev/null +++ b/src/shared/validation/project-history.schemas.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; + +export const listProjectRevisionsQuerySchema = z + .object({ + limit: z.coerce.number().int().min(1).max(100).default(25), + beforeRevision: z.coerce.number().int().positive().optional(), + }) + .strict(); diff --git a/tests/project-history-timeline.test.ts b/tests/project-history-timeline.test.ts new file mode 100644 index 0000000..c4a7b3e --- /dev/null +++ b/tests/project-history-timeline.test.ts @@ -0,0 +1,38 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { listProjectRevisionsQuerySchema } from "../src/shared/validation/project-history.schemas.js"; + +describe("project revision timeline query", () => { + it("applies a bounded default page size", () => { + assert.deepEqual( + listProjectRevisionsQuerySchema.parse({}), + { limit: 25 } + ); + assert.deepEqual( + listProjectRevisionsQuerySchema.parse({ + limit: "100", + beforeRevision: "42", + }), + { + limit: 100, + beforeRevision: 42, + } + ); + }); + + it("rejects invalid cursors, page sizes and unknown query fields", () => { + for (const query of [ + { limit: "0" }, + { limit: "101" }, + { limit: "1.5" }, + { beforeRevision: "0" }, + { beforeRevision: "not-a-number" }, + { unexpected: "value" }, + ]) { + assert.equal( + listProjectRevisionsQuerySchema.safeParse(query).success, + false + ); + } + }); +}); diff --git a/tests/project-history.repository.test.ts b/tests/project-history.repository.test.ts index 0c7594b..6d13a1a 100644 --- a/tests/project-history.repository.test.ts +++ b/tests/project-history.repository.test.ts @@ -83,6 +83,150 @@ function getRowQuantity(context: DatabaseContext) { } describe("project history repository", () => { + it("lists revision metadata in stable descending pages without payloads", () => { + const context = createTestDatabase(); + try { + const circuitsStore = new CircuitProjectCommandRepository(context.db); + const rowsStore = new CircuitDeviceRowProjectCommandRepository(context.db); + const first = circuitsStore.executeUpdate({ + projectId: "project-1", + expectedRevision: 0, + source: "user", + actorId: "planner-1", + description: "Stromkreis umbenennen", + command: createCircuitUpdateProjectCommand("circuit-1", { + displayName: "Neu", + }), + }); + const second = rowsStore.executeUpdate({ + projectId: "project-1", + expectedRevision: 1, + source: "user", + description: "Anzahl ändern", + command: createCircuitDeviceRowUpdateProjectCommand("row-1", { + quantity: 2, + }), + }); + const third = rowsStore.executeUpdate({ + projectId: "project-1", + expectedRevision: 2, + source: "undo", + description: "Anzahl zurücknehmen", + historyTargetChangeSetId: second.revision.changeSetId, + command: second.inverse, + }); + const history = new ProjectHistoryRepository(context.db); + + assert.deepEqual( + history.listRevisions({ + projectId: "project-1", + limit: 2, + }), + { + projectId: "project-1", + currentRevision: 3, + revisions: [ + { + revisionId: third.revision.revisionId, + changeSetId: third.revision.changeSetId, + revisionNumber: 3, + createdAtIso: third.revision.createdAtIso, + actorId: null, + source: "undo", + description: "Anzahl zurücknehmen", + commandType: "circuit-device-row.update", + payloadSchemaVersion: 1, + }, + { + revisionId: second.revision.revisionId, + changeSetId: second.revision.changeSetId, + revisionNumber: 2, + createdAtIso: second.revision.createdAtIso, + actorId: null, + source: "user", + description: "Anzahl ändern", + commandType: "circuit-device-row.update", + payloadSchemaVersion: 1, + }, + ], + nextBeforeRevision: 2, + } + ); + assert.deepEqual( + history.listRevisions({ + projectId: "project-1", + limit: 2, + beforeRevision: 2, + }), + { + projectId: "project-1", + currentRevision: 3, + revisions: [ + { + revisionId: first.revision.revisionId, + changeSetId: first.revision.changeSetId, + revisionNumber: 1, + createdAtIso: first.revision.createdAtIso, + actorId: "planner-1", + source: "user", + description: "Stromkreis umbenennen", + commandType: "circuit.update", + payloadSchemaVersion: 1, + }, + ], + nextBeforeRevision: null, + } + ); + } finally { + context.close(); + } + }); + + it("lists an empty timeline and rejects invalid page inputs", () => { + const context = createTestDatabase(); + try { + const history = new ProjectHistoryRepository(context.db); + assert.deepEqual( + history.listRevisions({ + projectId: "project-1", + limit: 25, + }), + { + projectId: "project-1", + currentRevision: 0, + revisions: [], + nextBeforeRevision: null, + } + ); + assert.equal( + history.listRevisions({ + projectId: "missing", + limit: 25, + }), + null + ); + assert.throws( + () => + history.listRevisions({ + projectId: "project-1", + limit: 0, + }), + /between 1 and 100/ + ); + assert.throws( + () => + history.listRevisions({ + projectId: "project-1", + limit: 25, + beforeRevision: 0, + }), + /positive integer/ + ); + } finally { + context.close(); + } + }); + it("persists project-wide undo and redo stacks across repository instances", () => { const context = createTestDatabase(); try {