From 7b7801b02996175925d74f997bbdf42a44523341 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 17 Jul 2026 17:50:56 +0000 Subject: [PATCH] test: verify the packaged Commonlib boundary --- .github/workflows/cli-deno-tests.yml | 8 --- package-lock.json | 8 +-- package.json | 5 +- src/apps/cli/commands/runCommand.ts | 7 ++- src/apps/cli/package.json | 11 ++--- src/apps/cli/testdeno/CONTRIBUTING_TESTS.md | 1 + src/apps/cli/testdeno/deno.json | 4 +- src/apps/cli/testdeno/run-ci-suite.ts | 32 ++++++++++++ src/apps/cli/testdeno/run-compose-p2p.ts | 48 ++++++++++++++++++ src/apps/cli/testdeno/test_dev_deno.md | 25 ++++++++-- .../webapp/obsidianMockExports.unit.spec.ts | 49 +++++++++++++++++++ test/harness/obsidian-mock.ts | 4 ++ 12 files changed, 176 insertions(+), 26 deletions(-) create mode 100644 src/apps/cli/testdeno/run-ci-suite.ts create mode 100644 src/apps/cli/testdeno/run-compose-p2p.ts create mode 100644 src/apps/webapp/obsidianMockExports.unit.spec.ts diff --git a/.github/workflows/cli-deno-tests.yml b/.github/workflows/cli-deno-tests.yml index 4df20bc5..93cda742 100644 --- a/.github/workflows/cli-deno-tests.yml +++ b/.github/workflows/cli-deno-tests.yml @@ -25,8 +25,6 @@ on: type: choice options: - test:ci - - test:p2p - - test:all - test:local - test:e2e-matrix default: test:ci @@ -60,12 +58,6 @@ jobs: test:ci) TASK_MATRIX='["test:setup-put-cat","test:mirror","test:daemon","test:push-pull","test:decoupled-vault","test:sync-two-local","test:sync-locked-remote","test:remote-commands","test:e2e-matrix:couchdb-enc0","test:e2e-matrix:couchdb-enc1","test:e2e-matrix:minio-enc0","test:e2e-matrix:minio-enc1"]' ;; - test:p2p) - TASK_MATRIX='["test:p2p-host","test:p2p-peers","test:p2p-sync","test:p2p-three-nodes","test:p2p-upload-download"]' - ;; - test:all) - TASK_MATRIX='["test:setup-put-cat","test:mirror","test:daemon","test:push-pull","test:decoupled-vault","test:sync-two-local","test:sync-locked-remote","test:remote-commands","test:p2p-host","test:p2p-peers","test:p2p-sync","test:p2p-three-nodes","test:p2p-upload-download","test:e2e-matrix:couchdb-enc0","test:e2e-matrix:couchdb-enc1","test:e2e-matrix:minio-enc0","test:e2e-matrix:minio-enc1"]' - ;; test:local) TASK_MATRIX='["test:setup-put-cat","test:mirror","test:daemon"]' ;; diff --git a/package-lock.json b/package-lock.json index 9dee857b..99f8ef1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@smithy/types": "^4.14.3", "@smithy/util-retry": "^4.4.5", "@trystero-p2p/nostr": "^0.24.0", - "@vrtmrz/livesync-commonlib": "0.1.0-rc.0", + "@vrtmrz/livesync-commonlib": "0.1.0-rc.1", "diff-match-patch": "^1.0.5", "fflate": "^0.8.2", "idb": "^8.0.3", @@ -4978,9 +4978,9 @@ } }, "node_modules/@vrtmrz/livesync-commonlib": { - "version": "0.1.0-rc.0", - "resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.0-rc.0.tgz", - "integrity": "sha512-Aa+xC7bG78M7H8leIMmZdTRzA0Xh+ZgmiZJv0pXjy5OdZTPDu1H9M0eRcs5xL2pNBiOLKs0CKiEBql7JgunnQg==", + "version": "0.1.0-rc.1", + "resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.0-rc.1.tgz", + "integrity": "sha512-OTZ2l1GyWBe7NWMRrWInZVfiGKTsh9zvjhpWj43Tsox/2JFNaa5LfZBunGV5UcjvGpD5cXGDAw94cQ9TdJBAdg==", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.808.0", diff --git a/package.json b/package.json index 79869eda..976df6c0 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,9 @@ "test:contract:context:webapp": "vitest run --config vitest.config.unit.ts src/apps/browser/createLiveSyncBrowserServiceHub.unit.spec.ts", "test:contract:context:cli": "npm run build --workspace self-hosted-livesync-cli && deno task --cwd src/apps/cli/testdeno test:setup-put-cat", "test:contract:context:obsidian": "npm run build && npm run test:e2e:obsidian:smoke", + "test:e2e:cli": "npm run test:e2e:ci --workspace self-hosted-livesync-cli", + "test:e2e:cli:p2p": "npm run test:e2e:p2p --workspace self-hosted-livesync-cli", + "test:e2e:cli:all": "npm run test:e2e:all --workspace self-hosted-livesync-cli", "test:integration": "npx dotenv-cli -e .env -e .test.env -- vitest run --config vitest.config.integration.ts", "test:unit:coverage": "vitest run --config vitest.config.unit.ts --coverage", "test:install-playwright": "npx playwright install chromium", @@ -155,7 +158,7 @@ "@smithy/types": "^4.14.3", "@smithy/util-retry": "^4.4.5", "@trystero-p2p/nostr": "^0.24.0", - "@vrtmrz/livesync-commonlib": "0.1.0-rc.0", + "@vrtmrz/livesync-commonlib": "0.1.0-rc.1", "diff-match-patch": "^1.0.5", "fflate": "^0.8.2", "idb": "^8.0.3", diff --git a/src/apps/cli/commands/runCommand.ts b/src/apps/cli/commands/runCommand.ts index 919c875e..3ba9ede1 100644 --- a/src/apps/cli/commands/runCommand.ts +++ b/src/apps/cli/commands/runCommand.ts @@ -11,7 +11,10 @@ import { type EntryDoc, } from "@vrtmrz/livesync-commonlib/compat/common/types"; import { ConnectionStringParser } from "@vrtmrz/livesync-commonlib/compat/common/ConnectionString"; -import { activateRemoteConfiguration, createRemoteConfigurationId } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/remoteConfig"; +import { + activateRemoteConfiguration, + createRemoteConfigurationId, +} from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/remoteConfig"; import { stripAllPrefixes } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/path"; import type { CLICommandContext, CLIOptions } from "./types"; import { toArrayBuffer, toDatabaseRelativePath } from "./utils"; @@ -22,7 +25,7 @@ import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFu import { fsPromises as fs, path } from "@vrtmrz/livesync-commonlib/node"; import type { LiveSyncCouchDBReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator"; import type { LiveSyncJournalReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/journal/LiveSyncJournalReplicator"; -import { writeStderrLine, writeStdoutLine } from "../cliOutput"; +import { writeStderrLine, writeStdoutLine } from "@/apps/cli/cliOutput"; function redactConnectionString(uri: string): string { return uri.replace(/\/\/([^@/]+)@/u, "//***@"); diff --git a/src/apps/cli/package.json b/src/apps/cli/package.json index 734f2bb0..5806c299 100644 --- a/src/apps/cli/package.json +++ b/src/apps/cli/package.json @@ -19,22 +19,19 @@ "test:e2e:push-pull": "bash test/test-push-pull-linux.sh", "test:e2e:setup-put-cat": "bash test/test-setup-put-cat-linux.sh", "test:e2e:sync-two-local": "bash test/test-sync-two-local-databases-linux.sh", - "test:e2e:p2p": "bash test/test-p2p-three-nodes-conflict-linux.sh", - "test:e2e:p2p-upload-download-repro": "bash test/test-p2p-upload-download-repro-linux.sh", - "test:e2e:p2p-host": "bash test/test-p2p-host-linux.sh", - "test:e2e:p2p-sync": "bash test/test-p2p-sync-linux.sh", + "pretest:e2e:ci": "npm run build", + "test:e2e:ci": "deno task --cwd testdeno test:ci", + "test:e2e:p2p": "deno task --cwd testdeno test:p2p:compose", "test:e2e:mirror": "bash test/test-mirror-linux.sh", "test:e2e:remote-commands": "bash test/test-remote-commands-linux.sh", "pretest:e2e:all": "npm run build", - "test:e2e:all": " export RUN_BUILD=0 && npm run test:e2e:setup-put-cat && npm run test:e2e:push-pull && npm run test:e2e:sync-two-local && npm run test:e2e:p2p && npm run test:e2e:mirror && npm run test:e2e:two-vaults && npm run test:e2e:remote-commands", + "test:e2e:all": "deno task --cwd testdeno test:ci && deno task --cwd testdeno test:p2p:compose", "pretest:e2e:docker:all": "npm run build:docker", "test:e2e:docker:push-pull": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-push-pull-linux.sh", "test:e2e:docker:setup-put-cat": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-setup-put-cat-linux.sh", "test:e2e:docker:mirror": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-mirror-linux.sh", "test:e2e:docker:remote-commands": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-remote-commands-linux.sh", "test:e2e:docker:sync-two-local": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-sync-two-local-databases-linux.sh", - "test:e2e:docker:p2p": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-p2p-three-nodes-conflict-linux.sh", - "test:e2e:docker:p2p-sync": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-p2p-sync-linux.sh", "test:e2e:docker:all": "export RUN_BUILD=0 && npm run test:e2e:docker:setup-put-cat && npm run test:e2e:docker:push-pull && npm run test:e2e:docker:sync-two-local && npm run test:e2e:docker:mirror && npm run test:e2e:docker:remote-commands" }, "dependencies": { diff --git a/src/apps/cli/testdeno/CONTRIBUTING_TESTS.md b/src/apps/cli/testdeno/CONTRIBUTING_TESTS.md index b2448078..051cfa9c 100644 --- a/src/apps/cli/testdeno/CONTRIBUTING_TESTS.md +++ b/src/apps/cli/testdeno/CONTRIBUTING_TESTS.md @@ -107,6 +107,7 @@ Deno.test("feature: behaviour", async () => { - Re-run sync operations where the protocol is eventually consistent. - For network-sensitive commands, use `LIVESYNC_CLI_RETRY` during debugging. - Keep Docker container reuse disabled by default unless debugging. +- Use `npm run test:e2e:cli:p2p` for canonical P2P validation. It runs the Deno scenario in Compose because host networking and WebRTC candidate selection are not reproducible across environments. Individual `deno task test:p2p-*` tasks remain available when explicitly invoked for cross-platform diagnostics, but are not selected by the default suite or CI. ## Environment variables diff --git a/src/apps/cli/testdeno/deno.json b/src/apps/cli/testdeno/deno.json index 7845ba1b..fd4727d8 100644 --- a/src/apps/cli/testdeno/deno.json +++ b/src/apps/cli/testdeno/deno.json @@ -1,6 +1,8 @@ { "tasks": { - "test": "deno test --env-file=.test.env -A --no-check test-*.ts", + "test": "deno task test:ci", + "test:ci": "deno run -A --no-check run-ci-suite.ts", + "test:p2p:compose": "deno run -A --no-check run-compose-p2p.ts", "test:local": "deno test --env-file=.test.env -A --no-check test-setup-put-cat.ts test-mirror.ts test-daemon.ts", "test:daemon": "deno test --env-file=.test.env -A --no-check test-daemon.ts", "test:decoupled-vault": "deno test --env-file=.test.env -A --no-check test-decoupled-vault.ts", diff --git a/src/apps/cli/testdeno/run-ci-suite.ts b/src/apps/cli/testdeno/run-ci-suite.ts new file mode 100644 index 00000000..7b5824ca --- /dev/null +++ b/src/apps/cli/testdeno/run-ci-suite.ts @@ -0,0 +1,32 @@ +const TASKS = [ + "test:setup-put-cat", + "test:mirror", + "test:daemon", + "test:push-pull", + "test:decoupled-vault", + "test:sync-two-local", + "test:sync-locked-remote", + "test:remote-commands", + "test:e2e-matrix:couchdb-enc0", + "test:e2e-matrix:couchdb-enc1", + "test:e2e-matrix:minio-enc0", + "test:e2e-matrix:minio-enc1", +] as const; + +for (const [index, task] of TASKS.entries()) { + console.log(`\n[CLI E2E ${index + 1}/${TASKS.length}] ${task}`); + const child = new Deno.Command(Deno.execPath(), { + args: ["task", task], + cwd: import.meta.dirname, + stdin: "inherit", + stdout: "inherit", + stderr: "inherit", + }).spawn(); + const status = await child.status; + if (!status.success) { + console.error(`[CLI E2E] ${task} failed with exit code ${status.code}.`); + Deno.exit(status.code); + } +} + +console.log(`\n[CLI E2E] CI suite passed (${TASKS.length} tasks).`); diff --git a/src/apps/cli/testdeno/run-compose-p2p.ts b/src/apps/cli/testdeno/run-compose-p2p.ts new file mode 100644 index 00000000..da08939a --- /dev/null +++ b/src/apps/cli/testdeno/run-compose-p2p.ts @@ -0,0 +1,48 @@ +const repositoryRoot = await Deno.realPath(new URL("../../../../", import.meta.url)); +const composeArgs = ["compose", "-f", "test/bench-network/compose.yml"]; +const p2pEnvironment = { + CLI_E2E_TASK: Deno.env.get("CLI_E2E_TASK") ?? "test:p2p-sync", + RELAY: Deno.env.get("RELAY") ?? "ws://nostr-relay:7777/", + PEERS_TIMEOUT: Deno.env.get("PEERS_TIMEOUT") ?? "20", + SYNC_TIMEOUT: Deno.env.get("SYNC_TIMEOUT") ?? "60", + LIVESYNC_USE_COTURN: Deno.env.get("LIVESYNC_USE_COTURN") ?? "0", + TURN_SERVERS: Deno.env.get("TURN_SERVERS") ?? "none", + LIVESYNC_P2P_PEERS_RETRY: Deno.env.get("LIVESYNC_P2P_PEERS_RETRY") ?? "1", + LIVESYNC_P2P_RELAY_READY_TIMEOUT_MS: Deno.env.get("LIVESYNC_P2P_RELAY_READY_TIMEOUT_MS") ?? "60000", + BENCH_LIVESYNC_TEST_TEE: Deno.env.get("BENCH_LIVESYNC_TEST_TEE") ?? "0", +}; + +async function runDocker(args: string[], env?: Record): Promise { + return await new Deno.Command("docker", { + args, + cwd: repositoryRoot, + env, + stdin: "inherit", + stdout: "inherit", + stderr: "inherit", + }).spawn().status; +} + +let testStatus: Deno.CommandStatus | undefined; +try { + testStatus = await runDocker( + [...composeArgs, "run", "--build", "--rm", "bench-runner", "run-livesync-cli-e2e"], + p2pEnvironment + ); +} finally { + const cleanupStatus = await runDocker([...composeArgs, "down", "-v", "--remove-orphans"]); + if (!cleanupStatus.success) { + console.error(`[CLI E2E] Compose cleanup failed with exit code ${cleanupStatus.code}.`); + if (testStatus?.success) { + Deno.exit(cleanupStatus.code); + } + } +} + +if (!testStatus?.success) { + const code = testStatus?.code ?? 1; + console.error(`[CLI E2E] Compose P2P suite failed with exit code ${code}.`); + Deno.exit(code); +} + +console.log("\n[CLI E2E] Compose P2P suite passed."); diff --git a/src/apps/cli/testdeno/test_dev_deno.md b/src/apps/cli/testdeno/test_dev_deno.md index 8926b9c1..da809ce2 100644 --- a/src/apps/cli/testdeno/test_dev_deno.md +++ b/src/apps/cli/testdeno/test_dev_deno.md @@ -1,13 +1,13 @@ # CLI Deno Test Development Notes This document provides an overview of the Deno-based compatibility tests under `src/apps/cli/testdeno/`. -The existing bash tests under `src/apps/cli/test/` are preserved, while a Windows-friendly suite is maintained in parallel. +The Deno suite is the canonical CLI E2E entry point. P2P scenarios run through the repository Compose entry point so that networking, signalling, and the runner environment are reproducible. Existing Bash tests under `src/apps/cli/test/` remain as legacy implementation references, but are not exposed as supported P2P entry points. --- ## Goals -- Keep existing bash tests intact. +- Keep the existing Bash tests as migration references while using Deno and Compose for supported execution. - Provide direct execution from Windows PowerShell. - Establish a TypeScript (Deno) foundation for core end-to-end and integration scenarios. @@ -18,6 +18,8 @@ The existing bash tests under `src/apps/cli/test/` are preserved, while a Window ``` src/apps/cli/testdeno/ deno.json + run-ci-suite.ts + run-compose-p2p.ts CONTRIBUTING_TESTS.md helpers/ backgroundCli.ts @@ -56,6 +58,8 @@ src/apps/cli/testdeno/ Main tasks: - `deno task test` +- `deno task test:ci` +- `deno task test:p2p:compose` - `deno task test:local` - `deno task test:daemon` - `deno task test:decoupled-vault` @@ -73,6 +77,8 @@ Main tasks: - `deno task test:e2e-couchdb` - `deno task test:e2e-matrix` +`deno task test` is an alias for the non-P2P `test:ci` suite. The individual P2P tasks are explicit host-direct entry points for cross-platform diagnostics; they are never selected by the default suite or CI. Use `test:p2p:compose` for canonical P2P verification. + ### `helpers/cli.ts` - CLI execution wrappers. @@ -206,11 +212,23 @@ Both CouchDB and P2P relay flows are bash-independent. ## Running tests (PowerShell) +From the repository root, use the canonical package scripts. `test:e2e:cli` runs the same non-P2P task set selected by the default CLI CI workflow. P2P validation runs in Compose so peer discovery does not depend on host loopback, firewall, or WebRTC candidate behaviour. + +```powershell +npm run test:e2e:cli +npm run test:e2e:cli:p2p +npm run test:e2e:cli:all +``` + From `src/apps/cli/testdeno`: ```powershell cd src/apps/cli/testdeno +# Canonical suites +deno task test:ci +deno task test:p2p:compose + # Local-only set deno task test:local @@ -227,7 +245,8 @@ deno task test:decoupled-vault deno task test:remote-commands deno task test:e2e-couchdb -# P2P-based tests +# Explicit host-direct P2P diagnostics for cross-platform investigations. +# These are not part of the default suite or release evidence. deno task test:p2p-host deno task test:p2p-peers deno task test:p2p-sync diff --git a/src/apps/webapp/obsidianMockExports.unit.spec.ts b/src/apps/webapp/obsidianMockExports.unit.spec.ts new file mode 100644 index 00000000..cf0e7407 --- /dev/null +++ b/src/apps/webapp/obsidianMockExports.unit.spec.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "vitest"; +import { fileURLToPath, path } from "@vrtmrz/livesync-commonlib/node"; +import * as ts from "typescript"; + +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../.."); +const dependencyFacadePath = path.resolve(repositoryRoot, "src/deps.ts"); +const obsidianMockPath = path.resolve(repositoryRoot, "test/harness/obsidian-mock.ts"); + +describe("Webapp Obsidian mock exports", () => { + it("provides every value re-exported from Obsidian", () => { + const program = ts.createProgram([dependencyFacadePath, obsidianMockPath], { + module: ts.ModuleKind.ESNext, + moduleResolution: ts.ModuleResolutionKind.Bundler, + skipLibCheck: true, + target: ts.ScriptTarget.ESNext, + }); + const dependencyFacade = program.getSourceFile(dependencyFacadePath); + const obsidianMock = program.getSourceFile(obsidianMockPath); + expect(dependencyFacade).toBeDefined(); + expect(obsidianMock).toBeDefined(); + + const requiredExports = new Set(); + for (const statement of dependencyFacade!.statements) { + if ( + ts.isExportDeclaration(statement) && + statement.moduleSpecifier && + ts.isStringLiteral(statement.moduleSpecifier) && + statement.moduleSpecifier.text === "obsidian" && + statement.exportClause && + ts.isNamedExports(statement.exportClause) && + !statement.isTypeOnly + ) { + for (const element of statement.exportClause.elements) { + if (!element.isTypeOnly) { + requiredExports.add((element.propertyName ?? element.name).text); + } + } + } + } + + const checker = program.getTypeChecker(); + const mockSymbol = checker.getSymbolAtLocation(obsidianMock!); + expect(mockSymbol).toBeDefined(); + const availableExports = new Set(checker.getExportsOfModule(mockSymbol!).map((symbol) => symbol.name)); + const missingExports = [...requiredExports].filter((name) => !availableExports.has(name)).sort(); + + expect(missingExports).toEqual([]); + }); +}); diff --git a/test/harness/obsidian-mock.ts b/test/harness/obsidian-mock.ts index a07f9025..262d45f3 100644 --- a/test/harness/obsidian-mock.ts +++ b/test/harness/obsidian-mock.ts @@ -980,6 +980,10 @@ export function parseYaml(s: string) { export function getLanguage() { return "en"; } +/** The mock does not implement APIs gated by an Obsidian application version. */ +export function requireApiVersion(_version: string): boolean { + return false; +} export function setIcon(el: HTMLElement, icon: string) {} export function arrayBufferToBase64(buffer: ArrayBuffer): string { return btoa(String.fromCharCode(...new Uint8Array(buffer)));