diff --git a/.vscode/settings.json b/.vscode/settings.json index 98187650..e5963c20 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,4 +7,4 @@ "**/.sqlx": true, "**/target": true } -} +} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 39161e39..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,155 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project shape - -VaultLink is a self-hosted Obsidian file-sync system. Two halves of one repo: - -- `sync-server/` — Rust (axum + sqlx/SQLite). Source of truth for vault state, broadcasts changes via WebSocket. -- `frontend/` — npm workspaces. The sync engine (`sync-client`) is consumed by an Obsidian plugin, a standalone CLI, a fuzz E2E harness, a scripted determinism harness, and a history UI. - -The HTTP/WS API types are generated from Rust (`ts-rs`) and mirrored into the TS workspaces. **Never hand-edit files in `frontend/sync-client/src/services/types/` or `frontend/history-ui/src/lib/types/`** — run `scripts/update-api-types.sh` after changing anything Serde-derived in the server. - -### Frontend workspaces - -- `sync-client` — the sync engine; published to consumers via `dist/`. All other TS workspaces depend on it via `file:../sync-client`. -- `obsidian-plugin` — Obsidian plugin built from `sync-client`. -- `local-client-cli` — same engine wrapped as a standalone CLI. -- `history-ui` — vault-history web UI. -- `test-client` — fuzz E2E harness (random ops across N processes). -- `deterministic-tests` — scripted multi-client tests with an in-memory FS, run against a real server. - -## Common commands - -Pre-push hygiene (formats, lints, runs tests, requires clean git state): - -```sh -scripts/check.sh --fix -``` - -Run the fuzz E2E (N parallel processes): - -```sh -scripts/e2e.sh 12 -# Logs land in logs/log_.log. Clean with scripts/clean-up.sh -``` - -Run deterministic tests (require a release-built server in `sync-server/target/release/sync_server` — they spawn it themselves): - -```sh -cd sync-server && cargo build --release && cd .. -cd frontend -npm run build -w sync-client -w deterministic-tests -node deterministic-tests/dist/cli.js # all -node deterministic-tests/dist/cli.js --filter=rename # subset -node deterministic-tests/dist/cli.js --filter=… -j 4 # cap parallelism -``` - -Run a single sync-client unit test by file: - -```sh -cd frontend/sync-client && npx tsx --test 'src/**/sync-event-queue.test.ts' -``` - -Server: dev runs from `sync-server/` against `config-e2e.yml`: - -```sh -cd sync-server -cargo run config-e2e.yml # dev -cargo build --release # used by both e2e harnesses -cargo test # unit + ts-rs binding export tests -``` - -Frontend dev (sync-client + obsidian-plugin watch in parallel): - -```sh -cd frontend && npm install && npm run dev -``` - -Regenerate TS bindings from Rust types (touches `frontend/{sync-client,history-ui}/src/.../types/`): - -```sh -scripts/update-api-types.sh -``` - -## SQLite / sqlx - -The server uses `sqlx::query!` macros that need a prepared `.sqlx` cache to compile offline. Touching any SQL means regenerating it: - -```sh -cd sync-server -sqlx database create --database-url sqlite://db.sqlite3 -sqlx migrate run --source src/app_state/database/migrations --database-url sqlite://db.sqlite3 -cargo sqlx prepare --workspace -``` - -New migrations: `sqlx migrate add --source src/app_state/database/migrations `. - -## Sync engine architecture - -Read `frontend/sync-client/src/sync-operations/` to follow the sync engine; the rest of `sync-client` is plumbing (filesystem ops, persistence, services, telemetry). - -The engine is **two independent loops with separate invariants**: - -- **Wire loop** (`syncer.ts`) — drains the single-consumer FIFO queue. HTTP and WS handlers update record fields (`remoteRelativePath`, `parentVersionId`, `remoteHash`) and write content to the file at `record.localPath`. They never move files for path placement. -- **Path reconciler** (`reconciler.ts`) — runs after every drained event. Best-effort pass that moves files to make `localPath === remoteRelativePath`. The move graph is topologically sorted; cycles are resolved by reading every file in the cycle into memory and writing each back to its new slot (no tmp files). Records with pending local events are skipped on each pass — the reconciler operates only on settled records. Failures (slot occupied by an untracked file, etc.) are silent skips; the next pass retries. - -**`SyncEventQueue`** (`sync-event-queue.ts`) holds: - -- `byDocId: Map` — primary record store. -- `byLocalPath: Map` — derived index for path lookups, maintained at every mutation point. -- `events: SyncEvent[]` — pending wire ops in FIFO drain order. - -```ts -DocumentRecord = { - documentId, - parentVersionId, - remoteHash?, - remoteRelativePath, - localPath: RelativePath | undefined -} -``` - -`localPath === undefined` means the doc has no local file yet — typically a remote create whose target slot was occupied at receive time; the reconciler will fetch and place when the slot frees (the bytes wait in `pendingPlacementContent`). - -Local FS events from the watcher update `localPath` synchronously at enqueue time via `setLocalPath` / `upsertRecord`. The wire loop never updates it for path placement; only the reconciler does. A user rename onto a tracked slot enqueues a `LocalDelete` for the displaced doc (the OS rename clobbered its content) and clears that doc's `localPath`. - -**Pending creates** use a `Promise` chain to serialize dependent ops (`LocalUpdate`, `LocalDelete`) behind the still-in-flight `LocalCreate`. `resolveCreate` resolves the promise once the server returns a docId, and `replacePendingDocumentId` swaps the resolved id across already-queued events. `findLatestCreateForPath` is the lookup the watcher uses to attach dependents; `updatePendingCreatePath` rewrites a pending create's `event.path` in place when the user renames the file before its create has acked. - -**Watermark.** `lastSeenUpdateId` uses a `MinCovered` (a contiguous-prefix tracker over a stream of integers): we only advance the published min when the next consecutive id has been processed, so out-of-order RemoteChange ids don't fool the WebSocket handshake into requesting a too-recent catch-up. - -**Server catch-up.** The server's WS handshake replays events newer than the client's `last_seen_vault_update_id` from the `latest_document_versions` view (one row per doc, the latest). On those replayed rows `is_new_file` means _new to this client_ (`creation_vault_update_id > last_seen_vault_update_id`), not "this row is the doc's first version" — necessary because the catch-up only carries the latest version; if a doc was created and updated past the watermark, the client never sees its create otherwise. - -## Edge-case patterns the sync engine has to survive - -The two-loop split defuses most of the old race catalogue (slot-collision stashes, conflict-uuid divergence, `MoveOnConflict.NEW`/`EXISTING` policy choices) by separating wire transport from path placement. What's left: - -**Pending-create docId is a `Promise`, not a string, until the create acks.** Any `LocalUpdate` / `LocalDelete` queued behind a still-in-flight `LocalCreate` carries the create's `resolvers.promise` as its `documentId`. `replacePendingDocumentId` swaps the resolved id across queued events when the create resolves; `===` comparisons against the resolved string elsewhere will silently fail until that swap runs. Anything that walks `events[]` looking for a docId match must either run after the swap or be tolerant of `Promise`-typed ids. - -**`processCreate` reads `event.path` live, not `event.originalPath`.** The watcher rewrites `event.path` in place via `updatePendingCreatePath` when the user renames a pending-create file. `originalPath` was removed from `LocalCreate` events specifically because reading it would send the stale pre-rename path to the server. - -**`record.localPath` mutates in place across awaits.** When the watcher renames a doc while a drain handler is awaiting an HTTP roundtrip, the queue mutates the in-flight event's record so subsequent reads see the new path. Snapshotting `record.localPath` into a local at function entry and using it after an `await` reads/writes a now-vacated slot. Read `record.localPath` live; only snapshot for the deliberate "did it change while I was awaiting" comparison. - -**Reconciler-defer is the wire-loop's contract with the reconciler.** The reconciler skips records where `hasPendingLocalEventsForDocumentId` returns true. Wire-loop handlers can therefore freely write `remoteRelativePath` to whatever the server returned — even if it disagrees with `localPath` — knowing the reconciler won't move the file out from under a queued user rename. - -**Watermark advancement is load-bearing both ways.** Branches that _skip_ a remote event without advancing `lastSeenUpdateId` create permanent gaps that re-deliver forever. Branches that _advance_ without applying the content lose data: the server has no further event to re-deliver, the catch-up only carries the latest version, and any state in between is gone. Don't advance unless the event was actually applied (or deliberately discarded after weighing both halves). - -**`isNewFile` semantics differ between catch-up and real-time.** On WS handshake replay it means _new to this client_ (`creation_vault_update_id > last_seen_vault_update_id`); on real-time broadcasts it means _this version is the create_ (`creation_vault_update_id == vault_update_id`). A handler that decides based on one interpretation will be wrong on the other channel; reasoning about fetch-and-treat-as-new vs. ignore needs to know which channel delivered the event. - -**Pause / disable-sync mid-flight** is the one race the new model doesn't structurally fix. An HTTP that committed server-side but whose response was discarded leaves the server holding a doc the client has no record of. Resume → offline scan → server-side dedupe handles it (the server merges the duplicate create into the existing doc), but if the merge produces a deconflict, the client picks up an extra file. Out of scope for the two-loop split. - -**Cycle reconciliation uses in-memory content swap.** When the move graph contains a cycle, the reconciler reads every file in the cycle into memory and writes each back to its new slot, with no tmp files. A write-ahead marker at `.vaultlink/swap-.json` lists each leg; on startup the reconciler reads the marker, hashes each `from` to determine which legs ran, and replays the rest. The `.vaultlink/**` glob is hard-coded as an internal ignore pattern so swap markers don't get sync'd. - -## Two complementary E2E harnesses - -- **`test-client` (fuzz):** random ops across N parallel processes for many minutes. Used by `scripts/e2e.sh`. Catches bugs nobody thought to write a test for, but reproductions are noisy. -- **`deterministic-tests`:** scripted scenarios with an in-memory FS pinned to a real server. Used to _capture_ a fuzz-discovered bug as a minimal repro before fixing it. See `frontend/deterministic-tests/README.md` for the step grammar (`pause-server`, `pause-websocket`, `barrier`, `assert-consistent`, etc.). - -When a fuzz failure surfaces, the workflow is: root-cause from logs → write a deterministic test that fails on the bug → fix → confirm both the deterministic test and `e2e.sh` pass. - -## Style - -- TS: 4-space indent, no tabs, LF, prettier (`trailingComma: "none"`). YAML/MD use 2-space indent. -- Rust: `rustfmt.toml` enforces 4-space spaces, LF. -- Lint: ESLint for TS, Clippy for Rust, `cargo machete` for unused deps. All wired into `scripts/check.sh`. diff --git a/frontend/deterministic-tests/README.md b/frontend/deterministic-tests/README.md index c422406d..678cd0fe 100644 --- a/frontend/deterministic-tests/README.md +++ b/frontend/deterministic-tests/README.md @@ -17,35 +17,30 @@ All tests run in parallel up to a concurrency limit. Clients always start with syncing disabled. **File operations** (per-client, fire-and-forget — sync is enqueued but not awaited): - - `create`, `update`, `rename`, `delete` **Sync control:** - - `sync` — wait for a specific client or all clients to finish pending operations - `barrier` — retry until all clients converge to identical file state (60s timeout) - `enable-sync` / `disable-sync` — simulate going online/offline **WebSocket control** (per-client): - - `pause-websocket` / `resume-websocket` — buffer/release WebSocket messages for a specific client **Server control:** - - `pause-server` / `resume-server` — SIGSTOP/SIGCONT the server process **Assertions:** - - `assert-consistent` — all clients have identical files; optionally takes a custom `verify(state: AssertableState)` callback ## Running ```sh # Build server first -cd sync-server && cargo build --release && cd - +cd sync-server && cargo build --release # Run all tests -cd frontend && npm run build -w sync-client && npm run test -w deterministic-tests +cd frontend && npm run test -w deterministic-tests # Filter by name npm run test -w deterministic-tests -- --filter=rename @@ -62,19 +57,15 @@ npm run test -w deterministic-tests -- -j 4 import type { TestDefinition } from "../test-definition"; export const myScenarioTest: TestDefinition = { - description: - "Client 0 creates A.md offline. After syncing, both clients should have the file.", - clients: 2, - steps: [ - { type: "create", client: 0, path: "A.md", content: "hello" }, - { type: "enable-sync", client: 0 }, - { type: "enable-sync", client: 1 }, - { type: "barrier" }, - { - type: "assert-consistent", - verify: (s) => s.assertFileCount(1).assertContent("A.md", "hello") - } - ] + description: "Client 0 creates A.md offline. After syncing, both clients should have the file.", + clients: 2, + steps: [ + { type: "create", client: 0, path: "A.md", content: "hello" }, + { type: "enable-sync", client: 0 }, + { type: "enable-sync", client: 1 }, + { type: "barrier" }, + { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertContent("A.md", "hello") } + ] }; ``` @@ -97,7 +88,7 @@ s.ifFileExists("path", (s) => ...) // conditional assertion import { myScenarioTest } from "./tests/my-scenario.test"; const TESTS = { - // ... - "my-scenario": myScenarioTest + // ... + "my-scenario": myScenarioTest }; ``` diff --git a/frontend/deterministic-tests/src/cli.ts b/frontend/deterministic-tests/src/cli.ts index 6e0e764f..57cee963 100644 --- a/frontend/deterministic-tests/src/cli.ts +++ b/frontend/deterministic-tests/src/cli.ts @@ -38,6 +38,137 @@ interface NamedTestResult { result: TestResult; } + +async function main(): Promise { + const cwd = process.cwd(); + let projectRoot = cwd; + + if (cwd.endsWith("frontend/deterministic-tests")) { + projectRoot = path.resolve(cwd, "../.."); + } else if (cwd.endsWith("frontend")) { + projectRoot = path.resolve(cwd, ".."); + } + + const serverPath = path.join(projectRoot, SERVER_BINARY_PATH); + if (!fs.existsSync(serverPath)) { + logger.error(`Server binary not found at: ${serverPath}`); + process.exit(1); + } + + const configPath = path.join(projectRoot, CONFIG_PATH); + if (!fs.existsSync(configPath)) { + logger.error(`Config file not found at: ${configPath}`); + process.exit(1); + } + + const filterArg = process.argv.find((a) => a.startsWith("--filter=")); + const filter = filterArg?.slice("--filter=".length); + + const testsToRun: [string, TestDefinition][] = []; + for (const [key, test] of Object.entries(TESTS)) { + if (test) { + if (filter && !key.includes(filter)) { + continue; + } + testsToRun.push([key, test]); + } + } + + if (testsToRun.length === 0) { + logger.error( + filter + ? `No tests matched filter "${filter}"` + : "No tests found" + ); + process.exit(1); + } + + const concurrency = parseConcurrency(); + const regularTests = testsToRun.filter( + ([, t]) => !testUsesPauseServer(t) + ); + const pauseTests = testsToRun.filter(([, t]) => testUsesPauseServer(t)); + + logger.info(`Server: ${serverPath}`); + logger.info(`Config: ${configPath}`); + logger.info( + `Tests: ${testsToRun.length} total (${regularTests.length} regular, ${pauseTests.length} server-pause)` + ); + logger.info(`Concurrency: ${concurrency}`); + + const allResults: NamedTestResult[] = []; + + if (regularTests.length > 0) { + logger.info( + `\n--- Running ${regularTests.length} regular tests (shared server, concurrency ${concurrency}) ---` + ); + const sharedServer = new ServerControl( + serverPath, + configPath, + logger + ); + serverManager.track(sharedServer); + + try { + await sharedServer.start(); + + const results = await runWithConcurrency( + regularTests, + concurrency, + async ([name, test]) => + runSharedServerTest(name, test, sharedServer) + ); + + allResults.push(...results); + } finally { + try { + await sharedServer.stop(); + } catch (error) { + logger.warn( + `Error stopping shared server: ${error instanceof Error ? error.message : String(error)}` + ); + } + serverManager.untrack(sharedServer); + } + } + + if (pauseTests.length > 0) { + logger.info( + `\n--- Running ${pauseTests.length} server-pause tests (dedicated servers, concurrency ${concurrency}) ---` + ); + + const results = await runWithConcurrency( + pauseTests, + concurrency, + async ([name, test]) => + runDedicatedServerTest(name, test, serverPath, configPath) + ); + + allResults.push(...results); + } + + const passed = allResults.filter((r) => r.result.success); + const failed = allResults.filter((r) => !r.result.success); + + logger.info(`\n--- Results: ${passed.length}/${allResults.length} passed ---`); + + if (failed.length > 0) { + for (const { name, result } of failed) { + logger.error(` FAILED: ${name}: ${result.error}`); + } + process.exit(1); + } else { + logger.info("All tests passed!"); + process.exit(0); + } +} + +main().catch((err: unknown) => { + logger.error(`Unexpected error: ${err}`); + process.exit(1); +}); + + async function runSharedServerTest( name: string, test: TestDefinition, @@ -98,132 +229,3 @@ async function runDedicatedServerTest( serverManager.untrack(server); } } - -async function main(): Promise { - const cwd = process.cwd(); - let projectRoot = cwd; - - if (cwd.endsWith("frontend/deterministic-tests")) { - projectRoot = path.resolve(cwd, "../.."); - } else if (cwd.endsWith("frontend")) { - projectRoot = path.resolve(cwd, ".."); - } - - const serverPath = path.join(projectRoot, SERVER_BINARY_PATH); - if (!fs.existsSync(serverPath)) { - logger.error(`Server binary not found at: ${serverPath}`); - process.exit(1); - } - - const configPath = path.join(projectRoot, CONFIG_PATH); - if (!fs.existsSync(configPath)) { - logger.error(`Config file not found at: ${configPath}`); - process.exit(1); - } - - const filterArg = process.argv.find((a) => a.startsWith("--filter=")); - const filter = filterArg?.slice("--filter=".length); - - const testsToRun: [string, TestDefinition][] = []; - for (const [key, test] of Object.entries(TESTS)) { - if (test) { - if ( - filter !== undefined && - filter.length > 0 && - !key.includes(filter) - ) { - continue; - } - testsToRun.push([key, test]); - } - } - - if (testsToRun.length === 0) { - logger.error( - filter !== undefined && filter.length > 0 - ? `No tests matched filter "${filter}"` - : "No tests found" - ); - process.exit(1); - } - - const concurrency = parseConcurrency(); - const regularTests = testsToRun.filter(([, t]) => !testUsesPauseServer(t)); - const pauseTests = testsToRun.filter(([, t]) => testUsesPauseServer(t)); - - logger.info(`Server: ${serverPath}`); - logger.info(`Config: ${configPath}`); - logger.info( - `Tests: ${testsToRun.length} total (${regularTests.length} regular, ${pauseTests.length} server-pause)` - ); - logger.info(`Concurrency: ${concurrency}`); - - const allResults: NamedTestResult[] = []; - - if (regularTests.length > 0) { - logger.info( - `\n--- Running ${regularTests.length} regular tests (shared server, concurrency ${concurrency}) ---` - ); - const sharedServer = new ServerControl(serverPath, configPath, logger); - serverManager.track(sharedServer); - - try { - await sharedServer.start(); - - const results = await runWithConcurrency( - regularTests, - concurrency, - async ([name, test]) => - runSharedServerTest(name, test, sharedServer) - ); - - allResults.push(...results); - } finally { - try { - await sharedServer.stop(); - } catch (error) { - logger.warn( - `Error stopping shared server: ${error instanceof Error ? error.message : String(error)}` - ); - } - serverManager.untrack(sharedServer); - } - } - - if (pauseTests.length > 0) { - logger.info( - `\n--- Running ${pauseTests.length} server-pause tests (dedicated servers, concurrency ${concurrency}) ---` - ); - - const results = await runWithConcurrency( - pauseTests, - concurrency, - async ([name, test]) => - runDedicatedServerTest(name, test, serverPath, configPath) - ); - - allResults.push(...results); - } - - const passed = allResults.filter((r) => r.result.success); - const failed = allResults.filter((r) => !r.result.success); - - logger.info( - `\n--- Results: ${passed.length}/${allResults.length} passed ---` - ); - - if (failed.length > 0) { - for (const { name, result } of failed) { - logger.error(` FAILED: ${name}: ${result.error}`); - } - process.exit(1); - } else { - logger.info("All tests passed!"); - process.exit(0); - } -} - -main().catch((err: unknown) => { - logger.error(`Unexpected error: ${err}`); - process.exit(1); -}); diff --git a/frontend/deterministic-tests/src/deterministic-agent.ts b/frontend/deterministic-tests/src/deterministic-agent.ts index 74ec2b8d..71f6a272 100644 --- a/frontend/deterministic-tests/src/deterministic-agent.ts +++ b/frontend/deterministic-tests/src/deterministic-agent.ts @@ -1,28 +1,13 @@ -import type { - HistoryEntry, - StoredDatabase, - SyncSettings, - RelativePath, - TextWithCursors -} from "sync-client"; -import { - SyncClient, - SyncResetError, - debugging, - LogLevel, - utils -} from "sync-client"; +import type { StoredDatabase, SyncSettings, RelativePath, TextWithCursors } from "sync-client"; +import { SyncClient, debugging, LogLevel } from "sync-client"; import { assert } from "./utils/assert"; import { sleep } from "./utils/sleep"; import { withTimeout } from "./utils/with-timeout"; -import { - IS_SYNC_ENABLED_BY_DEFAULT, - WAIT_TIMEOUT_MS, - WEBSOCKET_CONNECT_TIMEOUT_MS, - WEBSOCKET_POLL_INTERVAL_MS -} from "./consts"; +import { IS_SYNC_ENABLED_BY_DEFAULT, WAIT_TIMEOUT_MS, WEBSOCKET_CONNECT_TIMEOUT_MS, WEBSOCKET_POLL_INTERVAL_MS } from "./consts"; import { ManagedWebSocketFactory } from "./managed-websocket"; + + export class DeterministicAgent extends debugging.InMemoryFileSystem { public readonly clientId: number; private readonly logger: (msg: string) => void; @@ -35,18 +20,6 @@ export class DeterministicAgent extends debugging.InMemoryFileSystem { private readonly syncErrors: Error[] = []; private readonly pendingSyncOperations = new Set>(); private readonly wsFactory = new ManagedWebSocketFactory(); - private nextWriteRename: - | { - oldPath: RelativePath; - newPath: RelativePath; - } - | undefined; - private nextCreateResponseDrop: - | { - dropped: Promise; - resolveDropped: () => void; - } - | undefined; public constructor( clientId: number, @@ -60,7 +33,7 @@ export class DeterministicAgent extends debugging.InMemoryFileSystem { } public async init( - fetchImplementation: typeof globalThis.fetch + fetchImplementation: typeof globalThis.fetch, ): Promise { this.client = await SyncClient.create({ fs: this, @@ -68,7 +41,7 @@ export class DeterministicAgent extends debugging.InMemoryFileSystem { load: async () => this.data, save: async (data) => void (this.data = data) }, - fetch: this.wrapFetch(fetchImplementation), + fetch: fetchImplementation, webSocket: this.wsFactory.constructorFn }); @@ -113,65 +86,6 @@ export class DeterministicAgent extends debugging.InMemoryFileSystem { this.wsFactory.resume(); } - public dropNextCreateResponse(): void { - assert( - this.nextCreateResponseDrop === undefined, - `Client ${this.clientId} already has a create response drop armed` - ); - let resolveDropped!: () => void; - const dropped = new Promise((resolve) => { - resolveDropped = resolve; - }); - this.nextCreateResponseDrop = { - dropped, - resolveDropped - }; - this.log("Armed next create response drop"); - } - - public async waitForDroppedCreateResponse(): Promise { - assert( - this.nextCreateResponseDrop !== undefined, - `Client ${this.clientId} has no create response drop armed` - ); - await withTimeout( - this.nextCreateResponseDrop.dropped, - WAIT_TIMEOUT_MS, - `Client ${this.clientId} timed out waiting for create response drop` - ); - this.log("Create response was dropped after server commit"); - } - - public async waitForHistoryEntry( - matches: (entry: HistoryEntry) => boolean, - onMatch?: (entry: HistoryEntry) => void - ): Promise { - const existing = this.client.getHistoryEntries().find(matches); - if (existing !== undefined) { - onMatch?.(existing); - return; - } - - await withTimeout( - new Promise((resolve) => { - const unsubscribe = this.client.onSyncHistoryUpdated.add(() => { - const entry = this.client - .getHistoryEntries() - .find(matches); - if (entry === undefined) { - return; - } - - unsubscribe(); - onMatch?.(entry); - resolve(); - }); - }), - WAIT_TIMEOUT_MS, - `Client ${this.clientId} timed out waiting for history entry` - ); - } - public async waitForSync(): Promise { this.log("Waiting for sync to complete..."); // Drain agent-level sync operations first. These are the fire-and-forget @@ -193,15 +107,6 @@ export class DeterministicAgent extends debugging.InMemoryFileSystem { this.log("Sync complete"); } - public async reset(): Promise { - this.log("Resetting client (clears tracked state, keeps disk files)"); - await this.drainPendingSyncOperations(); - await this.client.reset(); - if (this.isSyncEnabled) { - await this.waitForWebSocket(); - } - } - public async disableSync(): Promise { this.log("Disabling sync"); // Drain pending enqueued operations before disabling so the SyncClient @@ -233,27 +138,17 @@ export class DeterministicAgent extends debugging.InMemoryFileSystem { await this.waitForWebSocket(); } + public async getFileContent(path: string): Promise { const bytes = await this.read(path); return new TextDecoder().decode(bytes); } - public renameNextWrite(oldPath: RelativePath, newPath: RelativePath): void { - assert( - this.nextWriteRename === undefined, - `Client ${this.clientId} already has a next-write rename armed` - ); - this.nextWriteRename = { oldPath, newPath }; - this.log(`Armed next write rename: ${oldPath} -> ${newPath}`); - } - public async cleanup(): Promise { this.log("Cleaning up..."); - // Guard against uninitialized client (init() failed partway). - // The class field uses `!:` so TS thinks this is always defined, - // but at runtime it can be undefined when init() throws partway. - const maybeClient = this.client as SyncClient | undefined; - if (maybeClient === undefined) { + // Guard against uninitialized client (init() failed partway) + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!this.client) { this.log("Client not initialized, nothing to clean up"); return; } @@ -288,40 +183,12 @@ export class DeterministicAgent extends debugging.InMemoryFileSystem { const isNew = !this.files.has(path); await super.write(path, content); - if (this.isSyncEnabled && isNew) { - this.enqueueSync(async () => { - this.client.syncLocallyCreatedFile(path); - }); - } - - const nextWriteRename = this.nextWriteRename; - if ( - nextWriteRename !== undefined && - nextWriteRename.oldPath === path - ) { - this.nextWriteRename = undefined; - await super.rename( - nextWriteRename.oldPath, - nextWriteRename.newPath + if (isNew) { + this.enqueueSync(async () => { this.client.syncLocallyCreatedFile(path); } + ); + } else { + this.enqueueSync(async () => { this.client.syncLocallyUpdatedFile({ relativePath: path }); } ); - if (this.isSyncEnabled) { - this.enqueueSync(async () => { - this.client.syncLocallyUpdatedFile({ - oldPath: nextWriteRename.oldPath, - relativePath: nextWriteRename.newPath - }); - }); - } - } - - if (!this.isSyncEnabled) { - return; - } - - if (!isNew) { - this.enqueueSync(async () => { - this.client.syncLocallyUpdatedFile({ relativePath: path }); - }); } } @@ -330,20 +197,18 @@ export class DeterministicAgent extends debugging.InMemoryFileSystem { updater: (current: TextWithCursors) => TextWithCursors ): Promise { const result = await super.atomicUpdateText(path, updater); - if (this.isSyncEnabled) { - this.enqueueSync(async () => { - this.client.syncLocallyUpdatedFile({ relativePath: path }); - }); - } + this.enqueueSync(async () => { this.client.syncLocallyUpdatedFile({ relativePath: path }); } + ); return result; + } + public override async delete(path: RelativePath): Promise { await super.delete(path); if (this.isSyncEnabled) { - this.enqueueSync(async () => { - this.client.syncLocallyDeletedFile(path); - }); + this.enqueueSync(async () => { this.client.syncLocallyDeletedFile(path); } + ); } } @@ -352,14 +217,13 @@ export class DeterministicAgent extends debugging.InMemoryFileSystem { newPath: RelativePath ): Promise { await super.rename(oldPath, newPath); - if (this.isSyncEnabled) { - this.enqueueSync(async () => { - this.client.syncLocallyUpdatedFile({ - oldPath, - relativePath: newPath - }); + this.enqueueSync(async () => { + this.client.syncLocallyUpdatedFile({ + oldPath, + relativePath: newPath }); } + ); } private async waitForWebSocket(): Promise { @@ -379,7 +243,7 @@ export class DeterministicAgent extends debugging.InMemoryFileSystem { */ private async drainPendingSyncOperations(): Promise { while (this.pendingSyncOperations.size > 0) { - await utils.awaitAll([...this.pendingSyncOperations]); + await Promise.all(this.pendingSyncOperations); } } @@ -423,42 +287,4 @@ export class DeterministicAgent extends debugging.InMemoryFileSystem { private log(message: string): void { this.logger(`[Client ${this.clientId}] ${message}`); } - - private wrapFetch( - fetchImplementation: typeof globalThis.fetch - ): typeof globalThis.fetch { - return async (input, init) => { - const response = await fetchImplementation(input, init); - const drop = this.nextCreateResponseDrop; - if ( - drop !== undefined && - DeterministicAgent.isCreateDocumentRequest(input, init) - ) { - this.nextCreateResponseDrop = undefined; - drop.resolveDropped(); - throw new SyncResetError(); - } - return response; - }; - } - - private static isCreateDocumentRequest( - input: RequestInfo | URL, - init: RequestInit | undefined - ): boolean { - const method = - init?.method ?? - (typeof Request !== "undefined" && input instanceof Request - ? input.method - : "GET"); - if (method.toUpperCase() !== "POST") { - return false; - } - - const url = - input instanceof URL - ? input - : new URL(typeof input === "string" ? input : input.url); - return /\/documents\/?$/.test(url.pathname); - } } diff --git a/frontend/deterministic-tests/src/managed-websocket.ts b/frontend/deterministic-tests/src/managed-websocket.ts index 421561fd..c09b44d7 100644 --- a/frontend/deterministic-tests/src/managed-websocket.ts +++ b/frontend/deterministic-tests/src/managed-websocket.ts @@ -2,129 +2,16 @@ * A WebSocket wrapper that can pause and resume message delivery. * When paused, incoming messages are buffered. When resumed, buffered * messages are delivered in order via the onmessage handler. - * - * Member layout follows typescript-eslint default member-ordering: all - * accessor properties are declared with `declare` and wired through the - * constructor using Object.defineProperty so we don't need conflicting - * get/set accessor pairs. */ -class ManagedWebSocket implements WebSocket { - public static readonly CONNECTING = WebSocket.CONNECTING; - public static readonly OPEN = WebSocket.OPEN; - public static readonly CLOSING = WebSocket.CLOSING; - public static readonly CLOSED = WebSocket.CLOSED; - - public readonly CONNECTING = WebSocket.CONNECTING; - public readonly OPEN = WebSocket.OPEN; - public readonly CLOSING = WebSocket.CLOSING; - public readonly CLOSED = WebSocket.CLOSED; - - declare public readonly readyState: number; - declare public readonly url: string; - declare public readonly protocol: string; - declare public readonly extensions: string; - declare public readonly bufferedAmount: number; - declare public binaryType: BinaryType; - declare public onopen: ((this: WebSocket, ev: Event) => unknown) | null; - declare public onclose: - | ((this: WebSocket, ev: CloseEvent) => unknown) - | null; - declare public onerror: ((this: WebSocket, ev: Event) => unknown) | null; - declare public onmessage: - | ((this: WebSocket, ev: MessageEvent) => unknown) - | null; - +export class ManagedWebSocket implements WebSocket { private readonly ws: WebSocket; - private readonly bufferedMessages: MessageEvent[] = []; private paused = false; + private readonly bufferedMessages: MessageEvent[] = []; private externalOnMessage: ((event: MessageEvent) => unknown) | null = null; public constructor(url: string | URL, protocols?: string | string[]) { this.ws = new WebSocket(url, protocols); - const { ws } = this; - Object.defineProperties(this, { - readyState: { - get: (): number => ws.readyState, - enumerable: true, - configurable: true - }, - url: { - get: (): string => ws.url, - enumerable: true, - configurable: true - }, - protocol: { - get: (): string => ws.protocol, - enumerable: true, - configurable: true - }, - extensions: { - get: (): string => ws.extensions, - enumerable: true, - configurable: true - }, - bufferedAmount: { - get: (): number => ws.bufferedAmount, - enumerable: true, - configurable: true - }, - binaryType: { - get: (): BinaryType => ws.binaryType, - set: (v: BinaryType): void => { - ws.binaryType = v; - }, - enumerable: true, - configurable: true - }, - onopen: { - get: (): ((this: WebSocket, ev: Event) => unknown) | null => - ws.onopen, - set: ( - h: ((this: WebSocket, ev: Event) => unknown) | null - ): void => { - ws.onopen = h; - }, - enumerable: true, - configurable: true - }, - onclose: { - get: (): - | ((this: WebSocket, ev: CloseEvent) => unknown) - | null => ws.onclose, - set: ( - h: ((this: WebSocket, ev: CloseEvent) => unknown) | null - ): void => { - ws.onclose = h; - }, - enumerable: true, - configurable: true - }, - onerror: { - get: (): ((this: WebSocket, ev: Event) => unknown) | null => - ws.onerror, - set: ( - h: ((this: WebSocket, ev: Event) => unknown) | null - ): void => { - ws.onerror = h; - }, - enumerable: true, - configurable: true - }, - onmessage: { - get: (): - | ((this: WebSocket, ev: MessageEvent) => unknown) - | null => this.externalOnMessage, - set: ( - h: ((this: WebSocket, ev: MessageEvent) => unknown) | null - ): void => { - this.externalOnMessage = h; - }, - enumerable: true, - configurable: true - } - }); - this.ws.onmessage = (event: MessageEvent): void => { if (this.paused) { this.bufferedMessages.push(event); @@ -146,6 +33,68 @@ class ManagedWebSocket implements WebSocket { } } + get readyState(): number { + return this.ws.readyState; + } + + get url(): string { + return this.ws.url; + } + + get protocol(): string { + return this.ws.protocol; + } + + get extensions(): string { + return this.ws.extensions; + } + + get bufferedAmount(): number { + return this.ws.bufferedAmount; + } + + get binaryType(): BinaryType { + return this.ws.binaryType; + } + + set binaryType(value: BinaryType) { + this.ws.binaryType = value; + } + + get onopen(): ((this: WebSocket, ev: Event) => unknown) | null { + return this.ws.onopen; + } + + set onopen(handler: ((this: WebSocket, ev: Event) => unknown) | null) { + this.ws.onopen = handler; + } + + get onclose(): ((this: WebSocket, ev: CloseEvent) => unknown) | null { + return this.ws.onclose; + } + + set onclose(handler: ((this: WebSocket, ev: CloseEvent) => unknown) | null) { + this.ws.onclose = handler; + } + + get onerror(): ((this: WebSocket, ev: Event) => unknown) | null { + return this.ws.onerror; + } + + set onerror(handler: ((this: WebSocket, ev: Event) => unknown) | null) { + this.ws.onerror = handler; + } + + get onmessage(): ((this: WebSocket, ev: MessageEvent) => unknown) | null { + return this.externalOnMessage; + } + + set onmessage( + handler: ((this: WebSocket, ev: MessageEvent) => unknown) | null + ) { + this.externalOnMessage = handler; + } + public send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void { this.ws.send(data); } @@ -169,6 +118,16 @@ class ManagedWebSocket implements WebSocket { public dispatchEvent(event: Event): boolean { return this.ws.dispatchEvent(event); } + + static readonly CONNECTING = WebSocket.CONNECTING; + static readonly OPEN = WebSocket.OPEN; + static readonly CLOSING = WebSocket.CLOSING; + static readonly CLOSED = WebSocket.CLOSED; + + readonly CONNECTING = WebSocket.CONNECTING; + readonly OPEN = WebSocket.OPEN; + readonly CLOSING = WebSocket.CLOSING; + readonly CLOSED = WebSocket.CLOSED; } /** @@ -177,41 +136,33 @@ class ManagedWebSocket implements WebSocket { */ export class ManagedWebSocketFactory { private readonly instances: ManagedWebSocket[] = []; - // Sticky pause state: applied to current instances on `pause()` AND - // to any new instance created later (e.g. WS reconnect after a - // `disable-sync` / `reset` cycle). Without this, a test pausing the - // WS before the agent reconnects would silently see the new socket - // start un-paused and miss the messages it meant to buffer. - private currentlyPaused = false; public get constructorFn(): typeof globalThis.WebSocket { - const trackInstance = (instance: ManagedWebSocket): void => { - this.instances.push(instance); - if (this.currentlyPaused) { - instance.pause(); - } - }; - class TrackedManagedWebSocket extends ManagedWebSocket { - public constructor( - url: string | URL, - protocols?: string | string[] - ) { - super(url, protocols); - trackInstance(this); - } - } - return TrackedManagedWebSocket; + const factory = this; + const ctor = function ManagedWS( + url: string | URL, + protocols?: string | string[] + ): ManagedWebSocket { + const ws = new ManagedWebSocket(url, protocols); + factory.instances.push(ws); + return ws; + } as unknown as typeof globalThis.WebSocket; + + Object.defineProperty(ctor, "CONNECTING", { value: WebSocket.CONNECTING }); + Object.defineProperty(ctor, "OPEN", { value: WebSocket.OPEN }); + Object.defineProperty(ctor, "CLOSING", { value: WebSocket.CLOSING }); + Object.defineProperty(ctor, "CLOSED", { value: WebSocket.CLOSED }); + + return ctor; } public pause(): void { - this.currentlyPaused = true; for (const ws of this.instances) { ws.pause(); } } public resume(): void { - this.currentlyPaused = false; for (const ws of this.instances) { ws.resume(); } diff --git a/frontend/deterministic-tests/src/parse-concurrency.ts b/frontend/deterministic-tests/src/parse-concurrency.ts index f926d1fa..a6622a04 100644 --- a/frontend/deterministic-tests/src/parse-concurrency.ts +++ b/frontend/deterministic-tests/src/parse-concurrency.ts @@ -8,9 +8,7 @@ export function parseConcurrency(): number { i + 1 < args.length ) { const n = parseInt(args[i + 1], 10); - if (!isNaN(n) && n > 0) { - return n; - } + if (!isNaN(n) && n > 0) return n; } } return os.cpus().length; diff --git a/frontend/deterministic-tests/src/server-control.ts b/frontend/deterministic-tests/src/server-control.ts index f903cc4c..de0dbe4b 100644 --- a/frontend/deterministic-tests/src/server-control.ts +++ b/frontend/deterministic-tests/src/server-control.ts @@ -42,7 +42,9 @@ export class ServerControl { this._port = reservation.port; // Prefer tmpfs (/host/tmp) over disk-backed /tmp for faster SQLite I/O const tmpBase = fs.existsSync("/host/tmp") ? "/host/tmp" : os.tmpdir(); - this.tempDir = fs.mkdtempSync(path.join(tmpBase, "vault-link-test-")); + this.tempDir = fs.mkdtempSync( + path.join(tmpBase, "vault-link-test-") + ); const tempConfigPath = path.join(this.tempDir, "config.yml"); const dbDir = path.join(this.tempDir, "databases"); @@ -223,7 +225,7 @@ export class ServerControl { } private cleanupTempDir(): void { - if (this.tempDir !== undefined) { + if (this.tempDir) { try { fs.rmSync(this.tempDir, { recursive: true, force: true }); } catch { @@ -232,4 +234,5 @@ export class ServerControl { this.tempDir = undefined; } } + } diff --git a/frontend/deterministic-tests/src/server-manager.ts b/frontend/deterministic-tests/src/server-manager.ts index a9697eb0..8764e669 100644 --- a/frontend/deterministic-tests/src/server-manager.ts +++ b/frontend/deterministic-tests/src/server-manager.ts @@ -19,9 +19,7 @@ export class ServerManager { } public async stopAll(): Promise { - if (this.isShuttingDown) { - return; - } + if (this.isShuttingDown) return; this.isShuttingDown = true; const servers = Array.from(this.activeServers); @@ -41,18 +39,14 @@ export class ServerManager { process.on("SIGINT", () => { this.logger.info("Received SIGINT, shutting down..."); void this.stopAll() - .catch(() => { - /* no-op */ - }) + .catch(() => {}) .then(() => process.exit(130)); }); process.on("SIGTERM", () => { this.logger.info("Received SIGTERM, shutting down..."); void this.stopAll() - .catch(() => { - /* no-op */ - }) + .catch(() => {}) .then(() => process.exit(143)); }); } diff --git a/frontend/deterministic-tests/src/test-definition.ts b/frontend/deterministic-tests/src/test-definition.ts index bd832a50..826c6014 100644 --- a/frontend/deterministic-tests/src/test-definition.ts +++ b/frontend/deterministic-tests/src/test-definition.ts @@ -9,32 +9,16 @@ export type TestStep = | { type: "create"; client: number; path: string; content: string } | { type: "update"; client: number; path: string; content: string } | { type: "rename"; client: number; oldPath: string; newPath: string } - | { - type: "rename-next-write"; - client: number; - oldPath: string; - newPath: string; - } | { type: "delete"; client: number; path: string } | { type: "sync"; client?: number } | { type: "disable-sync"; client: number } | { type: "enable-sync"; client: number } | { type: "pause-server" } | { type: "resume-server" } - | { - type: "resume-server-until-history-then-pause"; - client: number; - syncType: "CREATE" | "UPDATE" | "DELETE"; - path: string; - } | { type: "barrier" } | { type: "assert-consistent"; verify?: (state: AssertableState) => void } | { type: "pause-websocket"; client: number } - | { type: "resume-websocket"; client: number } - | { type: "drop-next-create-response"; client: number } - | { type: "wait-for-dropped-create-response"; client: number } - | { type: "sleep"; ms: number } - | { type: "reset"; client: number }; + | { type: "resume-websocket"; client: number }; export interface TestDefinition { description?: string; diff --git a/frontend/deterministic-tests/src/test-registry.ts b/frontend/deterministic-tests/src/test-registry.ts index ed4fe026..4a16db2b 100644 --- a/frontend/deterministic-tests/src/test-registry.ts +++ b/frontend/deterministic-tests/src/test-registry.ts @@ -6,6 +6,7 @@ import { deleteRenameConflictTest } from "./tests/delete-rename-conflict.test"; import { multiFileOperationsTest } from "./tests/multi-file-operations.test"; import { deleteRecreateSamePathTest } from "./tests/delete-recreate-same-path.test"; import { offlineRenameAndEditTest } from "./tests/offline-rename-and-edit.test"; +import { renameToExistingPathTest } from "./tests/rename-to-existing-path.test"; import { simultaneousCreateDeleteSamePathTest } from "./tests/simultaneous-create-delete-same-path.test"; import { idempotencyAfterServerPauseTest } from "./tests/idempotency-after-server-pause.test"; import { sequentialCreateDuplicateContentTest } from "./tests/sequential-create-duplicate-content.test"; @@ -25,6 +26,7 @@ import { offlineRenameRemoteCreateOldPathTest } from "./tests/offline-rename-rem import { offlineEditRemoteRenameTest } from "./tests/offline-edit-remote-rename.test"; import { renameChainThenDeleteTest } from "./tests/rename-chain-then-delete.test"; import { offlineDeleteRemoteRenameTest } from "./tests/offline-delete-remote-rename.test"; +import { renameToRecentlyDeletedPathTest } from "./tests/rename-to-recently-deleted-path.test"; import { overlappingEditsSameSectionTest } from "./tests/overlapping-edits-same-section.test"; import { rapidUpdatesAfterMergeTest } from "./tests/rapid-updates-after-merge.test"; import { deleteRecreateConcurrentUpdateTest } from "./tests/delete-recreate-concurrent-update.test"; @@ -51,6 +53,7 @@ import { updateDoesNotSurvivesRemoteDeleteTest } from "./tests/update-survives-r import { movePreservesRemoteUpdateTest } from "./tests/move-preserves-remote-update.test"; import { recentlyDeletedClearedOnReconnectTest } from "./tests/recently-deleted-cleared-on-reconnect.test"; import { migrateKeyPreservesExistingTest } from "./tests/migrate-key-preserves-existing.test"; +import { failedVfsMoveFallsBackTest } from "./tests/failed-vfs-move-falls-back.test"; import { watermarkAdvancesOnSkipTest } from "./tests/watermark-advances-on-skip.test"; import { watermarkGapRemoteUpdateNotRecordedTest } from "./tests/watermark-gap-remote-update-not-recorded.test"; import { queueResetLosesCoalescedLocalEditTest } from "./tests/queue-reset-loses-coalesced-local-edit.test"; @@ -89,23 +92,6 @@ import { serverPauseDeleteRecreateTest } from "./tests/server-pause-delete-recre import { onlineBothCreateSamePathDeconflictTest } from "./tests/online-both-create-same-path-deconflict.test"; import { onlineCreateUpdateWhileOtherCreatesSamePathTest } from "./tests/online-create-update-while-other-creates-same-path.test"; import { displacedFileNotMarkedDeletedTest } from "./tests/displaced-file-not-marked-deleted.test"; -import { remoteUpdateResurrectsDeletedDocTest } from "./tests/remote-update-resurrects-deleted-doc.test"; -import { localUpdateSurvivesRemoteRenameTest } from "./tests/local-update-survives-remote-rename.test"; -import { mergingUpdateResponseSurvivesUserRenameTest } from "./tests/merging-update-response-survives-user-rename.test"; -import { catchupCreateAndUpdateNotSkippedTest } from "./tests/catchup-create-and-update-not-skipped.test"; -import { localRenameSurvivesRemoteRenameTest } from "./tests/local-rename-survives-remote-rename.test"; -import { renameChainDuringPendingCreateTest } from "./tests/rename-chain-during-pending-create.test"; -import { remoteRenameCollidesWithPendingLocalCreateTest } from "./tests/remote-rename-collides-with-pending-local-create.test"; -import { remoteUpdateSurvivesUserRenameTest } from "./tests/remote-update-survives-user-rename.test"; -import { sameDocIdCollapseOnLocalCreateAfterRemoteCreateTest } from "./tests/same-doc-id-collapse-on-local-create-after-remote-create.test"; -import { sameDocIdCollapseAfterRemoteQuickWriteAndPendingRenameTest } from "./tests/same-doc-id-collapse-after-remote-quick-write-and-pending-rename.test"; -import { renameOverwritesPendingCreateThenDeleteTest } from "./tests/rename-overwrites-pending-create-then-delete.test"; -import { deleteRecreatedPendingCreateWithStaleDeletingRecordTest } from "./tests/delete-recreated-pending-create-with-stale-deleting-record.test"; -import { queuedCreateDeleteDoesNotHijackReusedPathTest } from "./tests/queued-create-delete-does-not-hijack-reused-path.test"; -import { renamedPendingCreateReusedPathThenDeleteTest } from "./tests/renamed-pending-create-reused-path-then-delete.test"; -import { renamePendingCreateOntoPendingDeletePathTest } from "./tests/rename-pending-create-onto-pending-delete-path.test"; -import { remoteQuickWriteRenameBeforeRecordTest } from "./tests/remote-quick-write-rename-before-record.test"; -import { selfMergePendingRenameAliasesSecondCreateTest } from "./tests/self-merge-pending-rename-aliases-second-create.test"; export const TESTS: Partial> = { "rename-create-conflict": renameCreateConflictTest, @@ -115,12 +101,11 @@ export const TESTS: Partial> = { "multi-file-operations": multiFileOperationsTest, "delete-recreate-same-path": deleteRecreateSamePathTest, "offline-rename-and-edit": offlineRenameAndEditTest, - "simultaneous-create-delete-same-path": - simultaneousCreateDeleteSamePathTest, + "rename-to-existing-path": renameToExistingPathTest, + "simultaneous-create-delete-same-path": simultaneousCreateDeleteSamePathTest, "idempotency-after-server-pause": idempotencyAfterServerPauseTest, "sequential-create-duplicate-content": sequentialCreateDuplicateContentTest, - "mc-three-client-rename-offline-update": - mcThreeClientRenameOfflineUpdateTest, + "mc-three-client-rename-offline-update": mcThreeClientRenameOfflineUpdateTest, "mc-multi-delete-offline-rename": mcMultiDeleteOfflineRenameTest, "mc-cross-create-rename-same-target": mcCrossCreateRenameSameTargetTest, "mc-delete-then-offline-rename": mcDeleteThenOfflineRenameTest, @@ -132,11 +117,11 @@ export const TESTS: Partial> = { "rename-swap": renameSwapTest, "rename-circular": renameCircularTest, "rename-roundtrip": renameRoundtripTest, - "offline-rename-remote-create-old-path": - offlineRenameRemoteCreateOldPathTest, + "offline-rename-remote-create-old-path": offlineRenameRemoteCreateOldPathTest, "offline-edit-remote-rename": offlineEditRemoteRenameTest, "rename-chain-then-delete": renameChainThenDeleteTest, "offline-delete-remote-rename": offlineDeleteRemoteRenameTest, + "rename-to-recently-deleted-path": renameToRecentlyDeletedPathTest, "overlapping-edits-same-section": overlappingEditsSameSectionTest, "rapid-updates-after-merge": rapidUpdatesAfterMergeTest, "delete-recreate-concurrent-update": deleteRecreateConcurrentUpdateTest, @@ -155,44 +140,34 @@ export const TESTS: Partial> = { "delete-recreate-different-content": deleteRecreateDifferentContentTest, "update-during-create-processing": updateDuringCreateProcessingTest, "offline-move-then-remote-delete": offlineMoveThenRemoteDeleteTest, - "reset-clears-recently-deleted-resurrection": - resetClearsRecentlyDeletedResurrectionTest, + "reset-clears-recently-deleted-resurrection": resetClearsRecentlyDeletedResurrectionTest, "move-then-delete-stale-path": moveThenDeleteStalePathTest, "offline-delete-vs-remote-update": offlineDeleteVsRemoteUpdateTest, "interrupted-delete-retry": interruptedDeleteRetryTest, "update-survives-remote-delete": updateDoesNotSurvivesRemoteDeleteTest, "move-preserves-remote-update": movePreservesRemoteUpdateTest, - "recently-deleted-cleared-on-reconnect": - recentlyDeletedClearedOnReconnectTest, + "recently-deleted-cleared-on-reconnect": recentlyDeletedClearedOnReconnectTest, "migrate-key-preserves-existing": migrateKeyPreservesExistingTest, + "failed-vfs-move-falls-back": failedVfsMoveFallsBackTest, "watermark-advances-on-skip": watermarkAdvancesOnSkipTest, - "watermark-gap-remote-update-not-recorded": - watermarkGapRemoteUpdateNotRecordedTest, - "queue-reset-loses-coalesced-local-edit": - queueResetLosesCoalescedLocalEditTest, + "watermark-gap-remote-update-not-recorded": watermarkGapRemoteUpdateNotRecordedTest, + "queue-reset-loses-coalesced-local-edit": queueResetLosesCoalescedLocalEditTest, "rename-to-pending-path-fallback": renameToPendingPathFallbackTest, "move-remote-update-reverts-rename": moveRemoteUpdateRevertsRenameTest, "local-edit-lost-during-create-merge": localEditLostDuringCreateMergeTest, - "rename-pending-create-before-response": - renamePendingCreateBeforeResponseTest, + "rename-pending-create-before-response": renamePendingCreateBeforeResponseTest, "create-rename-response-skips-file": createRenameResponseSkipsFileTest, - "online-create-rename-concurrent-create-orphan": - onlineCreateRenameConcurrentCreateOrphanTest, + "online-create-rename-concurrent-create-orphan": onlineCreateRenameConcurrentCreateOrphanTest, "concurrent-rename-first-wins": concurrentRenameFirstWinsTest, "binary-to-text-transition": binaryToTextTransitionTest, "text-pending-create-not-displaced": textPendingCreateNotDisplacedTest, "binary-pending-create-not-displaced": binaryPendingCreateNotDisplacedTest, - "coalesce-update-remote-update-data-loss": - coalesceUpdateRemoteUpdateDataLossTest, - "coalesced-remote-update-watermark-loss": - coalescedRemoteUpdateWatermarkLossTest, - "concurrent-delete-during-remote-update": - concurrentDeleteDuringRemoteUpdateTest, + "coalesce-update-remote-update-data-loss": coalesceUpdateRemoteUpdateDataLossTest, + "coalesced-remote-update-watermark-loss": coalescedRemoteUpdateWatermarkLossTest, + "concurrent-delete-during-remote-update": concurrentDeleteDuringRemoteUpdateTest, "concurrent-edit-exact-same-position": concurrentEditExactSamePositionTest, - "concurrent-rename-and-create-at-target-rename-first": - concurrentRenameAndCreateAtTargetRenameFirstTest, - "concurrent-rename-and-create-at-target-create-first": - concurrentRenameAndCreateAtTargetCreateFirstTest, + "concurrent-rename-and-create-at-target-rename-first": concurrentRenameAndCreateAtTargetRenameFirstTest, + "concurrent-rename-and-create-at-target-create-first": concurrentRenameAndCreateAtTargetCreateFirstTest, "concurrent-rename-same-target": concurrentRenameSameTargetTest, "concurrent-update-diff-consistency": concurrentUpdateDiffConsistencyTest, "user-parenthesized-file-not-deleted": userParenthesizedFileNotDeletedTest, @@ -201,49 +176,15 @@ export const TESTS: Partial> = { "move-identical-content-ambiguity": moveIdenticalContentAmbiguityTest, "create-update-coalesce-server-pause": createUpdateCoalesceServerPauseTest, "create-during-reconciliation": createDuringReconciliationTest, - "create-merge-preserves-renamed-update": - createMergePreservesRenamedUpdateTest, + "create-merge-preserves-renamed-update": createMergePreservesRenamedUpdateTest, "create-rename-create-same-path": createRenameCreateSamePathTest, "move-chain-three-files": moveChainThreeFilesTest, "delete-by-other-client-then-recreate": deleteByOtherClientThenRecreateTest, "online-delete-recreate-rapid-cycle": onlineDeleteRecreateRapidCycleTest, "online-edit-vs-delete-convergence": onlineEditVsDeleteConvergenceTest, - "rapid-edit-delete-online-convergence": - rapidEditDeleteOnlineConvergenceTest, + "rapid-edit-delete-online-convergence": rapidEditDeleteOnlineConvergenceTest, "server-pause-delete-recreate": serverPauseDeleteRecreateTest, - "online-both-create-same-path-deconflict": - onlineBothCreateSamePathDeconflictTest, - "online-create-update-while-other-creates-same-path": - onlineCreateUpdateWhileOtherCreatesSamePathTest, + "online-both-create-same-path-deconflict": onlineBothCreateSamePathDeconflictTest, + "online-create-update-while-other-creates-same-path": onlineCreateUpdateWhileOtherCreatesSamePathTest, "displaced-file-not-marked-deleted": displacedFileNotMarkedDeletedTest, - "remote-update-resurrects-deleted-doc": - remoteUpdateResurrectsDeletedDocTest, - "local-update-survives-remote-rename": localUpdateSurvivesRemoteRenameTest, - "merging-update-response-survives-user-rename": - mergingUpdateResponseSurvivesUserRenameTest, - "catchup-create-and-update-not-skipped": - catchupCreateAndUpdateNotSkippedTest, - "local-rename-survives-remote-rename": localRenameSurvivesRemoteRenameTest, - "rename-chain-during-pending-create": renameChainDuringPendingCreateTest, - "remote-rename-collides-with-pending-local-create": - remoteRenameCollidesWithPendingLocalCreateTest, - "remote-update-survives-user-rename": remoteUpdateSurvivesUserRenameTest, - "same-doc-id-collapse-on-local-create-after-remote-create": - sameDocIdCollapseOnLocalCreateAfterRemoteCreateTest, - "renamed-pending-create-reused-path-then-delete": - renamedPendingCreateReusedPathThenDeleteTest, - "rename-pending-create-onto-pending-delete-path": - renamePendingCreateOntoPendingDeletePathTest, - "rename-overwrites-pending-create-then-delete": - renameOverwritesPendingCreateThenDeleteTest, - "same-doc-id-collapse-after-remote-quick-write-and-pending-rename": - sameDocIdCollapseAfterRemoteQuickWriteAndPendingRenameTest, - "delete-recreated-pending-create-with-stale-deleting-record": - deleteRecreatedPendingCreateWithStaleDeletingRecordTest, - "queued-create-delete-does-not-hijack-reused-path": - queuedCreateDeleteDoesNotHijackReusedPathTest, - "remote-quick-write-rename-before-record": - remoteQuickWriteRenameBeforeRecordTest, - "self-merge-pending-rename-aliases-second-create": - selfMergePendingRenameAliasesSecondCreateTest }; diff --git a/frontend/deterministic-tests/src/test-runner.ts b/frontend/deterministic-tests/src/test-runner.ts index c8cbadd0..2d469fa2 100644 --- a/frontend/deterministic-tests/src/test-runner.ts +++ b/frontend/deterministic-tests/src/test-runner.ts @@ -1,4 +1,8 @@ -import type { TestDefinition, TestResult, TestStep } from "./test-definition"; +import type { + TestDefinition, + TestResult, + TestStep +} from "./test-definition"; import { DeterministicAgent } from "./deterministic-agent"; import type { ServerControl } from "./server-control"; import type { SyncSettings, Logger } from "sync-client"; @@ -109,7 +113,9 @@ export class TestRunner { // Push before init so cleanup() handles this agent if init fails this.agents.push(agent); await withTimeout( - agent.init(fetch), + agent.init( + fetch, + ), AGENT_INIT_TIMEOUT_MS, `Client ${i} init timed out after ${AGENT_INIT_TIMEOUT_MS}ms` ); @@ -144,13 +150,6 @@ export class TestRunner { ); break; - case "rename-next-write": - this.getAgent(step.client).renameNextWrite( - step.oldPath, - step.newPath - ); - break; - case "delete": await this.getAgent(step.client).delete(step.path); break; @@ -184,19 +183,6 @@ export class TestRunner { await this.serverControl.waitForReady(); break; - case "resume-server-until-history-then-pause": { - const agent = this.getAgent(step.client); - const historySeen = agent.waitForHistoryEntry( - (entry) => - entry.details.type === step.syncType && - entry.details.relativePath === step.path, - () => this.serverControl.pause() - ); - this.serverControl.resume(); - await historySeen; - break; - } - case "barrier": await this.waitForConvergence(); break; @@ -213,22 +199,6 @@ export class TestRunner { this.getAgent(step.client).resumeWebSocket(); break; - case "drop-next-create-response": - this.getAgent(step.client).dropNextCreateResponse(); - break; - - case "wait-for-dropped-create-response": - await this.getAgent(step.client).waitForDroppedCreateResponse(); - break; - - case "sleep": - await sleep(step.ms); - break; - - case "reset": - await this.getAgent(step.client).reset(); - break; - default: { const unknownStep = step as { type: string }; throw new Error(`Unknown step type: ${unknownStep.type}`); @@ -306,10 +276,7 @@ export class TestRunner { verify?: (state: AssertableState) => void ): Promise { this.logger.info("Asserting all clients are consistent..."); - assert( - this.agents.length >= 2, - "Need at least 2 agents for consistency check" - ); + assert(this.agents.length >= 2, "Need at least 2 agents for consistency check"); // Snapshot all agents' file states upfront to minimize the window // where background sync could mutate state between reads. diff --git a/frontend/deterministic-tests/src/tests/1-text-pending-create-not-displaced.test.ts b/frontend/deterministic-tests/src/tests/1-text-pending-create-not-displaced.test.ts index 28243525..fced7c5f 100644 --- a/frontend/deterministic-tests/src/tests/1-text-pending-create-not-displaced.test.ts +++ b/frontend/deterministic-tests/src/tests/1-text-pending-create-not-displaced.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const textPendingCreateNotDisplacedTest: TestDefinition = { @@ -24,13 +23,6 @@ export const textPendingCreateNotDisplacedTest: TestDefinition = { { type: "enable-sync", client: 1 }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1) - .assertFileExists("data.txt") - .assertAnyFileContains("client-0", "client-1"); - } - } + { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertFileExists("data.txt").assertAnyFileContains("client-0", "client-1") } ] }; diff --git a/frontend/deterministic-tests/src/tests/10-concurrent-update-diff-consistency.test.ts b/frontend/deterministic-tests/src/tests/10-concurrent-update-diff-consistency.test.ts index d21ce16b..94e6914e 100644 --- a/frontend/deterministic-tests/src/tests/10-concurrent-update-diff-consistency.test.ts +++ b/frontend/deterministic-tests/src/tests/10-concurrent-update-diff-consistency.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const concurrentUpdateDiffConsistencyTest: TestDefinition = { @@ -36,16 +35,6 @@ export const concurrentUpdateDiffConsistencyTest: TestDefinition = { { type: "enable-sync", client: 1 }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (state: AssertableState): void => { - state - .assertFileCount(1) - .assertContent( - "doc.md", - "header by 0\nmiddle\nfooter by 1" - ); - } - } + { type: "assert-consistent", verify: (state) => state.assertFileCount(1).assertContent("doc.md", "header by 0\nmiddle\nfooter by 1") } ] }; diff --git a/frontend/deterministic-tests/src/tests/10-user-parenthesized-file-not-deleted.test.ts b/frontend/deterministic-tests/src/tests/10-user-parenthesized-file-not-deleted.test.ts index ef6cd771..8be438e2 100644 --- a/frontend/deterministic-tests/src/tests/10-user-parenthesized-file-not-deleted.test.ts +++ b/frontend/deterministic-tests/src/tests/10-user-parenthesized-file-not-deleted.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const userParenthesizedFileNotDeletedTest: TestDefinition = { @@ -35,7 +34,7 @@ export const userParenthesizedFileNotDeletedTest: TestDefinition = { { type: "assert-consistent", - verify: (state: AssertableState): void => { + verify: (state) => { state .assertFileCount(3) .assertFileExists("Chapter.bin") diff --git a/frontend/deterministic-tests/src/tests/11-create-delete-noop.test.ts b/frontend/deterministic-tests/src/tests/11-create-delete-noop.test.ts index 6c766001..b1239217 100644 --- a/frontend/deterministic-tests/src/tests/11-create-delete-noop.test.ts +++ b/frontend/deterministic-tests/src/tests/11-create-delete-noop.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const createDeleteNoopTest: TestDefinition = { @@ -17,11 +16,6 @@ export const createDeleteNoopTest: TestDefinition = { { type: "enable-sync", client: 0 }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileNotExists("temp.md"); - } - } + { type: "assert-consistent", verify: (s) => s.assertFileNotExists("temp.md") } ] }; diff --git a/frontend/deterministic-tests/src/tests/12-create-merge-delete.test.ts b/frontend/deterministic-tests/src/tests/12-create-merge-delete.test.ts index ef7ea5c3..4b121939 100644 --- a/frontend/deterministic-tests/src/tests/12-create-merge-delete.test.ts +++ b/frontend/deterministic-tests/src/tests/12-create-merge-delete.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const createMergeDeleteTest: TestDefinition = { @@ -17,21 +16,12 @@ export const createMergeDeleteTest: TestDefinition = { { type: "assert-consistent", - verify: (state: AssertableState): void => { - state - .assertFileCount(1) - .assertContains("A.md", "from-zero", "from-one"); - } + verify: (state) => state.assertFileCount(1).assertContains("A.md", "from-zero", "from-one") }, { type: "delete", client: 0, path: "A.md" }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(0).assertFileNotExists("A.md"); - } - } + { type: "assert-consistent", verify: (s) => s.assertFileCount(0).assertFileNotExists("A.md") } ] }; diff --git a/frontend/deterministic-tests/src/tests/13-move-identical-content-ambiguity.test.ts b/frontend/deterministic-tests/src/tests/13-move-identical-content-ambiguity.test.ts index 2a9ce0b4..9c0f7245 100644 --- a/frontend/deterministic-tests/src/tests/13-move-identical-content-ambiguity.test.ts +++ b/frontend/deterministic-tests/src/tests/13-move-identical-content-ambiguity.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const moveIdenticalContentAmbiguityTest: TestDefinition = { @@ -32,7 +31,7 @@ export const moveIdenticalContentAmbiguityTest: TestDefinition = { { type: "assert-consistent", - verify: (state: AssertableState): void => { + verify: (state) => { state .assertFileCount(1) .assertFileNotExists("A.md") diff --git a/frontend/deterministic-tests/src/tests/15-create-update-coalesce-server-pause.test.ts b/frontend/deterministic-tests/src/tests/15-create-update-coalesce-server-pause.test.ts index 9b752d05..608f845d 100644 --- a/frontend/deterministic-tests/src/tests/15-create-update-coalesce-server-pause.test.ts +++ b/frontend/deterministic-tests/src/tests/15-create-update-coalesce-server-pause.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const createUpdateCoalesceServerPauseTest: TestDefinition = { @@ -20,13 +19,6 @@ export const createUpdateCoalesceServerPauseTest: TestDefinition = { { type: "barrier" }, - { - type: "assert-consistent", - verify: (state: AssertableState): void => { - state - .assertFileCount(1) - .assertContent("doc.md", "final version"); - } - } + { type: "assert-consistent", verify: (state) => state.assertFileCount(1).assertContent("doc.md", "final version") } ] }; diff --git a/frontend/deterministic-tests/src/tests/16-create-during-reconciliation.test.ts b/frontend/deterministic-tests/src/tests/16-create-during-reconciliation.test.ts index 0fe51106..54dc3f98 100644 --- a/frontend/deterministic-tests/src/tests/16-create-during-reconciliation.test.ts +++ b/frontend/deterministic-tests/src/tests/16-create-during-reconciliation.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const createDuringReconciliationTest: TestDefinition = { @@ -38,7 +37,7 @@ export const createDuringReconciliationTest: TestDefinition = { { type: "assert-consistent", - verify: (state: AssertableState): void => { + verify: (state) => { state .assertFileCount(3) .assertContent("A.md", "offline A") diff --git a/frontend/deterministic-tests/src/tests/17-create-merge-preserves-renamed-update.test.ts b/frontend/deterministic-tests/src/tests/17-create-merge-preserves-renamed-update.test.ts index a9bc37d4..f600c40e 100644 --- a/frontend/deterministic-tests/src/tests/17-create-merge-preserves-renamed-update.test.ts +++ b/frontend/deterministic-tests/src/tests/17-create-merge-preserves-renamed-update.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const createMergePreservesRenamedUpdateTest: TestDefinition = { @@ -15,13 +14,6 @@ export const createMergePreservesRenamedUpdateTest: TestDefinition = { { type: "enable-sync", client: 1 }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (state: AssertableState): void => { - state.assertContains("doc.md", "alpha", "beta"); - } - }, - { type: "disable-sync", client: 1 }, { @@ -47,13 +39,6 @@ export const createMergePreservesRenamedUpdateTest: TestDefinition = { { type: "enable-sync", client: 1 }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (state: AssertableState): void => { - state - .assertContent("moved.md", "alpha beta extra-update") - .assertContent("doc.md", "new-content"); - } - } + { type: "assert-consistent", verify: (state) => state.assertContent("moved.md", "alpha beta extra-update").assertContent("doc.md", "new-content") } ] }; diff --git a/frontend/deterministic-tests/src/tests/18-create-rename-create-same-path.test.ts b/frontend/deterministic-tests/src/tests/18-create-rename-create-same-path.test.ts index b9e16c90..2b169a1d 100644 --- a/frontend/deterministic-tests/src/tests/18-create-rename-create-same-path.test.ts +++ b/frontend/deterministic-tests/src/tests/18-create-rename-create-same-path.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const createRenameCreateSamePathTest: TestDefinition = { @@ -18,11 +17,12 @@ export const createRenameCreateSamePathTest: TestDefinition = { { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (state: AssertableState): void => { + verify: (state) => { state .assertFileCount(3) .assertContent("B.md", "first file") diff --git a/frontend/deterministic-tests/src/tests/19-move-chain-three-files.test.ts b/frontend/deterministic-tests/src/tests/19-move-chain-three-files.test.ts index fe9267d4..a6c6851b 100644 --- a/frontend/deterministic-tests/src/tests/19-move-chain-three-files.test.ts +++ b/frontend/deterministic-tests/src/tests/19-move-chain-three-files.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const moveChainThreeFilesTest: TestDefinition = { @@ -30,7 +29,7 @@ export const moveChainThreeFilesTest: TestDefinition = { { type: "assert-consistent", - verify: (state: AssertableState): void => { + verify: (state) => { state .assertFileCount(3) .assertContent("A.md", "was C") diff --git a/frontend/deterministic-tests/src/tests/2-binary-pending-create-not-displaced.test.ts b/frontend/deterministic-tests/src/tests/2-binary-pending-create-not-displaced.test.ts index 467c19f0..0616136b 100644 --- a/frontend/deterministic-tests/src/tests/2-binary-pending-create-not-displaced.test.ts +++ b/frontend/deterministic-tests/src/tests/2-binary-pending-create-not-displaced.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const binaryPendingCreateNotDisplacedTest: TestDefinition = { @@ -24,17 +23,6 @@ export const binaryPendingCreateNotDisplacedTest: TestDefinition = { { type: "enable-sync", client: 1 }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(2) - .assertFileExists("data.bin") - .assertFileExists("data (1).bin") - .assertAnyFileContains( - "binary data from client 0", - "binary data from client 1" - ); - } - } + { type: "assert-consistent", verify: (s) => s.assertFileCount(2).assertFileExists("data.bin").assertFileExists("data (1).bin").assertAnyFileContains("binary data from client 0", "binary data from client 1") } ] }; diff --git a/frontend/deterministic-tests/src/tests/3-coalesce-update-remote-update-data-loss.test.ts b/frontend/deterministic-tests/src/tests/3-coalesce-update-remote-update-data-loss.test.ts index 69a5ff10..33fb8107 100644 --- a/frontend/deterministic-tests/src/tests/3-coalesce-update-remote-update-data-loss.test.ts +++ b/frontend/deterministic-tests/src/tests/3-coalesce-update-remote-update-data-loss.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const coalesceUpdateRemoteUpdateDataLossTest: TestDefinition = { @@ -39,14 +38,10 @@ export const coalesceUpdateRemoteUpdateDataLossTest: TestDefinition = { { type: "assert-consistent", - verify: (state: AssertableState): void => { + verify: (state) => { state .assertFileCount(1) - .assertContains( - "doc.md", - "client 0 addition", - "client 1 addition" - ); + .assertContains("doc.md", "client 0 addition", "client 1 addition"); } } ] diff --git a/frontend/deterministic-tests/src/tests/4-coalesced-remote-update-watermark-loss.test.ts b/frontend/deterministic-tests/src/tests/4-coalesced-remote-update-watermark-loss.test.ts index aceb8baa..15fe3e82 100644 --- a/frontend/deterministic-tests/src/tests/4-coalesced-remote-update-watermark-loss.test.ts +++ b/frontend/deterministic-tests/src/tests/4-coalesced-remote-update-watermark-loss.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const coalescedRemoteUpdateWatermarkLossTest: TestDefinition = { @@ -16,14 +15,10 @@ export const coalescedRemoteUpdateWatermarkLossTest: TestDefinition = { { type: "update", client: 0, path: "doc.md", content: "update 1" }, { type: "update", client: 0, path: "doc.md", content: "update 2" }, { type: "update", client: 0, path: "doc.md", content: "final update" }, + { type: "sync", client: 0 }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContent("doc.md", "final update"); - } - }, + { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertContent("doc.md", "final update") }, { type: "disable-sync", client: 0 }, { type: "disable-sync", client: 1 }, @@ -31,23 +26,13 @@ export const coalescedRemoteUpdateWatermarkLossTest: TestDefinition = { { type: "enable-sync", client: 1 }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContent("doc.md", "final update"); - } - }, + { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertContent("doc.md", "final update") }, { type: "disable-sync", client: 0 }, { type: "disable-sync", client: 1 }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContent("doc.md", "final update"); - } - } + { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertContent("doc.md", "final update") } ] }; diff --git a/frontend/deterministic-tests/src/tests/5-concurrent-delete-during-remote-update.test.ts b/frontend/deterministic-tests/src/tests/5-concurrent-delete-during-remote-update.test.ts index 88376f22..3108ecfe 100644 --- a/frontend/deterministic-tests/src/tests/5-concurrent-delete-during-remote-update.test.ts +++ b/frontend/deterministic-tests/src/tests/5-concurrent-delete-during-remote-update.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const concurrentDeleteDuringRemoteUpdateTest: TestDefinition = { @@ -22,11 +21,7 @@ export const concurrentDeleteDuringRemoteUpdateTest: TestDefinition = { { type: "enable-sync", client: 1 }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (state: AssertableState): void => { - state.assertFileCount(0); - } - } + { type: "assert-consistent", verify: (state) => state.assertFileCount(0) } ] }; + diff --git a/frontend/deterministic-tests/src/tests/6-concurrent-edit-exact-same-position.test.ts b/frontend/deterministic-tests/src/tests/6-concurrent-edit-exact-same-position.test.ts index 5c141a0e..08778488 100644 --- a/frontend/deterministic-tests/src/tests/6-concurrent-edit-exact-same-position.test.ts +++ b/frontend/deterministic-tests/src/tests/6-concurrent-edit-exact-same-position.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const concurrentEditExactSamePositionTest: TestDefinition = { @@ -39,7 +38,7 @@ export const concurrentEditExactSamePositionTest: TestDefinition = { { type: "assert-consistent", - verify: (state: AssertableState): void => { + verify: (state) => { state .assertFileCount(1) .assertContains("doc.md", "slow", "fast", "brown fox"); diff --git a/frontend/deterministic-tests/src/tests/7-concurrent-rename-and-create-at-target.test.ts b/frontend/deterministic-tests/src/tests/7-concurrent-rename-and-create-at-target.test.ts index 63dee0db..3e71ed7d 100644 --- a/frontend/deterministic-tests/src/tests/7-concurrent-rename-and-create-at-target.test.ts +++ b/frontend/deterministic-tests/src/tests/7-concurrent-rename-and-create-at-target.test.ts @@ -1,10 +1,10 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const concurrentRenameAndCreateAtTargetTest: TestDefinition = { description: "One client renames X to Y while another creates a new file at Y, " + - "both offline. We can't merge the create because it would result in a cycle", + "both offline. After syncing, Y should contain merged content from " + + "both the renamed file and the newly created file.", clients: 2, steps: [ { @@ -37,15 +37,10 @@ export const concurrentRenameAndCreateAtTargetTest: TestDefinition = { { type: "assert-consistent", - verify: (state: AssertableState): void => { + verify: (state) => { state .assertFileNotExists("X.md") - .assertFileExists("Y.md") - .assertFileExists("Y (1).md") - .assertAnyFileContains( - "original file X", - "brand new Y content" - ); + .assertContains("Y.md", "original file X", "brand new Y content"); } } ] diff --git a/frontend/deterministic-tests/src/tests/8-concurrent-rename-and-create-at-target.test.ts b/frontend/deterministic-tests/src/tests/8-concurrent-rename-and-create-at-target.test.ts index a6f34102..9f0b0318 100644 --- a/frontend/deterministic-tests/src/tests/8-concurrent-rename-and-create-at-target.test.ts +++ b/frontend/deterministic-tests/src/tests/8-concurrent-rename-and-create-at-target.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const concurrentRenameAndCreateAtTargetTest: TestDefinition = { @@ -38,7 +37,7 @@ export const concurrentRenameAndCreateAtTargetTest: TestDefinition = { { type: "assert-consistent", - verify: (state: AssertableState): void => { + verify: (state) => { state .assertFileCount(2) .assertContains("Y (1).md", "original file X") diff --git a/frontend/deterministic-tests/src/tests/9-concurrent-rename-same-target.test.ts b/frontend/deterministic-tests/src/tests/9-concurrent-rename-same-target.test.ts index 0b72c0f3..230c7a1d 100644 --- a/frontend/deterministic-tests/src/tests/9-concurrent-rename-same-target.test.ts +++ b/frontend/deterministic-tests/src/tests/9-concurrent-rename-same-target.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const concurrentRenameSameTargetTest: TestDefinition = { @@ -21,11 +20,12 @@ export const concurrentRenameSameTargetTest: TestDefinition = { { type: "rename", client: 1, oldPath: "B.md", newPath: "C.md" }, { type: "enable-sync", client: 1 }, + { type: "sync", client: 1 }, { type: "barrier" }, { type: "assert-consistent", - verify: (state: AssertableState): void => { + verify: (state) => { state .assertFileCount(2) .assertFileNotExists("A.md") diff --git a/frontend/deterministic-tests/src/tests/binary-to-text-transition.test.ts b/frontend/deterministic-tests/src/tests/binary-to-text-transition.test.ts index 8b934c1b..f6e14152 100644 --- a/frontend/deterministic-tests/src/tests/binary-to-text-transition.test.ts +++ b/frontend/deterministic-tests/src/tests/binary-to-text-transition.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const binaryToTextTransitionTest: TestDefinition = { @@ -9,21 +8,11 @@ export const binaryToTextTransitionTest: TestDefinition = { "offline. The text merge should preserve both edits.", clients: 2, steps: [ - { - type: "create", - client: 0, - path: "data.bin", - content: "original content" - }, + { type: "create", client: 0, path: "data.bin", content: "original content" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent("data.bin", "original content"); - } - }, + { type: "assert-consistent", verify: (s) => s.assertContent("data.bin", "original content") }, { type: "disable-sync", client: 0 }, { type: "disable-sync", client: 1 }, @@ -35,63 +24,26 @@ export const binaryToTextTransitionTest: TestDefinition = { { type: "enable-sync", client: 1 }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContainsAny( - "data.bin", - "version A", - "version B" - ); - } - }, + { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertContainsAny("data.bin", "version A", "version B") }, { type: "disable-sync", client: 1 }, { type: "rename", client: 0, oldPath: "data.bin", newPath: "data.md" }, - { - type: "update", - client: 0, - path: "data.md", - content: "top line\nmiddle line\nbottom line" - }, + { type: "update", client: 0, path: "data.md", content: "top line\nmiddle line\nbottom line" }, { type: "sync", client: 0 }, { type: "enable-sync", client: 1 }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent( - "data.md", - "top line\nmiddle line\nbottom line" - ); - } - }, + { type: "assert-consistent", verify: (s) => s.assertContent("data.md", "top line\nmiddle line\nbottom line") }, { type: "disable-sync", client: 0 }, { type: "disable-sync", client: 1 }, - { - type: "update", - client: 0, - path: "data.md", - content: "alpha\nmiddle line\nbottom line" - }, - { - type: "update", - client: 1, - path: "data.md", - content: "top line\nmiddle line\nbeta" - }, + { type: "update", client: 0, path: "data.md", content: "alpha\nmiddle line\nbottom line" }, + { type: "update", client: 1, path: "data.md", content: "top line\nmiddle line\nbeta" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContains("data.md", "alpha", "beta"); - } - } - ] + { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertContains("data.md", "alpha", "beta") }, + ], }; diff --git a/frontend/deterministic-tests/src/tests/catchup-create-and-update-not-skipped.test.ts b/frontend/deterministic-tests/src/tests/catchup-create-and-update-not-skipped.test.ts deleted file mode 100644 index 2d40228f..00000000 --- a/frontend/deterministic-tests/src/tests/catchup-create-and-update-not-skipped.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { AssertableState } from "../utils/assertable-state"; -import type { TestDefinition } from "../test-definition"; - -export const catchupCreateAndUpdateNotSkippedTest: TestDefinition = { - description: - "Client 1 disconnects (sync disabled). Client 0 creates a doc and " + - "then updates it. When Client 1 reconnects, the server's catch-up " + - "stream sends only the doc's *latest* version (the update), not the " + - "full history. Pre-fix the wire's `is_new_file` was set to " + - "`creation == latest_version`, so the catch-up flagged the doc as " + - "non-new even though Client 1 had never seen its creation. Client " + - "1's `processRemoteChange` then dropped it as a 'stale RemoteChange " + - "for untracked, non-new document' and the doc was silently lost. " + - "Post-fix `is_new_file` in the catch-up stream means 'new relative " + - "to the recipient's watermark' (`creation > last_seen_vault_update_id`).", - clients: 2, - steps: [ - { type: "enable-sync", client: 0 }, - { type: "enable-sync", client: 1 }, - // Establish a baseline so Client 1's last_seen is non-zero before - // we take it offline. This makes the bug genuinely about catch-up - // missing the create rather than just an empty-vault first sync. - { type: "create", client: 0, path: "warmup.md", content: "w\n" }, - { type: "barrier" }, - - // Client 1 goes offline. - { type: "disable-sync", client: 1 }, - - // Client 0 creates the doc (vault_update_id v_C, after Client 1's - // watermark). Client 1 doesn't see this because it's offline. - { type: "create", client: 0, path: "doc.md", content: "v1\n" }, - // Wait for the create's HTTP to land before the update; otherwise - // both writes are coalesced into a single POST and the server - // never sees the doc as "create followed by update". - { type: "sync", client: 0 }, - - // Client 0 updates the doc (vault_update_id v_X > v_C). The - // server's `latest_document_versions` view now returns the - // *update* row — its `creation_vault_update_id != vault_update_id`. - { - type: "update", - client: 0, - path: "doc.md", - content: "v1\nupdate\n" - }, - { type: "sync", client: 0 }, - - // Client 1 reconnects. Server's catch-up replays docs with - // `vault_update_id > last_seen`. For doc.md it sends v_X with - // `is_new_file` derived from `creation_vault_update_id > - // last_seen_vault_update_id` (post-fix) — so Client 1 treats it - // as a fresh create and downloads the latest content. - { type: "enable-sync", client: 1 }, - { type: "barrier" }, - - { - type: "assert-consistent", - verify: (state: AssertableState): void => { - state.assertFileCount(2); - state.assertFileExists("doc.md"); - state.assertContent("doc.md", "v1\nupdate\n"); - state.assertContent("warmup.md", "w\n"); - } - } - ] -}; diff --git a/frontend/deterministic-tests/src/tests/concurrent-rename-first-wins.test.ts b/frontend/deterministic-tests/src/tests/concurrent-rename-first-wins.test.ts index 5337649d..1dddcf7a 100644 --- a/frontend/deterministic-tests/src/tests/concurrent-rename-first-wins.test.ts +++ b/frontend/deterministic-tests/src/tests/concurrent-rename-first-wins.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const concurrentRenameFirstWinsTest: TestDefinition = { @@ -9,53 +8,29 @@ export const concurrentRenameFirstWinsTest: TestDefinition = { "edits are merged.", clients: 2, steps: [ - { - type: "create", - client: 0, - path: "A.md", - content: "line 1\nline 2\nline 3" - }, + { type: "create", client: 0, path: "A.md", content: "line 1\nline 2\nline 3" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent("A.md", "line 1\nline 2\nline 3"); - } - }, + { type: "assert-consistent", verify: (s) => s.assertContent("A.md", "line 1\nline 2\nline 3") }, { type: "disable-sync", client: 0 }, { type: "disable-sync", client: 1 }, { type: "rename", client: 0, oldPath: "A.md", newPath: "B.md" }, - { - type: "update", - client: 0, - path: "B.md", - content: "edit from 0\nline 2\nline 3" - }, + { type: "update", client: 0, path: "B.md", content: "edit from 0\nline 2\nline 3" }, { type: "rename", client: 1, oldPath: "A.md", newPath: "C.md" }, - { - type: "update", - client: 1, - path: "C.md", - content: "line 1\nline 2\nedit from 1" - }, + { type: "update", client: 1, path: "C.md", content: "line 1\nline 2\nedit from 1" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileNotExists("A.md") - .assertFileCount(2) - .assertContent("B.md", "edit from 0\nline 2\nline 3") - .assertContent("C.md", "line 1\nline 2\nedit from 1"); - } - } - ] + { type: "assert-consistent", verify: (s) => { + s.assertFileNotExists("A.md"); + s.assertFileCount(1); + s.assertAnyFileContains("edit from 0", "edit from 1"); + } }, + ], }; diff --git a/frontend/deterministic-tests/src/tests/create-rename-response-skips-file.test.ts b/frontend/deterministic-tests/src/tests/create-rename-response-skips-file.test.ts index aa24b110..5bec2bcb 100644 --- a/frontend/deterministic-tests/src/tests/create-rename-response-skips-file.test.ts +++ b/frontend/deterministic-tests/src/tests/create-rename-response-skips-file.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const createRenameResponseSkipsFileTest: TestDefinition = { @@ -9,6 +8,8 @@ export const createRenameResponseSkipsFileTest: TestDefinition = { steps: [ { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, + { type: "barrier" }, { type: "create", @@ -24,13 +25,10 @@ export const createRenameResponseSkipsFileTest: TestDefinition = { newPath: "renamed.md" }, + { type: "sync" }, + { type: "sync" }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1).assertAnyFileContains("the-content"); - } - } + { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertAnyFileContains("the-content") } ] }; diff --git a/frontend/deterministic-tests/src/tests/delete-by-other-client-then-recreate.test.ts b/frontend/deterministic-tests/src/tests/delete-by-other-client-then-recreate.test.ts index dfef9961..204e9896 100644 --- a/frontend/deterministic-tests/src/tests/delete-by-other-client-then-recreate.test.ts +++ b/frontend/deterministic-tests/src/tests/delete-by-other-client-then-recreate.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const deleteByOtherClientThenRecreateTest: TestDefinition = { @@ -15,26 +14,11 @@ export const deleteByOtherClientThenRecreateTest: TestDefinition = { { type: "delete", client: 1, path: "A.md" }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileNotExists("A.md"); - } - }, + { type: "assert-consistent", verify: (s) => s.assertFileNotExists("A.md") }, - { - type: "create", - client: 0, - path: "A.md", - content: "recreated by client 0" - }, + { type: "create", client: 0, path: "A.md", content: "recreated by client 0" }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent("A.md", "recreated by client 0"); - } - } - ] + { type: "assert-consistent", verify: (s) => s.assertContent("A.md", "recreated by client 0") }, + ], }; diff --git a/frontend/deterministic-tests/src/tests/delete-during-pending-create.test.ts b/frontend/deterministic-tests/src/tests/delete-during-pending-create.test.ts index 3ba393b8..f6236060 100644 --- a/frontend/deterministic-tests/src/tests/delete-during-pending-create.test.ts +++ b/frontend/deterministic-tests/src/tests/delete-during-pending-create.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const deleteDuringPendingCreateTest: TestDefinition = { @@ -9,6 +8,7 @@ export const deleteDuringPendingCreateTest: TestDefinition = { steps: [ { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "pause-server" }, @@ -23,13 +23,9 @@ export const deleteDuringPendingCreateTest: TestDefinition = { { type: "delete", client: 0, path: "ephemeral.md" }, { type: "resume-server" }, + { type: "sync" }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(0).assertFileNotExists("ephemeral.md"); - } - } + { type: "assert-consistent", verify: (s) => s.assertFileCount(0).assertFileNotExists("ephemeral.md") } ] }; diff --git a/frontend/deterministic-tests/src/tests/delete-recreate-concurrent-update.test.ts b/frontend/deterministic-tests/src/tests/delete-recreate-concurrent-update.test.ts index 6cb4cb98..c95c6aa4 100644 --- a/frontend/deterministic-tests/src/tests/delete-recreate-concurrent-update.test.ts +++ b/frontend/deterministic-tests/src/tests/delete-recreate-concurrent-update.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const deleteRecreateConcurrentUpdateTest: TestDefinition = { @@ -10,16 +9,12 @@ export const deleteRecreateConcurrentUpdateTest: TestDefinition = { { type: "create", client: 0, path: "A.md", content: "original" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "disable-sync", client: 0 }, { type: "delete", client: 0, path: "A.md" }, - { - type: "create", - client: 0, - path: "A.md", - content: "recreated by client 0" - }, + { type: "create", client: 0, path: "A.md", content: "recreated by client 0" }, { type: "update", @@ -30,13 +25,9 @@ export const deleteRecreateConcurrentUpdateTest: TestDefinition = { { type: "sync", client: 1 }, { type: "enable-sync", client: 0 }, + { type: "sync" }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileExists("A.md").assertContains("A.md", "recreated"); - } - } + { type: "assert-consistent", verify: (s) => s.assertFileExists("A.md").assertContains("A.md", "recreated") } ] }; diff --git a/frontend/deterministic-tests/src/tests/delete-recreate-different-content.test.ts b/frontend/deterministic-tests/src/tests/delete-recreate-different-content.test.ts index 782c3cd5..02197b8d 100644 --- a/frontend/deterministic-tests/src/tests/delete-recreate-different-content.test.ts +++ b/frontend/deterministic-tests/src/tests/delete-recreate-different-content.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const deleteRecreateDifferentContentTest: TestDefinition = { @@ -15,6 +14,7 @@ export const deleteRecreateDifferentContentTest: TestDefinition = { }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "disable-sync", client: 0 }, @@ -38,17 +38,9 @@ export const deleteRecreateDifferentContentTest: TestDefinition = { { type: "enable-sync", client: 0 }, { type: "sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContains( - "A.md", - "brand new", - "client 1" - ); - } - } + { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertContains("A.md", "brand new", "client 1") } ] }; diff --git a/frontend/deterministic-tests/src/tests/delete-recreate-same-path.test.ts b/frontend/deterministic-tests/src/tests/delete-recreate-same-path.test.ts index dde8d341..10b00f70 100644 --- a/frontend/deterministic-tests/src/tests/delete-recreate-same-path.test.ts +++ b/frontend/deterministic-tests/src/tests/delete-recreate-same-path.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const deleteRecreateSamePathTest: TestDefinition = { @@ -10,25 +9,17 @@ export const deleteRecreateSamePathTest: TestDefinition = { { type: "create", client: 0, path: "A.md", content: "version 1" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent("A.md", "version 1"); - } - }, + { type: "assert-consistent", verify: (s) => s.assertContent("A.md", "version 1") }, { type: "disable-sync", client: 0 }, { type: "delete", client: 0, path: "A.md" }, { type: "create", client: 0, path: "A.md", content: "version 2" }, { type: "enable-sync", client: 0 }, + { type: "sync" }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent("A.md", "version 2"); - } - } + { type: "assert-consistent", verify: (s) => s.assertContent("A.md", "version 2") } ] }; diff --git a/frontend/deterministic-tests/src/tests/delete-recreated-pending-create-with-stale-deleting-record.test.ts b/frontend/deterministic-tests/src/tests/delete-recreated-pending-create-with-stale-deleting-record.test.ts deleted file mode 100644 index 80e95f48..00000000 --- a/frontend/deterministic-tests/src/tests/delete-recreated-pending-create-with-stale-deleting-record.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { AssertableState } from "../utils/assertable-state"; -import type { TestDefinition } from "../test-definition"; - -export const deleteRecreatedPendingCreateWithStaleDeletingRecordTest: TestDefinition = - { - description: - "A local delete for a recreated pending create must target the " + - "new pending create, not an older same-path record whose server " + - "delete has been acked but whose WebSocket delete receipt is " + - "still paused.", - clients: 2, - steps: [ - { type: "enable-sync", client: 0 }, - { type: "enable-sync", client: 1 }, - { type: "barrier" }, - - { type: "pause-websocket", client: 0 }, - { type: "pause-server" }, - { - type: "create", - client: 0, - path: "binary-14.bin", - content: "BINARY:first" - }, - { type: "sleep", ms: 100 }, - { type: "delete", client: 0, path: "binary-14.bin" }, - { type: "resume-server" }, - { type: "sync", client: 0 }, - - { type: "pause-server" }, - { - type: "create", - client: 0, - path: "binary-14.bin", - content: "BINARY:second" - }, - { type: "sleep", ms: 100 }, - { type: "delete", client: 0, path: "binary-14.bin" }, - { type: "resume-server" }, - { type: "sync", client: 0 }, - - { type: "resume-websocket", client: 0 }, - { type: "barrier" }, - - { - type: "assert-consistent", - verify: (state: AssertableState): void => { - state.assertFileCount(0); - } - } - ] - }; diff --git a/frontend/deterministic-tests/src/tests/delete-rename-conflict.test.ts b/frontend/deterministic-tests/src/tests/delete-rename-conflict.test.ts index 91e6289b..4cbeed25 100644 --- a/frontend/deterministic-tests/src/tests/delete-rename-conflict.test.ts +++ b/frontend/deterministic-tests/src/tests/delete-rename-conflict.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const deleteRenameConflictTest: TestDefinition = { @@ -11,13 +10,9 @@ export const deleteRenameConflictTest: TestDefinition = { { type: "create", client: 0, path: "B.md", content: "content-b" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileExists("A.md").assertFileExists("B.md"); - } - }, + { type: "assert-consistent", verify: (s) => s.assertFileExists("A.md").assertFileExists("B.md") }, { type: "disable-sync", client: 1 }, @@ -27,17 +22,13 @@ export const deleteRenameConflictTest: TestDefinition = { { type: "rename", client: 1, oldPath: "A.md", newPath: "C.md" }, { type: "enable-sync", client: 1 }, + { type: "sync", client: 1 }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent("B.md", "content-b"); - s.assertFileNotExists("A.md"); - s.ifFileExists("C.md", (inner) => - inner.assertContent("C.md", "content-a") - ); - } - } + { type: "assert-consistent", verify: (s) => { + s.assertContent("B.md", "content-b"); + s.assertFileNotExists("A.md"); + s.ifFileExists("C.md", (s) => s.assertContent("C.md", "content-a")); + } }, ] }; diff --git a/frontend/deterministic-tests/src/tests/displaced-file-not-marked-deleted.test.ts b/frontend/deterministic-tests/src/tests/displaced-file-not-marked-deleted.test.ts index cb995243..99d5f716 100644 --- a/frontend/deterministic-tests/src/tests/displaced-file-not-marked-deleted.test.ts +++ b/frontend/deterministic-tests/src/tests/displaced-file-not-marked-deleted.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const displacedFileNotMarkedDeletedTest: TestDefinition = { @@ -16,22 +15,25 @@ export const displacedFileNotMarkedDeletedTest: TestDefinition = { { type: "disable-sync", client: 1 }, - { type: "create", client: 0, path: "B.md", content: "content of B" }, + { type: "create", client: 0, path: "B.md", content: "new file B" }, { type: "rename", client: 0, oldPath: "A.md", newPath: "C.md" }, { type: "sync", client: 0 }, { type: "rename", client: 1, oldPath: "A.md", newPath: "B.md" }, + { type: "update", client: 1, path: "B.md", content: "edited A content" }, { type: "enable-sync", client: 1 }, { type: "barrier" }, { type: "assert-consistent", - verify: (state: AssertableState): void => { + verify: (state) => { state - .assertFileCount(2) - .assertContent("B.md", "content of B") - .assertContent("C.md", "content of A"); + .assertFileNotExists("A.md") + .assertFileExists("B.md") + .assertContains("B.md", "new file B") + .assertFileExists("C.md") + .assertContains("C.md", "edited A content"); } } ] diff --git a/frontend/deterministic-tests/src/tests/double-offline-cycle.test.ts b/frontend/deterministic-tests/src/tests/double-offline-cycle.test.ts index 744d862e..1034ce27 100644 --- a/frontend/deterministic-tests/src/tests/double-offline-cycle.test.ts +++ b/frontend/deterministic-tests/src/tests/double-offline-cycle.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const doubleOfflineCycleTest: TestDefinition = { @@ -15,13 +14,9 @@ export const doubleOfflineCycleTest: TestDefinition = { }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent("doc.md", "initial"); - } - }, + { type: "assert-consistent", verify: (s) => s.assertContent("doc.md", "initial") }, { type: "disable-sync", client: 0 }, { @@ -32,13 +27,9 @@ export const doubleOfflineCycleTest: TestDefinition = { }, { type: "enable-sync", client: 0 }, + { type: "sync" }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent("doc.md", "first edit"); - } - }, + { type: "assert-consistent", verify: (s) => s.assertContent("doc.md", "first edit") }, { type: "disable-sync", client: 0 }, { @@ -49,13 +40,9 @@ export const doubleOfflineCycleTest: TestDefinition = { }, { type: "enable-sync", client: 0 }, + { type: "sync" }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent("doc.md", "second edit"); - } - }, + { type: "assert-consistent", verify: (s) => s.assertContent("doc.md", "second edit") }, { type: "disable-sync", client: 0 }, { @@ -66,12 +53,8 @@ export const doubleOfflineCycleTest: TestDefinition = { }, { type: "enable-sync", client: 0 }, + { type: "sync" }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContent("doc.md", "third edit"); - } - } + { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertContent("doc.md", "third edit") } ] }; diff --git a/frontend/deterministic-tests/src/tests/failed-vfs-move-falls-back.test.ts b/frontend/deterministic-tests/src/tests/failed-vfs-move-falls-back.test.ts new file mode 100644 index 00000000..f9ae2a3f --- /dev/null +++ b/frontend/deterministic-tests/src/tests/failed-vfs-move-falls-back.test.ts @@ -0,0 +1,22 @@ +import type { TestDefinition } from "../test-definition"; + +export const failedVfsMoveFallsBackTest: TestDefinition = { + description: + "File A is renamed to B's path (overwriting B). Both clients " + + "should converge on a single file at B.md with A's content.", + clients: 2, + steps: [ + { type: "create", client: 0, path: "A.md", content: "content A" }, + { type: "create", client: 0, path: "B.md", content: "content B" }, + { type: "enable-sync", client: 0 }, + { type: "enable-sync", client: 1 }, + { type: "sync" }, + { type: "barrier" }, + + { type: "rename", client: 0, oldPath: "A.md", newPath: "B.md" }, + { type: "sync" }, + { type: "barrier" }, + + { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertContent("B.md", "content A") } + ] +}; diff --git a/frontend/deterministic-tests/src/tests/idempotency-after-server-pause.test.ts b/frontend/deterministic-tests/src/tests/idempotency-after-server-pause.test.ts index 551c702d..ce12df0c 100644 --- a/frontend/deterministic-tests/src/tests/idempotency-after-server-pause.test.ts +++ b/frontend/deterministic-tests/src/tests/idempotency-after-server-pause.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const idempotencyAfterServerPauseTest: TestDefinition = { @@ -9,25 +8,17 @@ export const idempotencyAfterServerPauseTest: TestDefinition = { steps: [ { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, - { - type: "create", - client: 0, - path: "doc.md", - content: "important data" - }, + { type: "create", client: 0, path: "doc.md", content: "important data" }, { type: "pause-server" }, { type: "resume-server" }, + { type: "sync" }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContent("doc.md", "important data"); - } - } + { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertContent("doc.md", "important data") } ] }; diff --git a/frontend/deterministic-tests/src/tests/interrupted-delete-retry.test.ts b/frontend/deterministic-tests/src/tests/interrupted-delete-retry.test.ts index 3ae7eda5..ef8404fb 100644 --- a/frontend/deterministic-tests/src/tests/interrupted-delete-retry.test.ts +++ b/frontend/deterministic-tests/src/tests/interrupted-delete-retry.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const interruptedDeleteRetryTest: TestDefinition = { @@ -10,6 +9,7 @@ export const interruptedDeleteRetryTest: TestDefinition = { { type: "create", client: 0, path: "doc.md", content: "to be deleted" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "delete", client: 0, path: "doc.md" }, @@ -17,13 +17,9 @@ export const interruptedDeleteRetryTest: TestDefinition = { { type: "pause-server" }, { type: "resume-server" }, + { type: "sync" }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(0); - } - } - ] + { type: "assert-consistent", verify: (s) => s.assertFileCount(0) }, + ], }; diff --git a/frontend/deterministic-tests/src/tests/key-migration-event-drop.test.ts b/frontend/deterministic-tests/src/tests/key-migration-event-drop.test.ts index cc40e6b0..9d9a870d 100644 --- a/frontend/deterministic-tests/src/tests/key-migration-event-drop.test.ts +++ b/frontend/deterministic-tests/src/tests/key-migration-event-drop.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const keyMigrationEventDropTest: TestDefinition = { @@ -9,6 +8,7 @@ export const keyMigrationEventDropTest: TestDefinition = { steps: [ { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "pause-server" }, @@ -27,13 +27,9 @@ export const keyMigrationEventDropTest: TestDefinition = { }, { type: "resume-server" }, + { type: "sync" }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContent("A.md", "updated content"); - } - } + { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertContent("A.md", "updated content") } ] }; diff --git a/frontend/deterministic-tests/src/tests/local-edit-lost-during-create-merge.test.ts b/frontend/deterministic-tests/src/tests/local-edit-lost-during-create-merge.test.ts index 20925889..66c832db 100644 --- a/frontend/deterministic-tests/src/tests/local-edit-lost-during-create-merge.test.ts +++ b/frontend/deterministic-tests/src/tests/local-edit-lost-during-create-merge.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const localEditLostDuringCreateMergeTest: TestDefinition = { @@ -29,13 +28,12 @@ export const localEditLostDuringCreateMergeTest: TestDefinition = { { type: "assert-consistent", - verify: (s: AssertableState): void => { + verify: (s) => s.assertFileCount(1).assertContains( "doc.md", "from-client-1", "local-edit-during-create" - ); - } + ), } ] }; diff --git a/frontend/deterministic-tests/src/tests/local-rename-survives-remote-rename.test.ts b/frontend/deterministic-tests/src/tests/local-rename-survives-remote-rename.test.ts deleted file mode 100644 index c2b80af3..00000000 --- a/frontend/deterministic-tests/src/tests/local-rename-survives-remote-rename.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { AssertableState } from "../utils/assertable-state"; -import type { TestDefinition } from "../test-definition"; - -export const localRenameSurvivesRemoteRenameTest: TestDefinition = { - description: - "Drain processes a RemoteChange (remote rename for doc D) while a " + - "LocalUpdate (user rename of D) is also queued behind it. " + - "`processRemoteUpdate` moves the disk file and, because there is a " + - "pending LocalUpdate, takes the else branch — but its setDocument " + - "uses the stale `record.path` (= the user-rename target) instead of " + - "the actualPath the file just moved to. The queued LocalUpdate then " + - "reads from `record.path`, throws FileNotFoundError, and is " + - "silently dropped. Setup pins the queue order: a sentinel " + - "LocalUpdate keeps drain busy on a SIGSTOPped HTTP roundtrip while " + - "we resume client 0's WebSocket (enqueues RemoteChange) and then " + - "user-rename D (enqueues LocalUpdate after the RemoteChange). On " + - "server resume the drain pops the sentinel, then RemoteChange, then " + - "LocalUpdate — exactly the order that triggers the bug.", - clients: 2, - steps: [ - { type: "enable-sync", client: 0 }, - { type: "enable-sync", client: 1 }, - { type: "create", client: 0, path: "doc.md", content: "v1\n" }, - { type: "create", client: 0, path: "sentinel.md", content: "s\n" }, - { type: "barrier" }, - - // Pause client 0's WebSocket so the upcoming remote rename buffers. - { type: "pause-websocket", client: 0 }, - - // Server applies remote rename of doc.md -> remote.md. Broadcast - // is buffered on client 0's WebSocket. - { type: "rename", client: 1, oldPath: "doc.md", newPath: "remote.md" }, - { type: "sync", client: 1 }, - - // Pause the server BEFORE arming the sentinel, so the sentinel's - // HTTP request will buffer at the kernel and keep drain occupied. - { type: "pause-server" }, - - // Sentinel: a LocalUpdate on a *different* doc that drain pops - // first. Its HTTP roundtrip stalls on SIGSTOP, freezing drain - // until we resume the server. While drain is frozen we can grow - // the queue with additional events whose order we control. - { - type: "update", - client: 0, - path: "sentinel.md", - content: "s\nedit\n" - }, - - // Resume the WebSocket — buffered remote rename enqueues as a - // RemoteChange. Drain is still stuck on the sentinel HTTP. - { type: "resume-websocket", client: 0 }, - - // User renames doc.md -> local.md on client 0. queue.enqueue - // mutates the doc's record.path to "local.md" and pushes a - // LocalUpdate(rename) onto the tail of the queue. Queue is now - // [sentinel-update (in-flight), RemoteChange, LocalUpdate-rename]. - { type: "rename", client: 0, oldPath: "doc.md", newPath: "local.md" }, - - // Resume the server. Drain pops sentinel-update (succeeds), then - // RemoteChange. Pre-fix: processRemoteUpdate moves disk - // local.md -> remote.md, takes the else branch, and - // setDocument(record.path = "local.md", …) leaves record.path - // stale. Drain pops the LocalUpdate-rename and reads from the - // stale record.path, hits FileNotFoundError, silent skip. - // Post-fix: when a local event is pending, we re-queue the - // remote update without touching disk or record, so the local - // rename drains first and both ends converge. - { type: "resume-server" }, - - { type: "barrier" }, - - { - type: "assert-consistent", - verify: (state: AssertableState): void => { - state.assertFileCount(2); - } - } - ] -}; diff --git a/frontend/deterministic-tests/src/tests/local-update-survives-remote-rename.test.ts b/frontend/deterministic-tests/src/tests/local-update-survives-remote-rename.test.ts deleted file mode 100644 index 0d8348c0..00000000 --- a/frontend/deterministic-tests/src/tests/local-update-survives-remote-rename.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { AssertableState } from "../utils/assertable-state"; -import type { TestDefinition } from "../test-definition"; - -export const localUpdateSurvivesRemoteRenameTest: TestDefinition = { - description: - "Client 0 has a local content edit pending while a remote rename for " + - "the same doc arrives over the WebSocket. The remote rename's internal " + - "move relocates the disk file from the old path (where the user wrote) " + - "to the new server path. Previously, the queued LocalUpdate's " + - "`event.path` was left pointing at the now-vacated old path, so " + - "`skipIfOversized`'s `getFileSize(event.path)` threw " + - "`FileNotFoundError`, which `processEvent`'s catch silently swallowed " + - "as 'Skipping sync event 'local-update' because the file no longer " + - "exists' — and the user's edit was lost. The fix routes the size " + - "check through `tracked.path` (the doc's current disk path), " + - "matching the path `processLocalUpdate` itself reads from.", - clients: 2, - steps: [ - { type: "create", client: 0, path: "doc.md", content: "v1\n" }, - { type: "enable-sync", client: 0 }, - { type: "enable-sync", client: 1 }, - { type: "barrier" }, - - // Pause client 0's WebSocket so the upcoming remote rename buffers - // there until we've already enqueued client 0's local content - // edit. This guarantees the LocalUpdate sits in client 0's queue - // when the rename's RemoteChange drains. - { type: "pause-websocket", client: 0 }, - - { - type: "rename", - client: 1, - oldPath: "doc.md", - newPath: "renamed.md" - }, - { type: "sync", client: 1 }, - - // Client 0 still believes the file is at `doc.md` (its WebSocket is - // paused, so the rename hasn't reached it). The user edits content - // at `doc.md`. This pushes a LocalUpdate(D, path=doc.md, - // originalPath=doc.md, isUserRename=false) into client 0's queue. - { - type: "update", - client: 0, - path: "doc.md", - content: "v1\nclient 0 edit\n" - }, - - // Resume the WebSocket. The buffered remote rename (server-broadcast) - // drains. `processRemoteUpdate` does an internal `move(doc.md, - // renamed.md)` and, because there's a pending LocalUpdate for D, - // takes the else branch (re-enqueue v_K, setDocument(renamed.md, …)). - // Then drain reaches the LocalUpdate. Pre-fix: skipped silently. - // Post-fix: PUTs the user's content to the doc (at its new path, - // since this is a content-only edit, not a user rename). - { type: "resume-websocket", client: 0 }, - - { type: "barrier" }, - - { - type: "assert-consistent", - verify: (state: AssertableState): void => { - state.assertFileCount(1); - state.assertFileExists("renamed.md"); - state.assertContent("renamed.md", "v1\nclient 0 edit\n"); - } - } - ] -}; diff --git a/frontend/deterministic-tests/src/tests/mc-cross-create-rename-same-target.test.ts b/frontend/deterministic-tests/src/tests/mc-cross-create-rename-same-target.test.ts index d986a733..ce991df3 100644 --- a/frontend/deterministic-tests/src/tests/mc-cross-create-rename-same-target.test.ts +++ b/frontend/deterministic-tests/src/tests/mc-cross-create-rename-same-target.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const mcCrossCreateRenameSameTargetTest: TestDefinition = { @@ -13,13 +12,12 @@ export const mcCrossCreateRenameSameTargetTest: TestDefinition = { { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileExists("X.md").assertFileExists("Y.md"); - } + verify: (s) => s.assertFileExists("X.md").assertFileExists("Y.md") }, { type: "disable-sync", client: 1 }, @@ -30,11 +28,12 @@ export const mcCrossCreateRenameSameTargetTest: TestDefinition = { { type: "rename", client: 1, oldPath: "Y.md", newPath: "Z.md" }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { + verify: (s) => { s.assertFileCount(2) .assertFileNotExists("X.md") .assertFileNotExists("Y.md") diff --git a/frontend/deterministic-tests/src/tests/mc-delete-then-offline-rename.test.ts b/frontend/deterministic-tests/src/tests/mc-delete-then-offline-rename.test.ts index 6727e99d..98504f03 100644 --- a/frontend/deterministic-tests/src/tests/mc-delete-then-offline-rename.test.ts +++ b/frontend/deterministic-tests/src/tests/mc-delete-then-offline-rename.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const mcDeleteThenOfflineRenameTest: TestDefinition = { @@ -12,6 +11,7 @@ export const mcDeleteThenOfflineRenameTest: TestDefinition = { { type: "create", client: 0, path: "C.md", content: "unrelated" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "disable-sync", client: 1 }, @@ -22,17 +22,15 @@ export const mcDeleteThenOfflineRenameTest: TestDefinition = { { type: "rename", client: 1, oldPath: "A.md", newPath: "B.md" }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent("C.md", "unrelated").assertFileNotExists( - "A.md" - ); - s.ifFileExists("B.md", (inner) => - inner.assertContent("B.md", "original") - ); + verify: (s) => { + s.assertContent("C.md", "unrelated") + .assertFileNotExists("A.md"); + s.ifFileExists("B.md", (s) => s.assertContent("B.md", "original")); } } ] diff --git a/frontend/deterministic-tests/src/tests/mc-multi-delete-offline-rename.test.ts b/frontend/deterministic-tests/src/tests/mc-multi-delete-offline-rename.test.ts index 8db90aab..26a095d5 100644 --- a/frontend/deterministic-tests/src/tests/mc-multi-delete-offline-rename.test.ts +++ b/frontend/deterministic-tests/src/tests/mc-multi-delete-offline-rename.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const mcMultiDeleteOfflineRenameTest: TestDefinition = { @@ -14,6 +13,7 @@ export const mcMultiDeleteOfflineRenameTest: TestDefinition = { { type: "create", client: 0, path: "file-5.md", content: "content-5" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "disable-sync", client: 0 }, @@ -22,27 +22,21 @@ export const mcMultiDeleteOfflineRenameTest: TestDefinition = { { type: "delete", client: 1, path: "file-4.md" }, { type: "sync", client: 1 }, - { - type: "rename", - client: 0, - oldPath: "file-2.md", - newPath: "renamed.md" - }, + { type: "rename", client: 0, oldPath: "file-2.md", newPath: "renamed.md" }, { type: "enable-sync", client: 0 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { + verify: (s) => { s.assertFileExists("file-1.md") .assertFileExists("file-3.md") .assertFileExists("file-5.md") .assertFileNotExists("file-2.md") .assertFileNotExists("file-4.md"); - s.ifFileExists("renamed.md", (inner) => - inner.assertContent("renamed.md", "content-2") - ); + s.ifFileExists("renamed.md", (s) => s.assertContent("renamed.md", "content-2")); } } ] diff --git a/frontend/deterministic-tests/src/tests/mc-three-client-rename-offline-update.test.ts b/frontend/deterministic-tests/src/tests/mc-three-client-rename-offline-update.test.ts index 4167b925..8144bbb5 100644 --- a/frontend/deterministic-tests/src/tests/mc-three-client-rename-offline-update.test.ts +++ b/frontend/deterministic-tests/src/tests/mc-three-client-rename-offline-update.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const mcThreeClientRenameOfflineUpdateTest: TestDefinition = { @@ -11,6 +10,7 @@ export const mcThreeClientRenameOfflineUpdateTest: TestDefinition = { { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, { type: "enable-sync", client: 2 }, + { type: "sync" }, { type: "barrier" }, { type: "disable-sync", client: 2 }, @@ -19,23 +19,12 @@ export const mcThreeClientRenameOfflineUpdateTest: TestDefinition = { { type: "sync", client: 1 }, { type: "sync", client: 0 }, - { - type: "update", - client: 2, - path: "A.md", - content: "updated-by-client-2" - }, + { type: "update", client: 2, path: "A.md", content: "updated-by-client-2" }, { type: "enable-sync", client: 2 }, + { type: "sync" }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1) - .assertFileNotExists("A.md") - .assertContains("B.md", "updated-by-client-2"); - } - } + { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertFileNotExists("A.md").assertContains("B.md", "updated-by-client-2") } ] }; diff --git a/frontend/deterministic-tests/src/tests/merging-update-response-survives-user-rename.test.ts b/frontend/deterministic-tests/src/tests/merging-update-response-survives-user-rename.test.ts deleted file mode 100644 index e93240f9..00000000 --- a/frontend/deterministic-tests/src/tests/merging-update-response-survives-user-rename.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { AssertableState } from "../utils/assertable-state"; -import type { TestDefinition } from "../test-definition"; - -export const mergingUpdateResponseSurvivesUserRenameTest: TestDefinition = { - description: - "Client 1 sends a content update with a stale `parent_version_id` " + - "(its WebSocket is paused, so it hasn't seen Client 0's intervening " + - "edit). The server merges and replies with `MergingUpdate` carrying " + - "the merged text. Before the response lands, the user renames the " + - "doc on Client 1, vacating the disk path the in-flight " + - "`processLocalUpdate` captured. Pre-fix: " + - "`handleMaybeMergingResponse`'s `operations.write(diskPath, …)` " + - "hits the `we wont recreate it` early-return inside `write`, " + - "silently dropping the server-merged content — Client 0's edit is " + - "lost on Client 1's disk, and Client 1's next local-update PUT " + - "(rebased on the now-untracked merged version) deletes Client 0's " + - "edit on the server too. Post-fix: the response is written to the " + - "doc's current tracked disk path, preserving both edits.", - clients: 2, - steps: [ - { type: "enable-sync", client: 0 }, - { type: "enable-sync", client: 1 }, - { type: "create", client: 0, path: "doc.md", content: "0\n" }, - { type: "barrier" }, - - // Stop Client 1 from seeing Client 0's next edit, so its next - // outbound PUT carries a stale `parent_version_id` and the server - // is forced to merge. - { type: "pause-websocket", client: 1 }, - - // Server now holds v_b = "0\nA\n". Client 1's tracked parent - // version stays at v_a = "0\n". - { type: "update", client: 0, path: "doc.md", content: "0\nA\n" }, - { type: "sync", client: 0 }, - - // Pause the server. Subsequent HTTP PUTs from Client 1 buffer at - // the OS layer until resume. This guarantees the merge response - // for Client 1's update is still in flight when the rename below - // mutates `queue.documents`. - { type: "pause-server" }, - - // Client 1 edits doc.md with "B". The drain pops the LocalUpdate, - // captures `diskPath = "doc.md"`, reads the file, and sends the - // HTTP PUT — which buffers because the server is SIGSTOPped. - { type: "update", client: 1, path: "doc.md", content: "0\nB\n" }, - - // User renames the file while the previous PUT is still in flight. - // `queue.enqueue`'s rename branch updates `documents` to point at - // `renamed.md` synchronously, but `processLocalUpdate`'s captured - // `diskPath` ("doc.md") is a local — it can't be retargeted. - { type: "rename", client: 1, oldPath: "doc.md", newPath: "renamed.md" }, - - // Resume the server. It reconciles parent=v_a, latest=v_b, - // new="0\nB\n" → v_c with both edits, replies `MergingUpdate`. - // Pre-fix: write("doc.md", …) sees no file at that path - // (renamed.md now holds the data) and bails out without ever - // writing the merged bytes. Post-fix: the merged bytes land at - // the tracked path (renamed.md). - { type: "resume-server" }, - { type: "resume-websocket", client: 1 }, - - { type: "barrier" }, - - { - type: "assert-consistent", - verify: (state: AssertableState): void => { - state.assertFileCount(1); - state.assertFileExists("renamed.md"); - state.assertFileNotExists("doc.md"); - // Both edits survive: Client 0's "A" and Client 1's "B". - // The reconcile may interleave them either way; assert - // both tokens are present in the converged content. - state.assertContains("renamed.md", "A", "B"); - } - } - ] -}; diff --git a/frontend/deterministic-tests/src/tests/migrate-key-preserves-existing.test.ts b/frontend/deterministic-tests/src/tests/migrate-key-preserves-existing.test.ts index bb669e45..a4f6d3d3 100644 --- a/frontend/deterministic-tests/src/tests/migrate-key-preserves-existing.test.ts +++ b/frontend/deterministic-tests/src/tests/migrate-key-preserves-existing.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const migrateKeyPreservesExistingTest: TestDefinition = { @@ -9,6 +8,7 @@ export const migrateKeyPreservesExistingTest: TestDefinition = { steps: [ { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "pause-server" }, @@ -22,16 +22,9 @@ export const migrateKeyPreservesExistingTest: TestDefinition = { }, { type: "resume-server" }, + { type: "sync" }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContains( - "A.md", - "updated by client 0" - ); - } - } + { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertContains("A.md", "updated by client 0") } ] }; diff --git a/frontend/deterministic-tests/src/tests/move-and-concurrent-remote-update.test.ts b/frontend/deterministic-tests/src/tests/move-and-concurrent-remote-update.test.ts index 86657f0f..f590f5b4 100644 --- a/frontend/deterministic-tests/src/tests/move-and-concurrent-remote-update.test.ts +++ b/frontend/deterministic-tests/src/tests/move-and-concurrent-remote-update.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const moveAndConcurrentRemoteUpdateTest: TestDefinition = { @@ -15,6 +14,7 @@ export const moveAndConcurrentRemoteUpdateTest: TestDefinition = { }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "disable-sync", client: 0 }, @@ -29,15 +29,9 @@ export const moveAndConcurrentRemoteUpdateTest: TestDefinition = { { type: "sync", client: 1 }, { type: "enable-sync", client: 0 }, + { type: "sync" }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1) - .assertFileNotExists("A.md") - .assertContains("B.md", "updated by client 1"); - } - } + { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertFileNotExists("A.md").assertContains("B.md", "updated by client 1") } ] }; diff --git a/frontend/deterministic-tests/src/tests/move-preserves-remote-update.test.ts b/frontend/deterministic-tests/src/tests/move-preserves-remote-update.test.ts index 13e27349..59bedbbe 100644 --- a/frontend/deterministic-tests/src/tests/move-preserves-remote-update.test.ts +++ b/frontend/deterministic-tests/src/tests/move-preserves-remote-update.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const movePreservesRemoteUpdateTest: TestDefinition = { @@ -7,42 +6,32 @@ export const movePreservesRemoteUpdateTest: TestDefinition = { "After both reconnect, the renamed file should contain client 1's edit.", clients: 2, steps: [ - { - type: "create", - client: 0, - path: "doc.md", - content: "line 1\nline 2" - }, + { type: "create", client: 0, path: "doc.md", content: "line 1\nline 2" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "disable-sync", client: 0 }, { type: "disable-sync", client: 1 }, { type: "rename", client: 0, oldPath: "doc.md", newPath: "renamed.md" }, - { - type: "update", - client: 1, - path: "doc.md", - content: "line 1\nclient 1 edit\nline 2" - }, + { type: "update", client: 1, path: "doc.md", content: "line 1\nclient 1 edit\nline 2" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { + verify: (s) => { s.assertFileCount(1); - const [content] = Array.from(s.files.values()); + const content = Array.from(s.files.values())[0]; if (!content.includes("client 1 edit")) { - throw new Error( - `Expected merged content to include "client 1 edit", got: "${content}"` - ); + throw new Error(`Expected merged content to include "client 1 edit", got: "${content}"`); } } - } - ] + }, + ], }; diff --git a/frontend/deterministic-tests/src/tests/move-remote-update-reverts-rename.test.ts b/frontend/deterministic-tests/src/tests/move-remote-update-reverts-rename.test.ts index 433bf01b..95fcfe26 100644 --- a/frontend/deterministic-tests/src/tests/move-remote-update-reverts-rename.test.ts +++ b/frontend/deterministic-tests/src/tests/move-remote-update-reverts-rename.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const moveRemoteUpdateRevertsRenameTest: TestDefinition = { @@ -10,28 +9,26 @@ export const moveRemoteUpdateRevertsRenameTest: TestDefinition = { { type: "create", client: 0, path: "doc.md", content: "original" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "disable-sync", client: 0 }, - { - type: "update", - client: 1, - path: "doc.md", - content: "updated by client 1" - }, + { type: "update", client: 1, path: "doc.md", content: "updated by client 1" }, { type: "sync", client: 1 }, { type: "enable-sync", client: 0 }, { type: "rename", client: 0, oldPath: "doc.md", newPath: "renamed.md" }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContent( - "renamed.md", - "updated by client 1" - ); + verify: (s) => { + s.assertFileCount(1); + const content = Array.from(s.files.values())[0]; + if (content !== "updated by client 1") { + throw new Error(`Expected "updated by client 1", got: "${content}"`); + } } } ] diff --git a/frontend/deterministic-tests/src/tests/move-then-delete-stale-path.test.ts b/frontend/deterministic-tests/src/tests/move-then-delete-stale-path.test.ts index 4f5feab5..77814669 100644 --- a/frontend/deterministic-tests/src/tests/move-then-delete-stale-path.test.ts +++ b/frontend/deterministic-tests/src/tests/move-then-delete-stale-path.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const moveThenDeleteStalePathTest: TestDefinition = { @@ -15,20 +14,15 @@ export const moveThenDeleteStalePathTest: TestDefinition = { }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "rename", client: 0, oldPath: "A.md", newPath: "B.md" }, { type: "delete", client: 0, path: "B.md" }, + { type: "sync" }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(0) - .assertFileNotExists("A.md") - .assertFileNotExists("B.md"); - } - } + { type: "assert-consistent", verify: (s) => s.assertFileCount(0).assertFileNotExists("A.md").assertFileNotExists("B.md") } ] }; diff --git a/frontend/deterministic-tests/src/tests/multi-file-operations.test.ts b/frontend/deterministic-tests/src/tests/multi-file-operations.test.ts index a47f5a2a..66efd778 100644 --- a/frontend/deterministic-tests/src/tests/multi-file-operations.test.ts +++ b/frontend/deterministic-tests/src/tests/multi-file-operations.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const multiFileOperationsTest: TestDefinition = { @@ -12,6 +11,7 @@ export const multiFileOperationsTest: TestDefinition = { { type: "create", client: 0, path: "C.md", content: "content-c" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "disable-sync", client: 1 }, @@ -19,26 +19,20 @@ export const multiFileOperationsTest: TestDefinition = { { type: "delete", client: 0, path: "A.md" }, { type: "sync", client: 0 }, - { - type: "update", - client: 1, - path: "B.md", - content: "updated by client 1" - }, + { type: "update", client: 1, path: "B.md", content: "updated by client 1" }, { type: "rename", client: 1, oldPath: "A.md", newPath: "D.md" }, { type: "enable-sync", client: 1 }, + { type: "sync", client: 1 }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { + verify: (s) => { s.assertContains("B.md", "updated") .assertFileExists("C.md") .assertFileNotExists("A.md"); - s.ifFileExists("D.md", (inner) => - inner.assertContent("D.md", "content-a") - ); + s.ifFileExists("D.md", (s) => s.assertContent("D.md", "content-a")); } } ] diff --git a/frontend/deterministic-tests/src/tests/offline-concurrent-renames.test.ts b/frontend/deterministic-tests/src/tests/offline-concurrent-renames.test.ts index 6c946b9c..56ecc00d 100644 --- a/frontend/deterministic-tests/src/tests/offline-concurrent-renames.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-concurrent-renames.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const offlineConcurrentRenamesTest: TestDefinition = { @@ -12,12 +11,11 @@ export const offlineConcurrentRenamesTest: TestDefinition = { { type: "create", client: 0, path: "A.md", content: "shared-content" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent("A.md", "shared-content"); - } + verify: (s) => s.assertContent("A.md", "shared-content") }, { type: "disable-sync", client: 0 }, @@ -39,19 +37,20 @@ export const offlineConcurrentRenamesTest: TestDefinition = { { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { + verify: (s) => { s.assertFileNotExists("A.md") .assertFileCount(1) .assertAnyFileContains("shared-content"); - s.ifFileExists("B.md", (inner) => - inner.assertContent("B.md", "shared-content") + s.ifFileExists("B.md", (s) => + s.assertContent("B.md", "shared-content") ); - s.ifFileExists("C.md", (inner) => - inner.assertContent("C.md", "shared-content") + s.ifFileExists("C.md", (s) => + s.assertContent("C.md", "shared-content") ); } } diff --git a/frontend/deterministic-tests/src/tests/offline-create-same-path-binary-conflict.test.ts b/frontend/deterministic-tests/src/tests/offline-create-same-path-binary-conflict.test.ts index cbd59a4a..ca777563 100644 --- a/frontend/deterministic-tests/src/tests/offline-create-same-path-binary-conflict.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-create-same-path-binary-conflict.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const offlineCreateSamePathMergeableTest: TestDefinition = { @@ -23,19 +22,20 @@ export const offlineCreateSamePathMergeableTest: TestDefinition = { { type: "enable-sync", client: 0 }, { type: "sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1) + verify: (s) => + s + .assertFileCount(1) .assertFileExists("notes.md") .assertContains( "notes.md", "alpha wrote this line", "beta wrote this different line" - ); - } + ) } ] }; diff --git a/frontend/deterministic-tests/src/tests/offline-delete-remote-rename.test.ts b/frontend/deterministic-tests/src/tests/offline-delete-remote-rename.test.ts index 1e9ea8f7..ed242b20 100644 --- a/frontend/deterministic-tests/src/tests/offline-delete-remote-rename.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-delete-remote-rename.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const offlineDeleteRemoteRenameTest: TestDefinition = { @@ -28,10 +27,9 @@ export const offlineDeleteRemoteRenameTest: TestDefinition = { { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileNotExists("A.md").assertFileNotExists( - "A_renamed.md" - ); + verify: (s) => { + s.assertFileNotExists("A.md") + .assertFileNotExists("A_renamed.md"); } } ] diff --git a/frontend/deterministic-tests/src/tests/offline-delete-vs-remote-update.test.ts b/frontend/deterministic-tests/src/tests/offline-delete-vs-remote-update.test.ts index 21e81aa6..d86e3066 100644 --- a/frontend/deterministic-tests/src/tests/offline-delete-vs-remote-update.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-delete-vs-remote-update.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const offlineDeleteVsRemoteUpdateTest: TestDefinition = { @@ -14,12 +13,11 @@ export const offlineDeleteVsRemoteUpdateTest: TestDefinition = { }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent("A.md", "original content"); - } + verify: (s) => s.assertContent("A.md", "original content") }, { type: "disable-sync", client: 0 }, @@ -34,13 +32,12 @@ export const offlineDeleteVsRemoteUpdateTest: TestDefinition = { { type: "sync", client: 1 }, { type: "enable-sync", client: 0 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(0); - } + verify: (s) => s.assertFileCount(0) } ] }; diff --git a/frontend/deterministic-tests/src/tests/offline-edit-remote-rename.test.ts b/frontend/deterministic-tests/src/tests/offline-edit-remote-rename.test.ts index ffc41b89..fc4383e4 100644 --- a/frontend/deterministic-tests/src/tests/offline-edit-remote-rename.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-edit-remote-rename.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const offlineEditRemoteRenameTest: TestDefinition = { @@ -10,12 +9,11 @@ export const offlineEditRemoteRenameTest: TestDefinition = { { type: "create", client: 0, path: "A.md", content: "original" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent("A.md", "original"); - } + verify: (s) => s.assertContent("A.md", "original") }, { type: "disable-sync", client: 0 }, @@ -35,15 +33,16 @@ export const offlineEditRemoteRenameTest: TestDefinition = { { type: "sync", client: 1 }, { type: "enable-sync", client: 0 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileNotExists("A.md") + verify: (s) => + s + .assertFileNotExists("A.md") .assertFileCount(1) - .assertContains("B.md", "edited by client 0"); - } + .assertContains("B.md", "edited by client 0") } ] }; diff --git a/frontend/deterministic-tests/src/tests/offline-edit-then-move-same-content.test.ts b/frontend/deterministic-tests/src/tests/offline-edit-then-move-same-content.test.ts index 970eabd3..77d50099 100644 --- a/frontend/deterministic-tests/src/tests/offline-edit-then-move-same-content.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-edit-then-move-same-content.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const offlineEditThenMoveSameContentTest: TestDefinition = { @@ -20,6 +19,7 @@ export const offlineEditThenMoveSameContentTest: TestDefinition = { }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "disable-sync", client: 0 }, @@ -36,16 +36,17 @@ export const offlineEditThenMoveSameContentTest: TestDefinition = { }, { type: "enable-sync", client: 0 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileNotExists("A.md") + verify: (s) => + s + .assertFileNotExists("A.md") .assertFileNotExists("B.md") .assertContent("C.md", "content A") - .assertFileCount(1); - } + .assertFileCount(1) } ] }; diff --git a/frontend/deterministic-tests/src/tests/offline-mixed-operations.test.ts b/frontend/deterministic-tests/src/tests/offline-mixed-operations.test.ts index da875b6e..68453a0e 100644 --- a/frontend/deterministic-tests/src/tests/offline-mixed-operations.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-mixed-operations.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const offlineMixedOperationsTest: TestDefinition = { @@ -13,15 +12,16 @@ export const offlineMixedOperationsTest: TestDefinition = { { type: "create", client: 0, path: "file3.md", content: "content-3" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent("file1.md", "content-1") + verify: (s) => + s + .assertContent("file1.md", "content-1") .assertContent("file2.md", "content-2") - .assertContent("file3.md", "content-3"); - } + .assertContent("file3.md", "content-3") }, { type: "disable-sync", client: 0 }, @@ -41,17 +41,18 @@ export const offlineMixedOperationsTest: TestDefinition = { }, { type: "enable-sync", client: 0 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileNotExists("file1.md") + verify: (s) => + s + .assertFileNotExists("file1.md") .assertFileNotExists("file2.md") .assertContent("moved.md", "content-2") .assertContent("file3.md", "updated-content-3") - .assertFileCount(2); - } + .assertFileCount(2) } ] }; diff --git a/frontend/deterministic-tests/src/tests/offline-move-then-remote-delete.test.ts b/frontend/deterministic-tests/src/tests/offline-move-then-remote-delete.test.ts index f8e92bd9..d1522528 100644 --- a/frontend/deterministic-tests/src/tests/offline-move-then-remote-delete.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-move-then-remote-delete.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const offlineMoveThenRemoteDeleteTest: TestDefinition = { @@ -15,6 +14,7 @@ export const offlineMoveThenRemoteDeleteTest: TestDefinition = { }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "disable-sync", client: 0 }, @@ -24,13 +24,16 @@ export const offlineMoveThenRemoteDeleteTest: TestDefinition = { { type: "sync", client: 1 }, { type: "enable-sync", client: 0 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(0); - } + verify: (s) => + s + .assertFileNotExists("A.md") + .assertFileNotExists("B.md") + .assertFileCount(0) } ] }; diff --git a/frontend/deterministic-tests/src/tests/offline-multiple-edits.test.ts b/frontend/deterministic-tests/src/tests/offline-multiple-edits.test.ts index 6341fe8f..e242223a 100644 --- a/frontend/deterministic-tests/src/tests/offline-multiple-edits.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-multiple-edits.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const offlineMultipleEditsTest: TestDefinition = { @@ -11,12 +10,11 @@ export const offlineMultipleEditsTest: TestDefinition = { { type: "create", client: 0, path: "doc.md", content: "original" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent("doc.md", "original"); - } + verify: (s) => s.assertContent("doc.md", "original") }, { type: "disable-sync", client: 0 }, @@ -28,13 +26,13 @@ export const offlineMultipleEditsTest: TestDefinition = { { type: "update", client: 0, path: "doc.md", content: "edit-5-final" }, { type: "enable-sync", client: 0 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContent("doc.md", "edit-5-final"); - } + verify: (s) => + s.assertFileCount(1).assertContent("doc.md", "edit-5-final") } ] }; diff --git a/frontend/deterministic-tests/src/tests/offline-rename-and-edit.test.ts b/frontend/deterministic-tests/src/tests/offline-rename-and-edit.test.ts index 836c7fb2..c446d459 100644 --- a/frontend/deterministic-tests/src/tests/offline-rename-and-edit.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-rename-and-edit.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const offlineRenameAndEditTest: TestDefinition = { @@ -11,33 +10,28 @@ export const offlineRenameAndEditTest: TestDefinition = { { type: "create", client: 0, path: "A.md", content: "original" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent("A.md", "original"); - } + verify: (s) => s.assertContent("A.md", "original") }, { type: "disable-sync", client: 0 }, { type: "rename", client: 0, oldPath: "A.md", newPath: "B.md" }, - { - type: "update", - client: 0, - path: "B.md", - content: "edited after rename" - }, + { type: "update", client: 0, path: "B.md", content: "edited after rename" }, { type: "enable-sync", client: 0 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileNotExists("A.md") + verify: (s) => + s + .assertFileNotExists("A.md") .assertFileCount(1) - .assertContent("B.md", "edited after rename"); - } + .assertContent("B.md", "edited after rename") } ] }; diff --git a/frontend/deterministic-tests/src/tests/offline-rename-remote-create-old-path.test.ts b/frontend/deterministic-tests/src/tests/offline-rename-remote-create-old-path.test.ts index c1b2913a..24f4ff2a 100644 --- a/frontend/deterministic-tests/src/tests/offline-rename-remote-create-old-path.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-rename-remote-create-old-path.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const offlineRenameRemoteCreateOldPathTest: TestDefinition = { @@ -11,12 +10,11 @@ export const offlineRenameRemoteCreateOldPathTest: TestDefinition = { { type: "create", client: 0, path: "X.md", content: "original" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent("X.md", "original"); - } + verify: (s) => s.assertContent("X.md", "original") }, { type: "disable-sync", client: 0 }, @@ -36,16 +34,15 @@ export const offlineRenameRemoteCreateOldPathTest: TestDefinition = { { type: "sync", client: 1 }, { type: "enable-sync", client: 0 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContains( - "Y.md", - "updated-by-client-1" - ); - } + verify: (s) => + s + .assertFileCount(1) + .assertContains("Y.md", "updated-by-client-1") } ] }; diff --git a/frontend/deterministic-tests/src/tests/offline-update-both-then-delete-one.test.ts b/frontend/deterministic-tests/src/tests/offline-update-both-then-delete-one.test.ts index 3442cda7..47a88328 100644 --- a/frontend/deterministic-tests/src/tests/offline-update-both-then-delete-one.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-update-both-then-delete-one.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const offlineUpdateBothThenDeleteOneTest: TestDefinition = { @@ -23,15 +22,14 @@ export const offlineUpdateBothThenDeleteOneTest: TestDefinition = { }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent("A.md", "A original").assertContent( - "B.md", - "B original" - ); - } + verify: (s) => + s + .assertContent("A.md", "A original") + .assertContent("B.md", "B original") }, { type: "disable-sync", client: 0 }, @@ -60,16 +58,15 @@ export const offlineUpdateBothThenDeleteOneTest: TestDefinition = { { type: "sync", client: 1 }, { type: "enable-sync", client: 0 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent( - "A.md", - "A updated by client 0" - ).assertFileNotExists("B.md"); - } + verify: (s) => + s + .assertContent("A.md", "A updated by client 0") + .assertFileNotExists("B.md") } ] }; diff --git a/frontend/deterministic-tests/src/tests/online-both-create-same-path-deconflict.test.ts b/frontend/deterministic-tests/src/tests/online-both-create-same-path-deconflict.test.ts index b951b0be..1639ed90 100644 --- a/frontend/deterministic-tests/src/tests/online-both-create-same-path-deconflict.test.ts +++ b/frontend/deterministic-tests/src/tests/online-both-create-same-path-deconflict.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const onlineBothCreateSamePathDeconflictTest: TestDefinition = { @@ -24,7 +23,7 @@ export const onlineBothCreateSamePathDeconflictTest: TestDefinition = { { type: "assert-consistent", - verify: (state: AssertableState): void => { + verify: (state) => { state .assertFileCount(1) .assertContains("A.md", "updated-by-0", "from-client-1 "); diff --git a/frontend/deterministic-tests/src/tests/online-create-rename-concurrent-create-orphan.test.ts b/frontend/deterministic-tests/src/tests/online-create-rename-concurrent-create-orphan.test.ts index f86b3347..3449e676 100644 --- a/frontend/deterministic-tests/src/tests/online-create-rename-concurrent-create-orphan.test.ts +++ b/frontend/deterministic-tests/src/tests/online-create-rename-concurrent-create-orphan.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const onlineCreateRenameConcurrentCreateOrphanTest: TestDefinition = { @@ -13,18 +12,8 @@ export const onlineCreateRenameConcurrentCreateOrphanTest: TestDefinition = { { type: "disable-sync", client: 0 }, - { - type: "create", - client: 0, - path: "data.bin", - content: "BINARY:offline-content" - }, - { - type: "rename", - client: 0, - oldPath: "data.bin", - newPath: "moved.bin" - }, + { type: "create", client: 0, path: "data.bin", content: "BINARY:offline-content" }, + { type: "rename", client: 0, oldPath: "data.bin", newPath: "moved.bin" }, { type: "enable-sync", client: 0 }, { type: "delete", client: 0, path: "moved.bin" }, @@ -33,7 +22,7 @@ export const onlineCreateRenameConcurrentCreateOrphanTest: TestDefinition = { { type: "assert-consistent", - verify: (state: AssertableState): void => { + verify: (state) => { state.assertFileCount(0); } } diff --git a/frontend/deterministic-tests/src/tests/online-create-update-while-other-creates-same-path.test.ts b/frontend/deterministic-tests/src/tests/online-create-update-while-other-creates-same-path.test.ts index e0ddc21a..f59a92e3 100644 --- a/frontend/deterministic-tests/src/tests/online-create-update-while-other-creates-same-path.test.ts +++ b/frontend/deterministic-tests/src/tests/online-create-update-while-other-creates-same-path.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const onlineCreateUpdateWhileOtherCreatesSamePathTest: TestDefinition = { @@ -12,36 +11,18 @@ export const onlineCreateUpdateWhileOtherCreatesSamePathTest: TestDefinition = { { type: "enable-sync", client: 1 }, { type: "pause-websocket", client: 1 }, - { - type: "create", - client: 0, - path: "data.bin", - content: "BINARY:content-v1" - }, - { - type: "update", - client: 0, - path: "data.bin", - content: "BINARY:content-v2" - }, - { - type: "create", - client: 1, - path: "data.bin", - content: "BINARY:other-content" - }, + { type: "create", client: 0, path: "data.bin", content: "BINARY:content-v1" }, + { type: "update", client: 0, path: "data.bin", content: "BINARY:content-v2" }, + { type: "create", client: 1, path: "data.bin", content: "BINARY:other-content" }, { type: "resume-websocket", client: 1 }, { type: "barrier" }, { - type: "assert-consistent", - verify: (state: AssertableState): void => { - state - .assertFileCount(2) - .assertNoFileContains("content-v1") - .assertAnyFileContains("content-v2") - .assertAnyFileContains("other-content"); + type: "assert-consistent", verify: (state) => { + state.assertFileCount(2) + .assertContains("data.bin", "content-v2") + .assertContains("data (1).bin", "other-content"); } } ] diff --git a/frontend/deterministic-tests/src/tests/online-delete-recreate-rapid-cycle.test.ts b/frontend/deterministic-tests/src/tests/online-delete-recreate-rapid-cycle.test.ts index de5d6c89..b575aa58 100644 --- a/frontend/deterministic-tests/src/tests/online-delete-recreate-rapid-cycle.test.ts +++ b/frontend/deterministic-tests/src/tests/online-delete-recreate-rapid-cycle.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const onlineDeleteRecreateRapidCycleTest: TestDefinition = { @@ -29,9 +28,7 @@ export const onlineDeleteRecreateRapidCycleTest: TestDefinition = { { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent("A.md", "round 3"); - } - } - ] + verify: (s) => s.assertContent("A.md", "round 3"), + }, + ], }; diff --git a/frontend/deterministic-tests/src/tests/online-edit-vs-delete-convergence.test.ts b/frontend/deterministic-tests/src/tests/online-edit-vs-delete-convergence.test.ts index d3a9d84e..16ed7236 100644 --- a/frontend/deterministic-tests/src/tests/online-edit-vs-delete-convergence.test.ts +++ b/frontend/deterministic-tests/src/tests/online-edit-vs-delete-convergence.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const onlineEditVsDeleteConvergenceTest: TestDefinition = { @@ -12,20 +11,17 @@ export const onlineEditVsDeleteConvergenceTest: TestDefinition = { { type: "enable-sync", client: 1 }, { type: "barrier" }, - { - type: "update", - client: 0, - path: "A.md", - content: "edited by client 0" - }, + { type: "update", client: 0, path: "A.md", content: "edited by client 0" }, { type: "delete", client: 1, path: "A.md" }, { type: "barrier" }, { type: "assert-consistent", - verify: (state: AssertableState): void => { - state.assertFileCount(0); + verify: (state) => { + state.ifFileExists("A.md", (s) => + s.assertContainsAny("A.md", "edited by client 0") + ); } - } - ] + }, + ], }; diff --git a/frontend/deterministic-tests/src/tests/overlapping-edits-same-section.test.ts b/frontend/deterministic-tests/src/tests/overlapping-edits-same-section.test.ts index a93a6f69..eeb705de 100644 --- a/frontend/deterministic-tests/src/tests/overlapping-edits-same-section.test.ts +++ b/frontend/deterministic-tests/src/tests/overlapping-edits-same-section.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const overlappingEditsSameSectionTest: TestDefinition = { @@ -15,6 +14,7 @@ export const overlappingEditsSameSectionTest: TestDefinition = { }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "disable-sync", client: 0 }, @@ -36,19 +36,14 @@ export const overlappingEditsSameSectionTest: TestDefinition = { { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContains( - "doc.md", - "# Title", - "alpha addition", - "beta addition", - "footer" - ); - } + verify: (s) => + s.assertFileCount(1) + .assertContains("doc.md", "# Title", "alpha addition", "beta addition", "footer"), } ] }; diff --git a/frontend/deterministic-tests/src/tests/queue-reset-loses-coalesced-local-edit.test.ts b/frontend/deterministic-tests/src/tests/queue-reset-loses-coalesced-local-edit.test.ts index 6d89acf4..ecf58d05 100644 --- a/frontend/deterministic-tests/src/tests/queue-reset-loses-coalesced-local-edit.test.ts +++ b/frontend/deterministic-tests/src/tests/queue-reset-loses-coalesced-local-edit.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const queueResetLosesCoalescedLocalEditTest: TestDefinition = { @@ -24,13 +23,8 @@ export const queueResetLosesCoalescedLocalEditTest: TestDefinition = { { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContains( - "doc.md", - "alpha", - "charlie" - ); - } + verify: (s) => + s.assertFileCount(1).assertContains("doc.md", "alpha", "charlie"), } ] }; diff --git a/frontend/deterministic-tests/src/tests/queued-create-delete-does-not-hijack-reused-path.test.ts b/frontend/deterministic-tests/src/tests/queued-create-delete-does-not-hijack-reused-path.test.ts deleted file mode 100644 index a29f8314..00000000 --- a/frontend/deterministic-tests/src/tests/queued-create-delete-does-not-hijack-reused-path.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { AssertableState } from "../utils/assertable-state"; -import type { TestDefinition } from "../test-definition"; - -export const queuedCreateDeleteDoesNotHijackReusedPathTest: TestDefinition = { - description: - "A create/delete pair that is still queued behind another request " + - "must collapse locally. It must not later read a different file " + - "that reused the same path before the queued create drained.", - clients: 2, - steps: [ - { type: "enable-sync", client: 0 }, - { type: "enable-sync", client: 1 }, - { type: "barrier" }, - - { type: "pause-server" }, - { - type: "create", - client: 1, - path: "blocker.bin", - content: "BINARY:blocker" - }, - { type: "sleep", ms: 100 }, - { - type: "create", - client: 1, - path: "target.bin", - content: "BINARY:old" - }, - { type: "delete", client: 1, path: "target.bin" }, - { - type: "create", - client: 1, - path: "source.bin", - content: "BINARY:new" - }, - { - type: "rename", - client: 1, - oldPath: "source.bin", - newPath: "target.bin" - }, - { type: "resume-server" }, - { type: "barrier" }, - - { - type: "assert-consistent", - verify: (state: AssertableState): void => { - state - .assertFileCount(2) - .assertContent("blocker.bin", "BINARY:blocker") - .assertContent("target.bin", "BINARY:new") - .assertFileNotExists("source.bin"); - } - } - ] -}; diff --git a/frontend/deterministic-tests/src/tests/rapid-create-update-delete-cycle.test.ts b/frontend/deterministic-tests/src/tests/rapid-create-update-delete-cycle.test.ts index f9c58753..45f90144 100644 --- a/frontend/deterministic-tests/src/tests/rapid-create-update-delete-cycle.test.ts +++ b/frontend/deterministic-tests/src/tests/rapid-create-update-delete-cycle.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const rapidCreateUpdateDeleteCycleTest: TestDefinition = { @@ -9,6 +8,7 @@ export const rapidCreateUpdateDeleteCycleTest: TestDefinition = { steps: [ { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "pause-server" }, @@ -41,12 +41,7 @@ export const rapidCreateUpdateDeleteCycleTest: TestDefinition = { { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContent( - "cycle.md", - "final creation" - ); - } + verify: (s) => s.assertFileCount(1).assertContent("cycle.md", "final creation"), } ] }; diff --git a/frontend/deterministic-tests/src/tests/rapid-edit-delete-online-convergence.test.ts b/frontend/deterministic-tests/src/tests/rapid-edit-delete-online-convergence.test.ts index 48c062e0..042942b3 100644 --- a/frontend/deterministic-tests/src/tests/rapid-edit-delete-online-convergence.test.ts +++ b/frontend/deterministic-tests/src/tests/rapid-edit-delete-online-convergence.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const rapidEditDeleteOnlineConvergenceTest: TestDefinition = { @@ -29,20 +28,17 @@ export const rapidEditDeleteOnlineConvergenceTest: TestDefinition = { { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { + verify: (s) => { for (const [path, content] of s.files) { for (const clientFiles of s.clientFiles) { - if ( - clientFiles.has(path) && - clientFiles.get(path) !== content - ) { + if (clientFiles.has(path) && clientFiles.get(path) !== content) { throw new Error( `Content mismatch for ${path}: "${clientFiles.get(path)}" vs "${content}"` ); } } } - } - } - ] + }, + }, + ], }; diff --git a/frontend/deterministic-tests/src/tests/rapid-updates-after-merge.test.ts b/frontend/deterministic-tests/src/tests/rapid-updates-after-merge.test.ts index 6f97ff05..bf0ed488 100644 --- a/frontend/deterministic-tests/src/tests/rapid-updates-after-merge.test.ts +++ b/frontend/deterministic-tests/src/tests/rapid-updates-after-merge.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const rapidUpdatesAfterMergeTest: TestDefinition = { @@ -12,6 +11,7 @@ export const rapidUpdatesAfterMergeTest: TestDefinition = { { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { @@ -36,14 +36,13 @@ export const rapidUpdatesAfterMergeTest: TestDefinition = { path: "doc.md", content: "update 3" }, + { type: "sync", client: 0 }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContains("doc.md", "update 3"); - } + verify: (s) => s.assertFileCount(1).assertContains("doc.md", "update 3"), } ] }; diff --git a/frontend/deterministic-tests/src/tests/recently-deleted-cleared-on-reconnect.test.ts b/frontend/deterministic-tests/src/tests/recently-deleted-cleared-on-reconnect.test.ts index c8e70243..128cd90e 100644 --- a/frontend/deterministic-tests/src/tests/recently-deleted-cleared-on-reconnect.test.ts +++ b/frontend/deterministic-tests/src/tests/recently-deleted-cleared-on-reconnect.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const recentlyDeletedClearedOnReconnectTest: TestDefinition = { @@ -20,12 +19,7 @@ export const recentlyDeletedClearedOnReconnectTest: TestDefinition = { { type: "disable-sync", client: 0 }, { type: "disable-sync", client: 1 }, - { - type: "create", - client: 1, - path: "doc.md", - content: "new content from client 1" - }, + { type: "create", client: 1, path: "doc.md", content: "new content from client 1" }, { type: "enable-sync", client: 1 }, { type: "sync", client: 1 }, @@ -34,12 +28,8 @@ export const recentlyDeletedClearedOnReconnectTest: TestDefinition = { { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContent( - "doc.md", - "new content from client 1" - ); - } - } - ] + verify: (s) => + s.assertFileCount(1).assertContent("doc.md", "new content from client 1"), + }, + ], }; diff --git a/frontend/deterministic-tests/src/tests/remote-quick-write-rename-before-record.test.ts b/frontend/deterministic-tests/src/tests/remote-quick-write-rename-before-record.test.ts deleted file mode 100644 index ca184b27..00000000 --- a/frontend/deterministic-tests/src/tests/remote-quick-write-rename-before-record.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { AssertableState } from "../utils/assertable-state"; -import type { TestDefinition } from "../test-definition"; - -export const remoteQuickWriteRenameBeforeRecordTest: TestDefinition = { - description: - "Client 0 receives a remote create and the user renames the new " + - "file immediately after the syncer writes it. The watcher event " + - "must bind to the new document instead of being dropped before " + - "the remote-create handler persists the record.", - clients: 2, - steps: [ - { type: "enable-sync", client: 0 }, - { type: "enable-sync", client: 1 }, - - { - type: "rename-next-write", - client: 0, - oldPath: "doc.md", - newPath: "renamed.md" - }, - - { type: "create", client: 1, path: "doc.md", content: "v1\n" }, - { type: "sync", client: 1 }, - { type: "barrier" }, - - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1); - s.assertFileExists("renamed.md"); - s.assertFileNotExists("doc.md"); - s.assertContent("renamed.md", "v1\n"); - } - } - ] -}; diff --git a/frontend/deterministic-tests/src/tests/remote-rename-collides-with-pending-local-create.test.ts b/frontend/deterministic-tests/src/tests/remote-rename-collides-with-pending-local-create.test.ts deleted file mode 100644 index d30fdc67..00000000 --- a/frontend/deterministic-tests/src/tests/remote-rename-collides-with-pending-local-create.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { AssertableState } from "../utils/assertable-state"; -import type { TestDefinition } from "../test-definition"; - -export const remoteRenameCollidesWithPendingLocalCreateTest: TestDefinition = { - // TODO(refactor): the failure mode described below is the - // pre-refactor "deflect-to-conflict-uuid" path that no longer - // exists. Under the new model the wire loop never moves files for - // path placement, so the remote rename can't deflect anywhere; the - // reconciler waits for the slot to free. Convergence assertion is - // still valid (no conflict-uuid stashes, both files present, the - // local create lands at a server-deconflicted sibling). - description: - "Client 0 has doc D tracked at `original.md`. Client 1 owns doc E " + - "and renames it to `target.md` server-side. Before client 0's " + - "drain processes the WS broadcast for E, the user creates a new " + - "local file `target.md` (a different doc, untracked). When the " + - "buffered RemoteChange for E drains, the engine has to reconcile " + - "doc E onto `target.md` even though the slot is held by client " + - "0's pending LocalCreate. Convergence requires both clients end " + - "up with [target.md = E] and the local create lands at a " + - "server-deconflicted sibling (e.g. `target (1).md`).", - clients: 2, - steps: [ - { type: "enable-sync", client: 0 }, - { type: "enable-sync", client: 1 }, - - { type: "create", client: 1, path: "original.md", content: "v1\n" }, - { type: "barrier" }, - - // Pause client 0's WS so the upcoming remote rename buffers and - // we can stage a colliding local create before the rename - // drains on client 0. - { type: "pause-websocket", client: 0 }, - - // Client 1 renames the doc. Server commits, broadcasts to - // client 0 (buffered). - { - type: "rename", - client: 1, - oldPath: "original.md", - newPath: "target.md" - }, - { type: "sync", client: 1 }, - - // Client 0 still believes the doc is at `original.md`. The user - // creates a NEW file at `target.md` (an unrelated untracked - // doc). Disk on client 0 now has both `original.md` (the - // tracked doc) and `target.md` (the new untracked file). - { type: "create", client: 0, path: "target.md", content: "extra\n" }, - - // Resume client 0's WS. The buffered RemoteChange drains. - // The reconciler must converge without ever leaving a - // conflict-uuid stash on disk. - { type: "resume-websocket", client: 0 }, - - { type: "barrier" }, - - { - type: "assert-consistent", - verify: (state: AssertableState): void => { - state.assertFileCount(2); - for (const path of state.files.keys()) { - if (path.startsWith("conflict-")) { - throw new Error( - `Unexpected conflict-uuid stash on a converged client: ${path}` - ); - } - } - state.assertFileExists("target.md"); - state.assertContent("target.md", "v1\n"); - // The local create gets server-deconflicted to a - // sibling path (e.g. `target (1).md`). - } - } - ] -}; diff --git a/frontend/deterministic-tests/src/tests/remote-update-resurrects-deleted-doc.test.ts b/frontend/deterministic-tests/src/tests/remote-update-resurrects-deleted-doc.test.ts deleted file mode 100644 index eb2ed86d..00000000 --- a/frontend/deterministic-tests/src/tests/remote-update-resurrects-deleted-doc.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { AssertableState } from "../utils/assertable-state"; -import type { TestDefinition } from "../test-definition"; - -export const remoteUpdateResurrectsDeletedDocTest: TestDefinition = { - description: - "Client 1 updates, deletes, and recreates P (with a new docId D2). " + - "While the buffered remote events are being processed by client 0, " + - "client 0 also makes a local edit to P. The local edit lands in the " + - "queue while v17 is mid-process, sending v17 down processRemoteUpdate's " + - "re-enqueue branch. The deferred v17 must NOT later resurrect D1 as a " + - "conflict-… file at P after the delete and the D2 create have drained.", - clients: 2, - steps: [ - { type: "enable-sync", client: 0 }, - { type: "enable-sync", client: 1 }, - - { type: "create", client: 1, path: "P.md", content: "v8 content\n" }, - { type: "barrier" }, - - { type: "pause-websocket", client: 0 }, - - { - type: "update", - client: 1, - path: "P.md", - content: "v17 content from client 1\n" - }, - { type: "sync", client: 1 }, - { type: "delete", client: 1, path: "P.md" }, - { type: "sync", client: 1 }, - { - type: "create", - client: 1, - path: "P.md", - content: "v21 content (D2)\n" - }, - { type: "sync", client: 1 }, - - { type: "resume-websocket", client: 0 }, - - { - type: "update", - client: 0, - path: "P.md", - content: "local edit by client 0\n" - }, - - { type: "barrier" }, - - { - type: "assert-consistent", - verify: (state: AssertableState): void => { - state - .assertFileCount(1) - .assertContent("P.md", "v21 content (D2)\n"); - } - } - ] -}; diff --git a/frontend/deterministic-tests/src/tests/remote-update-survives-user-rename.test.ts b/frontend/deterministic-tests/src/tests/remote-update-survives-user-rename.test.ts deleted file mode 100644 index b78ad143..00000000 --- a/frontend/deterministic-tests/src/tests/remote-update-survives-user-rename.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { AssertableState } from "../utils/assertable-state"; -import type { TestDefinition } from "../test-definition"; - -export const remoteUpdateSurvivesUserRenameTest: TestDefinition = { - description: - "Client 0 updates a tracked doc; while Client 1 is processing the " + - "broadcast and parked on the GET for the new version's content, the " + - "user renames the doc on Client 1. Pre-fix: `processRemoteUpdate` " + - "captures `actualPath` before the await and, after the GET returns, " + - "calls `write(actualPath, …)` (no-op — file was renamed away), " + - "`updateCache(actualPath, …)`, and `setDocument(actualPath, …)`. " + - "`setDocument` mutates the same record in place so its `path` is " + - "yanked from the user's renamed slot back to the pre-rename path, " + - "wiping the rename out of the queue's documents map. The queued " + - "`LocalUpdate` then reads from the now-stale `record.path`, hits " + - "`FileNotFoundError`, and is silently dropped — the user's rename " + - "never reaches the server. Post-fix: the handler defers when a " + - "local event landed mid-await, so the rename drains first and " + - "the deferred remote update is folded into the broadcast that " + - "follows the rename round-trip.", - clients: 2, - steps: [ - { type: "create", client: 0, path: "doc.md", content: "v1\n" }, - { type: "enable-sync", client: 0 }, - { type: "enable-sync", client: 1 }, - { type: "barrier" }, - - // Buffer Client 1's incoming broadcasts so it doesn't see - // Client 0's update until we've paused the server. - { type: "pause-websocket", client: 1 }, - - // Server now holds v=2 of doc.md. - { type: "update", client: 0, path: "doc.md", content: "v2\n" }, - { type: "sync", client: 0 }, - - // Pause the server. Client 1's upcoming GET for the new version - // content blocks at the OS layer until resume. - { type: "pause-server" }, - - // Release the buffered broadcast. Client 1's drain enters - // `processRemoteUpdate`, captures `actualPath`, fires the GET, - // and parks awaiting the response. - { type: "resume-websocket", client: 1 }, - - // Yield long enough for the drain to traverse all microtask - // hops between the WS handler and the GET, so the HTTP request - // is queued at the (paused) server before the rename runs. - // Without this yield the rename would be enqueued before - // `processRemoteUpdate`'s entry-time `hasPendingLocalEvents` - // check and the early-defer branch would mask the bug. - { type: "sleep", ms: 50 }, - - // While the GET is in flight the user renames the doc. The queue - // mutates `record.path` to "renamed.md" in place and pushes a - // LocalUpdate carrying the rename target. - { - type: "rename", - client: 1, - oldPath: "doc.md", - newPath: "renamed.md" - }, - - // Resume the server. The GET response unblocks - // `processRemoteUpdate`. With the fix in place it sees the - // queued LocalUpdate and defers; without the fix it walks past - // the rename and clobbers the documents map, dropping the - // pending LocalUpdate's read on the way back through. - { type: "resume-server" }, - - { type: "barrier" }, - - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1); - s.assertFileExists("renamed.md"); - s.assertFileNotExists("doc.md"); - // Both edits survive: the user's rename and Client 0's - // content update at v=2. - s.assertContent("renamed.md", "v2\n"); - } - } - ] -}; diff --git a/frontend/deterministic-tests/src/tests/rename-chain-during-pending-create.test.ts b/frontend/deterministic-tests/src/tests/rename-chain-during-pending-create.test.ts deleted file mode 100644 index 822e83df..00000000 --- a/frontend/deterministic-tests/src/tests/rename-chain-during-pending-create.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { AssertableState } from "../utils/assertable-state"; -import type { TestDefinition } from "../test-definition"; - -export const renameChainDuringPendingCreateTest: TestDefinition = { - description: - "User creates a doc, then renames it twice while the LocalCreate's " + - "HTTP roundtrip is still in flight (server paused). Each rename " + - "pushes a LocalUpdate whose `documentId` is the create's Promise " + - "(see `pendingDocumentId` in `SyncEventQueue.enqueue`). After the " + - "create resolves, the first rename drains successfully and " + - "`setDocument` walks `events[]` to retarget queued LocalUpdates' " + - "`event.path` to the new disk location — but the comparison " + - "`e.documentId === record.documentId` mismatches the still-Promise " + - "references, so the second rename's `event.path` stays at the " + - "vacated previous slot. On the next drain step `skipIfOversized`'s " + - "`getFileSize(event.path)` throws FileNotFoundError, which " + - "`processEvent` swallows as 'Skipping sync event ... because the " + - "file no longer exists' — losing the user's final rename. " + - "Post-fix: `resolveCreate` (and the displacement-merge branch in " + - "`processCreate`) swap the Promise references for the resolved id " + - "before `setDocument` runs, so retarget works.", - clients: 2, - steps: [ - { type: "enable-sync", client: 0 }, - { type: "enable-sync", client: 1 }, - { type: "barrier" }, - - // Pause the server so client 0's create stalls on the HTTP PUT - // while we queue rename events behind it. - { type: "pause-server" }, - - { type: "create", client: 0, path: "first.md", content: "v1\n" }, - { - type: "rename", - client: 0, - oldPath: "first.md", - newPath: "second.md" - }, - { - type: "rename", - client: 0, - oldPath: "second.md", - newPath: "third.md" - }, - - // Resume — drain pops LocalCreate (now resolves), then the two - // queued LocalUpdates. Pre-fix: only the first rename's - // file-system effect lands; the second is silently dropped. - // The server ends up with the doc at second.md, leaving - // client 0's local third.md untracked / out-of-sync. - { type: "resume-server" }, - - { type: "barrier" }, - - { - type: "assert-consistent", - verify: (state: AssertableState): void => { - state.assertFileCount(1); - state.assertFileExists("third.md"); - state.assertContent("third.md", "v1\n"); - } - } - ] -}; diff --git a/frontend/deterministic-tests/src/tests/rename-chain-then-delete.test.ts b/frontend/deterministic-tests/src/tests/rename-chain-then-delete.test.ts index 03196919..27787e4f 100644 --- a/frontend/deterministic-tests/src/tests/rename-chain-then-delete.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-chain-then-delete.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const renameChainThenDeleteTest: TestDefinition = { @@ -10,12 +9,11 @@ export const renameChainThenDeleteTest: TestDefinition = { { type: "create", client: 0, path: "X.md", content: "chain-content" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent("X.md", "chain-content"); - } + verify: (s) => s.assertContent("X.md", "chain-content"), }, { type: "disable-sync", client: 1 }, @@ -38,13 +36,9 @@ export const renameChainThenDeleteTest: TestDefinition = { { type: "sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, - { - type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(0); - } - } + { type: "assert-consistent", verify: (s) => s.assertFileCount(0) } ] }; diff --git a/frontend/deterministic-tests/src/tests/rename-chain.test.ts b/frontend/deterministic-tests/src/tests/rename-chain.test.ts index 8f9d7a7f..8cc3bde3 100644 --- a/frontend/deterministic-tests/src/tests/rename-chain.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-chain.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const renameChainTest: TestDefinition = { @@ -10,25 +9,20 @@ export const renameChainTest: TestDefinition = { steps: [ { type: "enable-sync", client: 1 }, - { - type: "create", - client: 0, - path: "A.md", - content: "important content" - }, + { type: "create", client: 0, path: "A.md", content: "important content" }, { type: "rename", client: 0, oldPath: "A.md", newPath: "B.md" }, { type: "rename", client: 0, oldPath: "B.md", newPath: "C.md" }, { type: "enable-sync", client: 0 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { + verify: (s) => s.assertFileNotExists("A.md") .assertFileNotExists("B.md") - .assertContent("C.md", "important content"); - } + .assertContent("C.md", "important content"), } ] }; diff --git a/frontend/deterministic-tests/src/tests/rename-circular.test.ts b/frontend/deterministic-tests/src/tests/rename-circular.test.ts index 44a65149..5c85ca71 100644 --- a/frontend/deterministic-tests/src/tests/rename-circular.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-circular.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const renameCircularTest: TestDefinition = { @@ -14,11 +13,10 @@ export const renameCircularTest: TestDefinition = { { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { + verify: (s) => s.assertContent("A.md", "content-a") .assertContent("B.md", "content-b") - .assertContent("C.md", "content-c"); - } + .assertContent("C.md", "content-c"), }, { type: "disable-sync", client: 0 }, @@ -28,17 +26,17 @@ export const renameCircularTest: TestDefinition = { { type: "rename", client: 0, oldPath: "temp-a.md", newPath: "B.md" }, { type: "enable-sync", client: 0 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { + verify: (s) => s.assertFileNotExists("temp-a.md") .assertFileCount(3) - .assertAnyFileContains("content-c") - .assertAnyFileContains("content-a") - .assertAnyFileContains("content-b"); - } + .assertContent("A.md", "content-c") + .assertContent("B.md", "content-a") + .assertContent("C.md", "content-b"), } ] }; diff --git a/frontend/deterministic-tests/src/tests/rename-create-conflict.test.ts b/frontend/deterministic-tests/src/tests/rename-create-conflict.test.ts index fc6a00a7..c29b1dc5 100644 --- a/frontend/deterministic-tests/src/tests/rename-create-conflict.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-create-conflict.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const renameCreateConflictTest: TestDefinition = { @@ -9,26 +8,23 @@ export const renameCreateConflictTest: TestDefinition = { { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, { type: "create", client: 0, path: "A.md", content: "hi" }, - { type: "barrier" }, + { type: "sync", client: 0 }, + { type: "sync", client: 1 }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent("A.md", "hi"); - } + verify: (s) => s.assertContent("A.md", "hi"), }, { type: "disable-sync", client: 0 }, { type: "rename", client: 1, oldPath: "A.md", newPath: "B.md" }, { type: "sync", client: 1 }, { type: "create", client: 0, path: "B.md", content: "hi" }, { type: "enable-sync", client: 0 }, + { type: "sync", client: 0 }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(2) - .assertContent("B.md", "hi") - .assertContent("B (1).md", "hi"); - } + verify: (s) => + s.assertFileNotExists("A.md").assertContent("B.md", "hi"), } ] }; diff --git a/frontend/deterministic-tests/src/tests/rename-overwrites-pending-create-then-delete.test.ts b/frontend/deterministic-tests/src/tests/rename-overwrites-pending-create-then-delete.test.ts deleted file mode 100644 index 0b47c781..00000000 --- a/frontend/deterministic-tests/src/tests/rename-overwrites-pending-create-then-delete.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { AssertableState } from "../utils/assertable-state"; -import type { TestDefinition } from "../test-definition"; - -export const renameOverwritesPendingCreateThenDeleteTest: TestDefinition = { - description: - "A pending local create at a path must not mask a synced document renamed onto that path; later rename/delete events still belong to the synced document.", - clients: 2, - steps: [ - { type: "enable-sync", client: 0 }, - { type: "enable-sync", client: 1 }, - { - type: "create", - client: 0, - path: "tracked.bin", - content: "BINARY:tracked" - }, - { type: "barrier" }, - - { type: "pause-server" }, - - { - type: "create", - client: 0, - path: "pending.bin", - content: "BINARY:pending" - }, - { - type: "rename", - client: 0, - oldPath: "tracked.bin", - newPath: "pending.bin" - }, - { - type: "rename", - client: 0, - oldPath: "pending.bin", - newPath: "final.bin" - }, - { type: "delete", client: 0, path: "final.bin" }, - - { type: "resume-server" }, - { type: "barrier" }, - - { - type: "assert-consistent", - verify: (state: AssertableState): void => { - state.assertFileCount(0); - } - } - ] -}; diff --git a/frontend/deterministic-tests/src/tests/rename-pending-create-before-response.test.ts b/frontend/deterministic-tests/src/tests/rename-pending-create-before-response.test.ts index 26623c43..d38a0392 100644 --- a/frontend/deterministic-tests/src/tests/rename-pending-create-before-response.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-pending-create-before-response.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const renamePendingCreateBeforeResponseTest: TestDefinition = { @@ -8,6 +7,8 @@ export const renamePendingCreateBeforeResponseTest: TestDefinition = { steps: [ { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, + { type: "barrier" }, { type: "pause-server" }, @@ -27,16 +28,14 @@ export const renamePendingCreateBeforeResponseTest: TestDefinition = { { type: "resume-server" }, + { type: "sync" }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContent( - "renamed.md", - "original-content" - ); - } + verify: (s) => + s.assertFileCount(1).assertContent("renamed.md", "original-content"), } ] }; diff --git a/frontend/deterministic-tests/src/tests/rename-pending-create-onto-pending-delete-path.test.ts b/frontend/deterministic-tests/src/tests/rename-pending-create-onto-pending-delete-path.test.ts deleted file mode 100644 index 0906f209..00000000 --- a/frontend/deterministic-tests/src/tests/rename-pending-create-onto-pending-delete-path.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { AssertableState } from "../utils/assertable-state"; -import type { TestDefinition } from "../test-definition"; - -export const renamePendingCreateOntoPendingDeletePathTest: TestDefinition = { - description: - "A pending create is renamed onto a path whose old server document " + - "has a queued delete. The delete must reach the server before the " + - "new create so the new generation is not merged into the soon-to-be " + - "deleted document.", - clients: 2, - steps: [ - { type: "enable-sync", client: 0 }, - { type: "enable-sync", client: 1 }, - { type: "barrier" }, - - { - type: "create", - client: 1, - path: "file-17.md", - content: "old\n" - }, - { type: "barrier" }, - - { type: "pause-server" }, - { - type: "create", - client: 1, - path: "blocker.md", - content: "blocker\n" - }, - { type: "sleep", ms: 100 }, - { - type: "create", - client: 1, - path: "file-23.md", - content: "new\n" - }, - { type: "delete", client: 1, path: "file-17.md" }, - { - type: "rename", - client: 1, - oldPath: "file-23.md", - newPath: "file-17.md" - }, - { type: "resume-server" }, - { type: "barrier" }, - - { - type: "assert-consistent", - verify: (state: AssertableState): void => { - state - .assertFileCount(2) - .assertContent("blocker.md", "blocker\n") - .assertContent("file-17.md", "new\n") - .assertFileNotExists("file-23.md"); - } - } - ] -}; diff --git a/frontend/deterministic-tests/src/tests/rename-roundtrip.test.ts b/frontend/deterministic-tests/src/tests/rename-roundtrip.test.ts index 0373debf..bdf043f4 100644 --- a/frontend/deterministic-tests/src/tests/rename-roundtrip.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-roundtrip.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const renameRoundtripTest: TestDefinition = { @@ -9,32 +8,31 @@ export const renameRoundtripTest: TestDefinition = { { type: "create", client: 0, path: "A.md", content: "original" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent("A.md", "original"); - } + verify: (s) => s.assertContent("A.md", "original"), }, { type: "rename", client: 0, oldPath: "A.md", newPath: "B.md" }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileNotExists("A.md").assertContent("B.md", "original"); - } + verify: (s) => + s.assertFileNotExists("A.md").assertContent("B.md", "original"), }, { type: "rename", client: 0, oldPath: "B.md", newPath: "A.md" }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileNotExists("B.md").assertContent("A.md", "original"); - } + verify: (s) => + s.assertFileNotExists("B.md").assertContent("A.md", "original"), } ] }; diff --git a/frontend/deterministic-tests/src/tests/rename-swap.test.ts b/frontend/deterministic-tests/src/tests/rename-swap.test.ts index 9910e8ef..18489f33 100644 --- a/frontend/deterministic-tests/src/tests/rename-swap.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-swap.test.ts @@ -1,11 +1,11 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const renameSwapTest: TestDefinition = { description: "Client 0 has A.md and B.md synced. Goes offline and swaps them using " + "a temp file: A.md -> temp.md, B.md -> A.md, temp.md -> B.md. " + - "When Client 0 reconnects, both contents should exist across two files.", + "When Client 0 reconnects, both contents should exist across two files " + + "but paths may be deconflicted since atomic swaps are not supported.", clients: 2, steps: [ { type: "create", client: 0, path: "A.md", content: "content-a" }, @@ -15,12 +15,8 @@ export const renameSwapTest: TestDefinition = { { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent("A.md", "content-a").assertContent( - "B.md", - "content-b" - ); - } + verify: (s) => + s.assertContent("A.md", "content-a").assertContent("B.md", "content-b"), }, { type: "disable-sync", client: 0 }, @@ -33,12 +29,12 @@ export const renameSwapTest: TestDefinition = { { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileNotExists("temp.md") + verify: (s) => + s + .assertFileNotExists("temp.md") .assertFileCount(2) - .assertAnyFileContains("content-b") - .assertAnyFileContains("content-a"); - } + .assertContent("A.md", "content-b") + .assertContent("B.md", "content-a"), } ] }; diff --git a/frontend/deterministic-tests/src/tests/rename-to-existing-path.test.ts b/frontend/deterministic-tests/src/tests/rename-to-existing-path.test.ts new file mode 100644 index 00000000..b1d09c7f --- /dev/null +++ b/frontend/deterministic-tests/src/tests/rename-to-existing-path.test.ts @@ -0,0 +1,26 @@ +import type { TestDefinition } from "../test-definition"; + +export const renameToExistingPathTest: TestDefinition = { + description: + "Client 0 has A.md and B.md. Client 0 renames A.md to B.md (overwriting B.md). " + + "Both clients should converge: A.md gone, B.md has A.md's content.", + clients: 2, + steps: [ + { type: "create", client: 0, path: "A.md", content: "alpha" }, + { type: "create", client: 0, path: "B.md", content: "beta" }, + { type: "enable-sync", client: 0 }, + { type: "enable-sync", client: 1 }, + { type: "sync" }, + { type: "barrier" }, + + { type: "rename", client: 0, oldPath: "A.md", newPath: "B.md" }, + { type: "sync" }, + { type: "barrier" }, + + { + type: "assert-consistent", + verify: (s) => + s.assertFileNotExists("A.md").assertContent("B.md", "alpha"), + } + ] +}; diff --git a/frontend/deterministic-tests/src/tests/rename-to-path-of-unconfirmed-delete.test.ts b/frontend/deterministic-tests/src/tests/rename-to-path-of-unconfirmed-delete.test.ts index 34a3867c..b5745e3b 100644 --- a/frontend/deterministic-tests/src/tests/rename-to-path-of-unconfirmed-delete.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-to-path-of-unconfirmed-delete.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const renameToPathOfUnconfirmedDeleteTest: TestDefinition = { @@ -33,12 +32,10 @@ export const renameToPathOfUnconfirmedDeleteTest: TestDefinition = { { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileNotExists("B.md").assertContains( - "A.md", - "content B" - ); - } + verify: (s) => + s + .assertFileNotExists("B.md") + .assertContains("A.md", "content B"), } ] }; diff --git a/frontend/deterministic-tests/src/tests/rename-to-pending-path-fallback.test.ts b/frontend/deterministic-tests/src/tests/rename-to-pending-path-fallback.test.ts index 8747218a..a17f52d4 100644 --- a/frontend/deterministic-tests/src/tests/rename-to-pending-path-fallback.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-to-pending-path-fallback.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const renameToPendingPathFallbackTest: TestDefinition = { @@ -6,38 +5,26 @@ export const renameToPendingPathFallbackTest: TestDefinition = { "Client 0 creates B.md and syncs. Goes offline, creates A.md, then renames B.md to A.md (overwriting the unsynced A). After reconnecting, B.md should be gone and A.md should have B's content.", clients: 2, steps: [ - { - type: "create", - client: 0, - path: "B.md", - content: "tracked B content" - }, + { type: "create", client: 0, path: "B.md", content: "tracked B content" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "disable-sync", client: 0 }, - { - type: "create", - client: 0, - path: "A.md", - content: "pending A content" - }, + { type: "create", client: 0, path: "A.md", content: "pending A content" }, { type: "rename", client: 0, oldPath: "B.md", newPath: "A.md" }, { type: "enable-sync", client: 0 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileNotExists("B.md").assertContains( - "A.md", - "tracked B content" - ); - } + verify: (s) => + s.assertFileNotExists("B.md").assertContains("A.md", "tracked B content"), } ] }; diff --git a/frontend/deterministic-tests/src/tests/rename-to-recently-deleted-path.test.ts b/frontend/deterministic-tests/src/tests/rename-to-recently-deleted-path.test.ts new file mode 100644 index 00000000..754c0c18 --- /dev/null +++ b/frontend/deterministic-tests/src/tests/rename-to-recently-deleted-path.test.ts @@ -0,0 +1,40 @@ +import type { TestDefinition } from "../test-definition"; + +export const renameToRecentlyDeletedPathTest: TestDefinition = { + description: + "Client 0 deletes B.md. Client 1 renames A.md to B.md offline. After reconnecting, only B.md should exist with A's content.", + clients: 2, + steps: [ + { type: "create", client: 0, path: "A.md", content: "content-a" }, + { type: "create", client: 0, path: "B.md", content: "content-b" }, + { type: "enable-sync", client: 0 }, + { type: "enable-sync", client: 1 }, + { type: "sync" }, + { type: "barrier" }, + + { type: "disable-sync", client: 1 }, + + { type: "delete", client: 0, path: "B.md" }, + { type: "sync", client: 0 }, + + { + type: "rename", + client: 1, + oldPath: "A.md", + newPath: "B.md" + }, + + { type: "enable-sync", client: 1 }, + { type: "sync" }, + { type: "barrier" }, + + { + type: "assert-consistent", + verify: (s) => + s + .assertFileCount(1) + .assertFileNotExists("A.md") + .assertContent("B.md", "content-a"), + } + ] +}; diff --git a/frontend/deterministic-tests/src/tests/rename-update-conflict.test.ts b/frontend/deterministic-tests/src/tests/rename-update-conflict.test.ts index 18d4c101..099009fb 100644 --- a/frontend/deterministic-tests/src/tests/rename-update-conflict.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-update-conflict.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const renameUpdateConflictTest: TestDefinition = { @@ -9,12 +8,11 @@ export const renameUpdateConflictTest: TestDefinition = { { type: "create", client: 0, path: "A.md", content: "original" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent("A.md", "original"); - } + verify: (s) => s.assertContent("A.md", "original"), }, { type: "disable-sync", client: 1 }, @@ -22,21 +20,16 @@ export const renameUpdateConflictTest: TestDefinition = { { type: "rename", client: 0, oldPath: "A.md", newPath: "B.md" }, { type: "sync", client: 0 }, - { - type: "update", - client: 1, - path: "A.md", - content: "updated by client 1" - }, + { type: "update", client: 1, path: "A.md", content: "updated by client 1" }, { type: "enable-sync", client: 1 }, + { type: "sync", client: 1 }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileNotExists("A.md").assertContains("B.md", "updated"); - } + verify: (s) => + s.assertFileNotExists("A.md").assertContains("B.md", "updated"), } ] }; diff --git a/frontend/deterministic-tests/src/tests/renamed-pending-create-reused-path-then-delete.test.ts b/frontend/deterministic-tests/src/tests/renamed-pending-create-reused-path-then-delete.test.ts deleted file mode 100644 index 3ffb376e..00000000 --- a/frontend/deterministic-tests/src/tests/renamed-pending-create-reused-path-then-delete.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { AssertableState } from "../utils/assertable-state"; -import type { TestDefinition } from "../test-definition"; - -export const renamedPendingCreateReusedPathThenDeleteTest: TestDefinition = { - description: - "A queued create is renamed away from file-59.md, a newer local " + - "file reuses file-59.md before the queued create drains, and the " + - "renamed-away generation is deleted. The delete must not erase or " + - "orphan the newer file-59.md generation.", - clients: 2, - steps: [ - { type: "enable-sync", client: 0 }, - { type: "enable-sync", client: 1 }, - { type: "barrier" }, - - { type: "pause-server" }, - { - type: "create", - client: 1, - path: "blocker.md", - content: "blocker\n" - }, - { type: "sleep", ms: 100 }, - - { - type: "create", - client: 1, - path: "file-59.md", - content: "old\n" - }, - { - type: "rename", - client: 1, - oldPath: "file-59.md", - newPath: "file-33.md" - }, - { - type: "create", - client: 1, - path: "file-59.md", - content: "new\n" - }, - - { - type: "resume-server-until-history-then-pause", - client: 1, - syncType: "CREATE", - path: "file-33.md" - }, - { type: "delete", client: 1, path: "file-33.md" }, - { type: "resume-server" }, - { type: "barrier" }, - - { - type: "assert-consistent", - verify: (state: AssertableState): void => { - state - .assertFileCount(2) - .assertContent("blocker.md", "blocker\n") - .assertContent("file-59.md", "new\n") - .assertFileNotExists("file-33.md"); - } - } - ] -}; diff --git a/frontend/deterministic-tests/src/tests/reset-clears-recently-deleted-resurrection.test.ts b/frontend/deterministic-tests/src/tests/reset-clears-recently-deleted-resurrection.test.ts index e0a1565c..e7b001e2 100644 --- a/frontend/deterministic-tests/src/tests/reset-clears-recently-deleted-resurrection.test.ts +++ b/frontend/deterministic-tests/src/tests/reset-clears-recently-deleted-resurrection.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const resetClearsRecentlyDeletedResurrectionTest: TestDefinition = { @@ -16,28 +15,28 @@ export const resetClearsRecentlyDeletedResurrectionTest: TestDefinition = { }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "delete", client: 0, path: "ghost.md" }, + { type: "sync", client: 0 }, + + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileNotExists("ghost.md"); - } + verify: (s) => s.assertFileNotExists("ghost.md"), }, { type: "disable-sync", client: 1 }, { type: "enable-sync", client: 1 }, - + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(0); - } + verify: (s) => s.assertFileCount(0), } ] }; diff --git a/frontend/deterministic-tests/src/tests/same-doc-id-collapse-after-remote-quick-write-and-pending-rename.test.ts b/frontend/deterministic-tests/src/tests/same-doc-id-collapse-after-remote-quick-write-and-pending-rename.test.ts deleted file mode 100644 index 2a3b5de4..00000000 --- a/frontend/deterministic-tests/src/tests/same-doc-id-collapse-after-remote-quick-write-and-pending-rename.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { AssertableState } from "../utils/assertable-state"; -import type { TestDefinition } from "../test-definition"; - -export const sameDocIdCollapseAfterRemoteQuickWriteAndPendingRenameTest: TestDefinition = - { - description: - "A remote create starts quick-writing at doc.md while a local " + - "create for the same path is queued and renamed to renamed.md. " + - "Because the local create was renamed before it reached the " + - "server, the two generations should remain separate tracked " + - "documents.", - clients: 2, - steps: [ - { type: "enable-sync", client: 0 }, - - // Create a deleted latest version before client 1 joins. - // Catch-up will advance MinCovered with a non-contiguous id, - // keeping client 1's create lastSeen low enough to exercise - // the server's same-doc merge path from the e2e failure. - { - type: "create", - client: 0, - path: "history.md", - content: "history-v1" - }, - { type: "sync", client: 0 }, - { - type: "update", - client: 0, - path: "history.md", - content: "history-v2" - }, - { type: "sync", client: 0 }, - { type: "delete", client: 0, path: "history.md" }, - { type: "sync", client: 0 }, - - { type: "enable-sync", client: 1 }, - { type: "sync", client: 1 }, - - { type: "pause-websocket", client: 1 }, - - { - type: "create", - client: 0, - path: "doc.md", - content: "remote\n" - }, - { type: "sync", client: 0 }, - - // Let client 1's buffered RemoteCreate enter the quick-write - // path, but hold the content fetch until the local create has - // appeared and moved away from doc.md. - { type: "pause-server" }, - { type: "resume-websocket", client: 1 }, - { type: "sleep", ms: 100 }, - - { - type: "create", - client: 1, - path: "doc.md", - content: "local\n" - }, - { - type: "rename", - client: 1, - oldPath: "doc.md", - newPath: "renamed.md" - }, - - { type: "resume-server" }, - { type: "barrier" }, - - { - type: "assert-consistent", - verify: (state: AssertableState): void => { - state.assertFileCount(2); - state.assertContent("doc.md", "remote\n"); - state.assertContent("renamed.md", "local\n"); - } - } - ] - }; diff --git a/frontend/deterministic-tests/src/tests/same-doc-id-collapse-on-local-create-after-remote-create.test.ts b/frontend/deterministic-tests/src/tests/same-doc-id-collapse-on-local-create-after-remote-create.test.ts deleted file mode 100644 index dee3a9ad..00000000 --- a/frontend/deterministic-tests/src/tests/same-doc-id-collapse-on-local-create-after-remote-create.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -import type { AssertableState } from "../utils/assertable-state"; -import type { TestDefinition } from "../test-definition"; - -export const sameDocIdCollapseOnLocalCreateAfterRemoteCreateTest: TestDefinition = - { - description: - "Client B creates X with content C2; the server commits and " + - "broadcasts. Client A's WS is paused so the RemoteCreate buffers. " + - "Server is then paused so A's about-to-POST LocalCreate will " + - "hang. A creates X with content C1: file lands on disk, " + - "LocalCreate enqueues, drain starts the POST, the POST stalls " + - "at the paused server. A's WS is resumed: the buffered " + - "RemoteCreate for doc-X is delivered to A and enqueues behind " + - "the in-flight LocalCreate. Per the lazy-paths model, when " + - "the RemoteCreate is processed it observes that path X is " + - "occupied locally by A's pending-create bytes, so it tracks " + - "doc-X with `localPath = undefined` / `remoteRelativePath = " + - "X` and does NOT fetch content. The server is then resumed: " + - "A's LocalCreate POST returns. The server, finding X already " + - "taken by doc-X, replies with doc-X's existing documentId " + - "(typically a MergingUpdate carrying the merged bytes). A's " + - "processCreate handler detects that response.documentId " + - "matches the no-localPath record built from the RemoteCreate " + - "and collapses the two: it sets localPath = X on that " + - "record, writes the merged bytes, and resolves the pending " + - "create promise. Final state: exactly one file at X on both " + - "clients, both pointing at doc-X's documentId, content " + - "carrying both contributions, and no conflict-- " + - "stash anywhere.", - clients: 2, - steps: [ - { type: "enable-sync", client: 0 }, - { type: "enable-sync", client: 1 }, - { type: "barrier" }, - - // Buffer broadcasts to client 0 (A) so client 1's create - // doesn't reach A's WS handler until we say so. - { type: "pause-websocket", client: 0 }, - - // Client 1 (B) commits doc-X at path X with content C2. - // The server commits, broadcasts (broadcast queued at A's - // paused WS). - { - type: "create", - client: 1, - path: "X.md", - content: "from-client-1 " - }, - { type: "sync", client: 1 }, - - // Pause the server so A's upcoming LocalCreate POST hangs. - // This holds A's drain on the in-flight POST while we - // release the WS so the RemoteCreate enqueues behind it. - { type: "pause-server" }, - - // Client 0 (A) creates X locally with content C1. The - // file lands on A's disk; LocalCreate enqueues; drain - // starts the POST; POST stalls at the paused server. - { - type: "create", - client: 0, - path: "X.md", - content: "from-client-0 " - }, - - // Release A's WS. The buffered RemoteCreate for doc-X is - // delivered to A and enqueues behind the in-flight - // LocalCreate. Whichever of (RemoteCreate processed first - // → no-localPath record, then LocalCreate POST returns - // with merging response that collapses) or (LocalCreate - // POST returns first with merging response that creates - // the canonical record, then RemoteCreate finds the doc - // already tracked by id and no-ops) actually plays out - // depends on the fine-grained interleaving the runtime - // produces, but both paths are required to converge to - // the same single-record same-docId state. - { type: "resume-websocket", client: 0 }, - - // Resume the server: A's LocalCreate POST completes. - // Server returns doc-X's existing documentId (MergingUpdate - // with merged content). processCreate runs the collapse - // path. - { type: "resume-server" }, - - { type: "barrier" }, - - { - type: "assert-consistent", - verify: (state: AssertableState): void => { - state.assertFileCount(1); - state.assertFileExists("X.md"); - // Server-side merge of the two text creates must - // carry both contributions through to the - // converged file. - state.assertContains( - "X.md", - "from-client-0", - "from-client-1" - ); - // The lazy-paths collapse path must not leave a - // conflict-- stash on either client. - for (const path of state.files.keys()) { - if (path.startsWith("conflict-")) { - throw new Error( - `Unexpected conflict-uuid stash on a converged client: ${path}` - ); - } - } - for (const perClient of state.clientFiles) { - for (const path of perClient.keys()) { - if (path.startsWith("conflict-")) { - throw new Error( - `Unexpected conflict-uuid stash on a per-client view: ${path}` - ); - } - } - } - } - } - ] - }; diff --git a/frontend/deterministic-tests/src/tests/self-merge-pending-rename-aliases-second-create.test.ts b/frontend/deterministic-tests/src/tests/self-merge-pending-rename-aliases-second-create.test.ts deleted file mode 100644 index ac8ed3ed..00000000 --- a/frontend/deterministic-tests/src/tests/self-merge-pending-rename-aliases-second-create.test.ts +++ /dev/null @@ -1,152 +0,0 @@ -import type { AssertableState } from "../utils/assertable-state"; -import type { TestDefinition } from "../test-definition"; - -export const selfMergePendingRenameAliasesSecondCreateTest: TestDefinition = { - description: - "Single client makes two distinct creates that briefly share a path. " + - "Client 0 POSTs the first create at primary.md while the server is " + - "paused. While that POST is in flight: a second create is queued at " + - "staging.md, primary.md is renamed to moved.md (rewriting the in- " + - "flight create's event.path to moved.md and pushing a rename " + - "LocalUpdate at the queue tail), and staging.md is renamed onto the " + - "now-vacated primary.md slot (rewriting the second create's " + - "event.path to primary.md and pushing another rename LocalUpdate). " + - "Client 0's WS is paused throughout, so its watermark stays at 0. " + - "On resume the first POST commits Doc-X at primary.md (creation_vuid " + - "= N). The drain then processes the second LocalCreate (POST " + - "relativePath=primary.md, last_seen=0); the server's path-based " + - "dedup sees N > 0 and merges the second create into Doc-X " + - "(MergingUpdate). The buggy behaviour: processCreate's resolveCreate " + - "calls upsertRecord with localPath=primary.md, but the existing " + - "record (from the first create) already holds localPath=moved.md, " + - "and upsertRecord's `existing.localPath !== undefined` guard " + - "silently drops the new claim. The file at primary.md is left " + - "orphaned: tracked by no record, never broadcast, never deleted. " + - "After the user's renames the expected user-visible state is two " + - "distinct files at moved.md and primary.md — both clients must " + - "converge to that.", - clients: 2, - steps: [ - // Both clients online so the WS connection is established before - // the test starts pausing things. - { type: "enable-sync", client: 0 }, - { type: "enable-sync", client: 1 }, - { type: "barrier" }, - - // Pause client 0's WS so its MinCovered watermark stays at 0 - // through the whole bug sequence. The merge condition the - // server is going to fire is `creation_vuid > last_seen`; with - // a non-zero gap the same-device second create gets merged - // into the same-device first create. - { type: "pause-websocket", client: 0 }, - - // Client 1 commits a doc to push the server's vuid above 0. - // Without this filler, Doc-X's create vuid could be 1 and - // client 0's last_seen.add(1) would advance min to 1, killing - // the watermark gap that triggers the merge. - { - type: "create", - client: 1, - path: "filler.md", - content: "filler-content " - }, - { type: "sync", client: 1 }, - - // Pause the server so client 0's first create POST hangs in - // flight, giving us a deterministic window in which to enqueue - // the second create and the renames. - { type: "pause-server" }, - - // First create — Doc-X. The wire-loop drains it, captures - // requestPath = event.path = "primary.md", reads the bytes, - // sends the POST, and stalls on the response. - { - type: "create", - client: 0, - path: "primary.md", - content: "primary content " - }, - - // Make sure the POST is actually on the wire with - // relativePath="primary.md" before we rewrite event.path. - // Without this delay the rename can win the race, the POST - // goes out with relativePath="moved.md", and the server-side - // path-collision merge never fires. - { type: "sleep", ms: 100 }, - - // Second create at a staging path. The wire-loop is still - // blocked on Doc-X's POST, so this LocalCreate just queues at - // index 1. - { - type: "create", - client: 0, - path: "staging.md", - content: "secondary content " - }, - - // Rename Doc-X's path. enqueue's pending-create branch - // rewrites Doc-X's event.path in place (moved.md) and pushes - // a LocalUpdate(rename, originalPath=moved.md) at the END of - // the queue. Note the ordering: this LocalUpdate is enqueued - // AFTER the staging LocalCreate above. That ordering is - // load-bearing — it is what makes the second create's POST - // drain (and trigger the server-side merge) before Doc-X's - // rename PUT moves the doc away from primary.md on the - // server. - { - type: "rename", - client: 0, - oldPath: "primary.md", - newPath: "moved.md" - }, - - // Rename the staging file onto Doc-X's now-vacated primary.md - // slot. enqueue rewrites the staging LocalCreate's event.path - // to primary.md and pushes a LocalUpdate(rename, - // originalPath=primary.md) at the queue tail. After this the - // disk has: moved.md = Doc-X's bytes, primary.md = Doc-Y's - // bytes. - { - type: "rename", - client: 0, - oldPath: "staging.md", - newPath: "primary.md" - }, - - // Let everything fly: server processes the queued POSTs; - // client 0 catches up on broadcasts. - { type: "resume-server" }, - { type: "resume-websocket", client: 0 }, - - { type: "barrier" }, - - { - type: "assert-consistent", - verify: (state: AssertableState): void => { - // The user did two distinct creates (Doc-X and Doc-Y); - // both contents must survive on both clients. - state.assertFileCount(3); - state.assertFileExists("filler.md"); - state.assertFileExists("moved.md"); - state.assertFileExists("primary.md"); - - // After the renames the user expects: - // - moved.md = the file that was originally created - // at primary.md (Doc-X's content). - // - primary.md = the file that was originally created - // at staging.md (Doc-Y's content). - state.assertContains("moved.md", "primary content"); - state.assertContains("primary.md", "secondary content"); - - // No content cross-contamination: each contribution - // should land in exactly one of the user-visible - // files. Under the bug, the orphan at primary.md - // carries Doc-X's content (because Doc-Y's PUT was - // aliased onto Doc-X's record and read Doc-X's bytes - // from moved.md), so this catches the leak too. - state.assertContentInAtMostOneFile("primary content"); - state.assertContentInAtMostOneFile("secondary content"); - } - } - ] -}; diff --git a/frontend/deterministic-tests/src/tests/sequential-create-duplicate-content.test.ts b/frontend/deterministic-tests/src/tests/sequential-create-duplicate-content.test.ts index 611e1ae3..968166a9 100644 --- a/frontend/deterministic-tests/src/tests/sequential-create-duplicate-content.test.ts +++ b/frontend/deterministic-tests/src/tests/sequential-create-duplicate-content.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const sequentialCreateDuplicateContentTest: TestDefinition = { @@ -6,38 +5,28 @@ export const sequentialCreateDuplicateContentTest: TestDefinition = { "Client 0 creates A.md, syncs, then creates B.md with identical content. Both files must remain as separate documents on both clients.", clients: 2, steps: [ - { - type: "create", - client: 0, - path: "A.md", - content: "identical content here" - }, + { type: "create", client: 0, path: "A.md", content: "identical content here" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent("A.md", "identical content here"); - } + verify: (s) => s.assertContent("A.md", "identical content here"), }, - { - type: "create", - client: 0, - path: "B.md", - content: "identical content here" - }, + { type: "create", client: 0, path: "B.md", content: "identical content here" }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(2) + verify: (s) => + s + .assertFileCount(2) .assertContent("A.md", "identical content here") - .assertContent("B.md", "identical content here"); - } + .assertContent("B.md", "identical content here"), } ] }; diff --git a/frontend/deterministic-tests/src/tests/server-pause-both-clients-create.test.ts b/frontend/deterministic-tests/src/tests/server-pause-both-clients-create.test.ts index f99cf92d..fea4adad 100644 --- a/frontend/deterministic-tests/src/tests/server-pause-both-clients-create.test.ts +++ b/frontend/deterministic-tests/src/tests/server-pause-both-clients-create.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const serverPauseBothClientsCreateTest: TestDefinition = { @@ -8,6 +7,7 @@ export const serverPauseBothClientsCreateTest: TestDefinition = { steps: [ { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { @@ -27,16 +27,15 @@ export const serverPauseBothClientsCreateTest: TestDefinition = { { type: "resume-server" }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContains("alpha.md", "from client 0").assertContains( - "beta.md", - "from client 1" - ); - } + verify: (s) => + s + .assertContains("alpha.md", "from client 0") + .assertContains("beta.md", "from client 1"), } ] }; diff --git a/frontend/deterministic-tests/src/tests/server-pause-both-edit-same-file.test.ts b/frontend/deterministic-tests/src/tests/server-pause-both-edit-same-file.test.ts index ff8cf194..394a531a 100644 --- a/frontend/deterministic-tests/src/tests/server-pause-both-edit-same-file.test.ts +++ b/frontend/deterministic-tests/src/tests/server-pause-both-edit-same-file.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const serverPauseBothEditSameFileTest: TestDefinition = { @@ -14,6 +13,7 @@ export const serverPauseBothEditSameFileTest: TestDefinition = { }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "pause-server" }, @@ -34,17 +34,15 @@ export const serverPauseBothEditSameFileTest: TestDefinition = { }, { type: "resume-server" }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContains( - "shared.md", - "edited by client 0", - "edited by client 1" - ); - } + verify: (s) => + s + .assertFileCount(1) + .assertContains("shared.md", "edited by client 0", "edited by client 1"), }, { @@ -53,16 +51,13 @@ export const serverPauseBothEditSameFileTest: TestDefinition = { path: "shared.md", content: "post-merge edit from client 0" }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContains( - "shared.md", - "post-merge edit from client 0" - ); - } + verify: (s) => + s.assertFileCount(1).assertContains("shared.md", "post-merge edit from client 0"), } ] }; diff --git a/frontend/deterministic-tests/src/tests/server-pause-delete-recreate.test.ts b/frontend/deterministic-tests/src/tests/server-pause-delete-recreate.test.ts index 5ac97f0d..920259e1 100644 --- a/frontend/deterministic-tests/src/tests/server-pause-delete-recreate.test.ts +++ b/frontend/deterministic-tests/src/tests/server-pause-delete-recreate.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const serverPauseDeleteRecreateTest: TestDefinition = { @@ -16,23 +15,18 @@ export const serverPauseDeleteRecreateTest: TestDefinition = { { type: "pause-server" }, - { - type: "create", - client: 0, - path: "A.md", - content: "recreated during contention" - }, + { type: "create", client: 0, path: "A.md", content: "recreated during contention" }, { type: "resume-server" }, { type: "barrier" }, { type: "assert-consistent", - verify: (state: AssertableState): void => { + verify: (state) => { state .assertFileCount(1) .assertContent("A.md", "recreated during contention"); } - } - ] + }, + ], }; diff --git a/frontend/deterministic-tests/src/tests/server-pause-rename-edit-resume.test.ts b/frontend/deterministic-tests/src/tests/server-pause-rename-edit-resume.test.ts index b1739135..c2d6772e 100644 --- a/frontend/deterministic-tests/src/tests/server-pause-rename-edit-resume.test.ts +++ b/frontend/deterministic-tests/src/tests/server-pause-rename-edit-resume.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const serverPauseRenameEditResumeTest: TestDefinition = { @@ -16,12 +15,11 @@ export const serverPauseRenameEditResumeTest: TestDefinition = { path: "A.md", content: "original content" }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent("A.md", "original content"); - } + verify: (s) => s.assertContent("A.md", "original content"), }, { type: "pause-server" }, @@ -36,15 +34,16 @@ export const serverPauseRenameEditResumeTest: TestDefinition = { { type: "resume-server" }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1) + verify: (s) => + s + .assertFileCount(1) .assertFileNotExists("A.md") - .assertContent("B.md", "edited after rename during pause"); - } + .assertContent("B.md", "edited after rename during pause"), } ] }; diff --git a/frontend/deterministic-tests/src/tests/server-pause-update-and-create.test.ts b/frontend/deterministic-tests/src/tests/server-pause-update-and-create.test.ts index 2389ccf5..3523cf79 100644 --- a/frontend/deterministic-tests/src/tests/server-pause-update-and-create.test.ts +++ b/frontend/deterministic-tests/src/tests/server-pause-update-and-create.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const serverPauseUpdateAndCreateTest: TestDefinition = { @@ -14,12 +13,11 @@ export const serverPauseUpdateAndCreateTest: TestDefinition = { path: "shared.md", content: "initial content" }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent("shared.md", "initial content"); - } + verify: (s) => s.assertContent("shared.md", "initial content"), }, { type: "pause-server" }, @@ -39,16 +37,15 @@ export const serverPauseUpdateAndCreateTest: TestDefinition = { { type: "resume-server" }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertContent( - "shared.md", - "updated during pause" - ).assertContent("new-file.md", "created by client 1"); - } + verify: (s) => + s + .assertContent("shared.md", "updated during pause") + .assertContent("new-file.md", "created by client 1"), } ] }; diff --git a/frontend/deterministic-tests/src/tests/simultaneous-create-delete-same-path.test.ts b/frontend/deterministic-tests/src/tests/simultaneous-create-delete-same-path.test.ts index 7ec116ac..2e74b3a5 100644 --- a/frontend/deterministic-tests/src/tests/simultaneous-create-delete-same-path.test.ts +++ b/frontend/deterministic-tests/src/tests/simultaneous-create-delete-same-path.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const simultaneousCreateDeleteSamePathTest: TestDefinition = { @@ -11,6 +10,7 @@ export const simultaneousCreateDeleteSamePathTest: TestDefinition = { { type: "create", client: 0, path: "A.md", content: "original from 0" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "disable-sync", client: 1 }, @@ -18,21 +18,22 @@ export const simultaneousCreateDeleteSamePathTest: TestDefinition = { { type: "delete", client: 0, path: "A.md" }, { type: "sync", client: 0 }, - { - type: "update", - client: 1, - path: "A.md", - content: "modified by 1 while offline" - }, + { type: "update", client: 1, path: "A.md", content: "modified by 1 while offline" }, { type: "enable-sync", client: 1 }, + { type: "sync", client: 1 }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(0); - } + verify: (s) => { + s.ifFileExists("A.md", (s) => + s.assertFileCount(1).assertContent("A.md", "modified by 1 while offline") + ); + if (!s.files.has("A.md")) { + s.assertFileCount(0); + } + }, } ] }; diff --git a/frontend/deterministic-tests/src/tests/three-client-rename-create-delete.test.ts b/frontend/deterministic-tests/src/tests/three-client-rename-create-delete.test.ts index 80478adc..174bcdc4 100644 --- a/frontend/deterministic-tests/src/tests/three-client-rename-create-delete.test.ts +++ b/frontend/deterministic-tests/src/tests/three-client-rename-create-delete.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const threeClientRenameCreateDeleteTest: TestDefinition = { @@ -45,11 +44,10 @@ export const threeClientRenameCreateDeleteTest: TestDefinition = { { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileNotExists("X.md").assertAnyFileContains( - "new from C" - ); - } + verify: (s) => + s + .assertFileNotExists("X.md") + .assertAnyFileContains("new from C"), } ] }; diff --git a/frontend/deterministic-tests/src/tests/update-during-create-processing.test.ts b/frontend/deterministic-tests/src/tests/update-during-create-processing.test.ts index ca53244e..43536bed 100644 --- a/frontend/deterministic-tests/src/tests/update-during-create-processing.test.ts +++ b/frontend/deterministic-tests/src/tests/update-during-create-processing.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const updateDuringCreateProcessingTest: TestDefinition = { @@ -8,6 +7,7 @@ export const updateDuringCreateProcessingTest: TestDefinition = { steps: [ { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "pause-server" }, @@ -27,16 +27,13 @@ export const updateDuringCreateProcessingTest: TestDefinition = { }, { type: "resume-server" }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContent( - "file.md", - "updated during create" - ); - } + verify: (s) => + s.assertFileCount(1).assertContent("file.md", "updated during create"), } ] }; diff --git a/frontend/deterministic-tests/src/tests/update-survives-remote-delete.test.ts b/frontend/deterministic-tests/src/tests/update-survives-remote-delete.test.ts index 70a2fc8c..09ec9427 100644 --- a/frontend/deterministic-tests/src/tests/update-survives-remote-delete.test.ts +++ b/frontend/deterministic-tests/src/tests/update-survives-remote-delete.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const updateDoesNotSurvivesRemoteDeleteTest: TestDefinition = { @@ -15,12 +14,7 @@ export const updateDoesNotSurvivesRemoteDeleteTest: TestDefinition = { { type: "disable-sync", client: 1 }, { type: "delete", client: 0, path: "doc.md" }, - { - type: "update", - client: 1, - path: "doc.md", - content: "edited by client 1" - }, + { type: "update", client: 1, path: "doc.md", content: "edited by client 1" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, @@ -28,9 +22,8 @@ export const updateDoesNotSurvivesRemoteDeleteTest: TestDefinition = { { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(0); - } - } - ] + verify: (s) => + s.assertFileCount(0) + }, + ], }; diff --git a/frontend/deterministic-tests/src/tests/watermark-advances-on-skip.test.ts b/frontend/deterministic-tests/src/tests/watermark-advances-on-skip.test.ts index 063faff4..202bd437 100644 --- a/frontend/deterministic-tests/src/tests/watermark-advances-on-skip.test.ts +++ b/frontend/deterministic-tests/src/tests/watermark-advances-on-skip.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const watermarkAdvancesOnSkipTest: TestDefinition = { @@ -8,6 +7,7 @@ export const watermarkAdvancesOnSkipTest: TestDefinition = { steps: [ { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "disable-sync", client: 0 }, @@ -17,19 +17,19 @@ export const watermarkAdvancesOnSkipTest: TestDefinition = { { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "disable-sync", client: 0 }, { type: "disable-sync", client: 1 }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1).assertFileExists("doc.md"); - } - } - ] + verify: (s) => s.assertFileCount(1).assertFileExists("doc.md"), + }, + ], }; diff --git a/frontend/deterministic-tests/src/tests/watermark-gap-remote-update-not-recorded.test.ts b/frontend/deterministic-tests/src/tests/watermark-gap-remote-update-not-recorded.test.ts index ac9ba467..0f5ade3d 100644 --- a/frontend/deterministic-tests/src/tests/watermark-gap-remote-update-not-recorded.test.ts +++ b/frontend/deterministic-tests/src/tests/watermark-gap-remote-update-not-recorded.test.ts @@ -1,4 +1,3 @@ -import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const watermarkGapRemoteUpdateNotRecordedTest: TestDefinition = { @@ -9,29 +8,31 @@ export const watermarkGapRemoteUpdateNotRecordedTest: TestDefinition = { { type: "create", client: 0, path: "doc.md", content: "original" }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "update", client: 0, path: "doc.md", content: "update 1" }, { type: "sync", client: 0 }, { type: "update", client: 0, path: "doc.md", content: "update 2" }, + { type: "sync", client: 0 }, + { type: "sync", client: 1 }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContent("doc.md", "update 2"); - } + verify: (s) => + s.assertFileCount(1).assertContent("doc.md", "update 2"), }, { type: "disable-sync", client: 1 }, { type: "enable-sync", client: 1 }, + { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s: AssertableState): void => { - s.assertFileCount(1).assertContent("doc.md", "update 2"); - } + verify: (s) => + s.assertFileCount(1).assertContent("doc.md", "update 2"), } ] }; diff --git a/frontend/deterministic-tests/src/utils/assertable-state.ts b/frontend/deterministic-tests/src/utils/assertable-state.ts index 7c6f192c..05414342 100644 --- a/frontend/deterministic-tests/src/utils/assertable-state.ts +++ b/frontend/deterministic-tests/src/utils/assertable-state.ts @@ -1,15 +1,15 @@ import type { ClientState } from "../test-definition"; export class AssertableState { - public readonly files: Map; - public readonly clientFiles: Map[]; + readonly files: Map; + readonly clientFiles: Map[]; - public constructor(state: ClientState) { + constructor(state: ClientState) { this.files = state.files; this.clientFiles = state.clientFiles; } - public assertFileCount(expected: number): this { + assertFileCount(expected: number): this { if (this.files.size !== expected) { const keys = Array.from(this.files.keys()).join(", "); throw new Error( @@ -19,15 +19,17 @@ export class AssertableState { return this; } - public assertFileExists(path: string): this { + assertFileExists(path: string): this { if (!this.files.has(path)) { const keys = Array.from(this.files.keys()).join(", "); - throw new Error(`Expected "${path}" to exist. Files: [${keys}]`); + throw new Error( + `Expected "${path}" to exist. Files: [${keys}]` + ); } return this; } - public assertFileNotExists(path: string): this { + assertFileNotExists(path: string): this { if (this.files.has(path)) { const keys = Array.from(this.files.keys()).join(", "); throw new Error( @@ -37,7 +39,7 @@ export class AssertableState { return this; } - public assertContent(path: string, expected: string): this { + assertContent(path: string, expected: string): this { this.assertFileExists(path); const actual = this.files.get(path) ?? ""; if (actual !== expected) { @@ -48,7 +50,7 @@ export class AssertableState { return this; } - public assertContains(path: string, ...substrings: string[]): this { + assertContains(path: string, ...substrings: string[]): this { this.assertFileExists(path); const content = this.files.get(path) ?? ""; const missing = substrings.filter((s) => !content.includes(s)); @@ -60,7 +62,7 @@ export class AssertableState { return this; } - public assertContainsAny(path: string, ...substrings: string[]): this { + assertContainsAny(path: string, ...substrings: string[]): this { this.assertFileExists(path); const content = this.files.get(path) ?? ""; const found = substrings.some((s) => content.includes(s)); @@ -72,7 +74,7 @@ export class AssertableState { return this; } - public assertAnyFileContains(...substrings: string[]): this { + assertAnyFileContains(...substrings: string[]): this { const allContent = Array.from(this.files.values()).join("\n"); const missing = substrings.filter((s) => !allContent.includes(s)); if (missing.length > 0) { @@ -86,27 +88,7 @@ export class AssertableState { return this; } - public assertNoFileContains(...substrings: string[]): this { - const offenders: { path: string; substring: string }[] = []; - for (const [path, content] of this.files) { - for (const s of substrings) { - if (content.includes(s)) { - offenders.push({ path, substring: s }); - } - } - } - if (offenders.length > 0) { - const dump = Array.from(this.files.entries()) - .map(([k, v]) => ` ${k}: "${v}"`) - .join("\n"); - throw new Error( - `Expected no file to contain ${substrings.map((s) => `"${s}"`).join(", ")}, but found ${offenders.map((o) => `"${o.substring}" in "${o.path}"`).join(", ")}.\nFiles:\n${dump}` - ); - } - return this; - } - - public assertSubstringCount( + assertSubstringCount( path: string, substring: string, expected: number @@ -122,7 +104,7 @@ export class AssertableState { return this; } - public assertContentInAtMostOneFile(substring: string): this { + assertContentInAtMostOneFile(substring: string): this { const matches = Array.from(this.files.entries()).filter(([, content]) => content.includes(substring) ); @@ -137,14 +119,14 @@ export class AssertableState { return this; } - public ifFileExists(path: string, fn: (state: this) => void): this { + ifFileExists(path: string, fn: (state: this) => void): this { if (this.files.has(path)) { fn(this); } return this; } - public getContent(path: string): string { + getContent(path: string): string { return this.files.get(path) ?? ""; } } diff --git a/frontend/deterministic-tests/src/utils/find-free-port.ts b/frontend/deterministic-tests/src/utils/find-free-port.ts index 0734c1a9..3c965049 100644 --- a/frontend/deterministic-tests/src/utils/find-free-port.ts +++ b/frontend/deterministic-tests/src/utils/find-free-port.ts @@ -1,6 +1,6 @@ import * as net from "node:net"; -interface PortReservation { +export interface PortReservation { port: number; release: () => void; } diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs index eed30760..61a8bade 100644 --- a/frontend/eslint.config.mjs +++ b/frontend/eslint.config.mjs @@ -19,7 +19,6 @@ export default [ rules: { "no-console": "error", "no-unused-vars": "off", - "curly": ["error", "all"], "@typescript-eslint/restrict-template-expressions": "off", "@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-floating-promises": [ diff --git a/frontend/history-ui/src/components/ActivityFeed.svelte b/frontend/history-ui/src/components/ActivityFeed.svelte index b20991e2..c1c82c29 100644 --- a/frontend/history-ui/src/components/ActivityFeed.svelte +++ b/frontend/history-ui/src/components/ActivityFeed.svelte @@ -1,5 +1,5 @@