diff --git a/.github/workflows/cli-docker.yml b/.github/workflows/cli-docker.yml index b651ccd3..54eaa5fd 100644 --- a/.github/workflows/cli-docker.yml +++ b/.github/workflows/cli-docker.yml @@ -23,7 +23,6 @@ on: - "src/apps/webpeer/**" - ".github/workflows/release.yml" - ".github/workflows/unit-ci.yml" - - ".github/workflows/harness-ci.yml" workflow_dispatch: inputs: dry_run: diff --git a/.github/workflows/harness-ci.yml b/.github/workflows/harness-ci.yml deleted file mode 100644 index bf60e281..00000000 --- a/.github/workflows/harness-ci.yml +++ /dev/null @@ -1,66 +0,0 @@ -# Run tests by Harnessed CI -name: harness-ci - -on: - workflow_dispatch: - inputs: - testsuite: - description: 'Run specific test suite (leave empty to run all)' - type: choice - options: - - '' - - 'suite/' - - 'suitep2p/' - default: '' - -permissions: - contents: read - -jobs: - test: - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '24.x' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Install test dependencies (Playwright Chromium) - run: npm run test:install-dependencies - - - name: Start test services (CouchDB) - run: npm run test:docker-couchdb:start - if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suite/' }} - - name: Start test services (MinIO) - run: npm run test:docker-s3:start - if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suite/' }} - - name: Start test services (Nostr Relay + WebPeer) - run: npm run test:docker-p2p:start - if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suitep2p/' }} - - name: Run tests suite - if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suite/' }} - env: - CI: true - run: npm run test suite/ - - name: Run P2P tests suite - if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suitep2p/' }} - env: - CI: true - run: npm run test:p2p - - name: Stop test services (CouchDB) - run: npm run test:docker-couchdb:stop - if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suite/' }} - - name: Stop test services (MinIO) - run: npm run test:docker-s3:stop - if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suite/' }} - - name: Stop test services (Nostr Relay + WebPeer) - run: npm run test:docker-p2p:stop - if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suitep2p/' }} diff --git a/AGENTS.md b/AGENTS.md index 47f71d70..5148dabd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,7 +62,8 @@ Before submitting code, you should run verification scripts locally to ensure co - Run `npm run check` to perform code verification. This runs type-checking (`tsc-check`), ESLint (`lint`), and Svelte checks (`svelte-check`). 2. **Unit Tests**: - Run `npm run test:unit` to execute fast local unit tests. - - Run `npm run test` or `npm run test:full` for full testing suites (including dockerised services). + - Run `npm run test:unit:coverage` when unit-test coverage is required. + - Run focused integration, CLI E2E, or real Obsidian E2E commands for the boundary being changed. Start only the Docker services required by that command. 3. **Build**: - Run `npm run build` to compile the production bundle (`main.js`). - Run `npm run dev` for the development watch/build task. diff --git a/devs.md b/devs.md index b8cae7cb..80b2da28 100644 --- a/devs.md +++ b/devs.md @@ -35,13 +35,14 @@ npm run check # TypeScript and svelte type checking npm run dev # Development build with auto-rebuild (uses .env for test vault paths) npm run build # Production build npm run buildDev # Development build (one-time) -npm run test:unit # Run unit tests only (no Docker services required) -npm test # Run Harness based vitest tests (requires Docker services), not recommended, unstable. +npm run test:integration # Run CouchDB-backed integration tests +npm run test:e2e:cli:p2p # Run canonical P2P validation in Compose +npm run test:e2e:obsidian:local-suite # Run the real Obsidian local suite ``` ### Tips -Use CLI E2E tests or real Obsidian E2E scripts instead of `npm test` when the behaviour can be verified outside the browser harness. +Select the narrowest unit, integration, CLI E2E, or real Obsidian E2E command that owns the behaviour being changed. The obsolete mocked browser Harness has been retired. ### Unreleased change notes @@ -64,16 +65,15 @@ To facilitate development and testing, the build process can automatically copy - **Unit Tests** (`vitest.config.unit.ts`): Unit tests run in Node.js (excluding harnesses and integration tests). Unit tests should be `*.unit.spec.ts` and placed alongside the implementation file (e.g., `ChunkFetcher.unit.spec.ts`). Executed via `npm run test:unit`. - **Integration Tests** (`vitest.config.integration.ts`): Tests run in Node.js against a real CouchDB instance. Integration tests should be `*.integration.spec.ts` or `*.integration.test.ts` and placed alongside the implementation file (e.g., `StreamingFetch.integration.spec.ts`). Executed via `npm run test:integration`. - If you add a feature that interacts with the remote database (e.g., replication changes, custom changes feed parameters, or custom HTTP queries), you strongly expected to write an integration test to verify the behaviour against a real CouchDB server. - - **Browser Harness Tests** (`vitest.config.ts`): Transitional browser-based harness tests using Playwright/Chromium. Executed via `npm run test`. This layer is no longer the preferred destination for new broad E2E coverage because `test/harness` can drift from real Obsidian behaviour. - - **P2P Tests** (`vitest.config.p2p.ts`): Browser-based Peer-to-Peer replication tests. Executed via `npm run test:p2p`. - **Commonlib Tests**: Commonlib owns unit and package tests for shared RPC, storage, replication, and platform contracts. LiveSync CI verifies the exact packed dependency as a downstream consumer. +- **CLI E2E** (`src/apps/cli/testdeno/`): Host-independent consumer workflows, including the canonical Compose-based P2P scenarios. Run `npm run test:e2e:cli` for the ordinary suite or `npm run test:e2e:cli:p2p` for P2P validation. - **Real Obsidian E2E** (`test/e2e-obsidian/`): Local-first scripts that launch real Obsidian with temporary vaults and the built Self-hosted LiveSync plug-in. Use these for boot-up sequence, vault reflection, RedFlag flows, Fast Setup (Simple Fetch), settings dialogues, restart-sensitive workflows, Object Storage regressions, and other behaviour that depends on Obsidian itself. Run focused scripts such as `npm run test:e2e:obsidian:two-vault-sync`, or use `npm run test:e2e:obsidian:local-suite:services` to run the broader local suite with CouchDB and MinIO fixtures managed by the wrapper. -- **Docker Services**: Tests require CouchDB, MinIO (S3), and P2P services: +- **Docker Services**: Service-backed tests use CouchDB and MinIO (S3). Canonical P2P validation owns its relay through the CLI Compose runner: ```bash npm run test:docker-all:start # Start all test services - npm run test:full # Run tests with coverage + npm run test:integration # Run the relevant service-backed suite npm run test:docker-all:stop # Stop services ``` @@ -82,9 +82,9 @@ To facilitate development and testing, the build process can automatically copy - **Test Structure**: - `test/e2e-obsidian/` - Real Obsidian E2E scripts for local verification - - `test/suite/` - Transitional browser harness tests for sync operations - - `test/unit/` - Unit tests (via vitest, as harness is browser-based) - - `test/harness/` - Transitional mock implementations (e.g., `obsidian-mock.ts`). Avoid adding broad new E2E coverage here unless it is explicitly a compatibility bridge. + - co-located `*.unit.spec.ts` files - Node-based unit tests + - co-located `*.integration.spec.ts` files - service-backed integration tests + - `src/apps/webapp/obsidianMock.ts` - Webapp-only Obsidian compatibility adapter; it is not an E2E Harness ### Import Path Normalisation diff --git a/docs/adr/2026_06_real_obsidian_e2e.md b/docs/adr/2026_06_real_obsidian_e2e.md index 863b7568..f65c8b9d 100644 --- a/docs/adr/2026_06_real_obsidian_e2e.md +++ b/docs/adr/2026_06_real_obsidian_e2e.md @@ -2,15 +2,15 @@ ## Status -Proposed / Spike Implemented +Accepted / Implemented ## Release -Not yet. Planned after the serviceFeature refactoring branch is reviewed. +Not yet. Included in the 1.0.0 release preparation. ## Context -The current end-to-end tests run through Vitest browser mode and a mocked Obsidian environment in `test/harness`. This has been useful for exercising synchronisation flows without launching Obsidian, but it is no longer a reliable final signal for plug-in behaviour. +When this decision was proposed, the end-to-end tests ran through Vitest browser mode and a mocked Obsidian environment in `test/harness`. This was useful for exercising synchronisation flows without launching Obsidian, but it was no longer a reliable final signal for plug-in behaviour. The main issues are: @@ -31,7 +31,7 @@ The long-term test pyramid should be: 2. Integration tests for CouchDB, Object Storage, P2P services, database operations, and replication protocols. 3. Real Obsidian E2E tests for boot-up sequence, vault reflection, command registration, settings dialogues, restart scheduling, and user-visible workflows. -The existing `test/harness` should be demoted to a transitional compatibility layer. It may remain temporarily while the real Obsidian runner reaches parity for critical flows, but new high-level E2E coverage should target the real runner. +Retain the existing browser Harness only as a transitional compatibility layer while the real Obsidian runner reaches parity for critical flows, then remove it. New high-level E2E coverage targets the real runner. ## Non-Goals @@ -155,7 +155,7 @@ Current verification: Known limits: - The smoke runner currently proves only one-vault launch and plug-in load readiness. Broader workflows are covered by separate real Obsidian scripts, including CouchDB upload, startup scan, two-vault note synchronisation, Hidden File Sync, Customisation Sync, and setting Markdown export. -- Cross-platform support is still discovery-level. The working path has been validated on Linux ARM64. macOS and Windows should be validated in their own environments as follow-up work. +- The working path has been validated on Linux ARM64 and on macOS with Obsidian 1.12.7. Windows remains unverified. - CI wiring is intentionally not implemented. The runner depends on a licensed desktop application and is treated as a local verification tool. ### Phase 2: First Real Workflow @@ -199,9 +199,10 @@ Current implementation status: Current implementation status: -- `test/harness` is now documented as a transitional compatibility layer. -- New broad E2E work should target `test/e2e-obsidian/` when real Obsidian behaviour is the risk being tested. -- The next high-value scenarios are RedFlag variants and Fast Setup (Simple Fetch) variants, not a line-by-line migration of `test/suite`. +- The mocked Vitest browser suites, their P2P runner, their root-level relay helpers, and the manual `harness-ci` workflow have been removed after maintained suites covered the critical flows. +- CouchDB and Object Storage combinations, with and without encryption, are owned by the CLI two-Vault matrix. P2P validation is owned by the CLI Compose E2E suite. Plug-in lifecycle and vault integration are owned by real Obsidian E2E. +- The Obsidian compatibility implementation still needed by the Webapp has moved to `src/apps/webapp/obsidianMock.ts`; it is not a retained browser E2E Harness. +- Remaining high-value scenarios, including RedFlag and Fast Setup (Simple Fetch) variants, should be added according to their owning integration boundary rather than copied line by line from the retired suite. ## Local Verification Strategy @@ -240,12 +241,12 @@ Real Obsidian E2E is a local verification layer. It should not be wired into the 4. Add `test:e2e:obsidian:smoke` for one-vault plug-in load. 5. Document required local environment variables, especially `OBSIDIAN_BINARY`. 6. Port one CouchDB-backed workflow after the smoke test is stable. -7. Mark `test/harness` as transitional and block new broad E2E work from targeting it. +7. Retire the browser Harness after critical workflows have replacement coverage. 8. Add the local suite script for broader local verification. ## Consequences - Real Obsidian E2E becomes the source of truth for plug-in lifecycle and vault integration. - Unit and integration tests remain the primary fast feedback loops. -- The old browser harness can be deleted once the new runner covers the critical workflows. +- The old browser Harness has been deleted now that the replacement coverage owns its critical workflows. - The project will gain slower but higher-confidence tests for the behaviours most likely to differ between mocks and Obsidian itself. diff --git a/package-lock.json b/package-lock.json index 3f206aad..985ac9eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,8 +56,6 @@ "@types/pouchdb-replication": "^6.4.7", "@types/transform-pouch": "^1.0.6", "@typescript-eslint/parser": "8.56.1", - "@vitest/browser": "^4.1.8", - "@vitest/browser-playwright": "^4.1.8", "@vitest/coverage-v8": "^4.1.8", "@vrtmrz/obsidian-test-session": "0.2.2", "dotenv-cli": "^11.0.0", @@ -1261,13 +1259,6 @@ "node": ">=18" } }, - "node_modules/@blazediff/core": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@blazediff/core/-/core-1.9.1.tgz", - "integrity": "sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==", - "dev": true, - "license": "MIT" - }, "node_modules/@codemirror/state": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz", @@ -2757,13 +2748,6 @@ "node": ">=18" } }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true, - "license": "MIT" - }, "node_modules/@promptbook/utils": { "version": "0.69.5", "resolved": "https://registry.npmjs.org/@promptbook/utils/-/utils-0.69.5.tgz", @@ -4768,54 +4752,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@vitest/browser": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.8.tgz", - "integrity": "sha512-u21VzX07HzlJYpFgkxmjEXar/tG2UqWGgyGG/46SrrPc7rSdCTPw5vuowopO9CIqF8UCUQzDFdbVnNpw6N0BfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@blazediff/core": "1.9.1", - "@vitest/mocker": "4.1.8", - "@vitest/utils": "4.1.8", - "magic-string": "^0.30.21", - "pngjs": "^7.0.0", - "sirv": "^3.0.2", - "tinyrainbow": "^3.1.0", - "ws": "^8.19.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "vitest": "4.1.8" - } - }, - "node_modules/@vitest/browser-playwright": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.8.tgz", - "integrity": "sha512-SR7FqgegaexEg73xvf3ArtygXegagMdXnL0EZMpxrWvvhQxvicD/E8p0ib0J91riPRtQUViyh67Xjw3NqvyhVg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@vitest/browser": "4.1.8", - "@vitest/mocker": "4.1.8", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "playwright": "*", - "vitest": "4.1.8" - }, - "peerDependenciesMeta": { - "playwright": { - "optional": false - } - } - }, "node_modules/@vitest/coverage-v8": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.8.tgz", @@ -11480,16 +11416,6 @@ "node": ">=4" } }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -12307,16 +12233,6 @@ "node": ">=18" } }, - "node_modules/pngjs": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", - "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.19.0" - } - }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -13603,21 +13519,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/sirv": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", - "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -14531,16 +14432,6 @@ "url": "https://github.com/sponsors/ota-meshi" } }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/tough-cookie": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", diff --git a/package.json b/package.json index f8c69ec4..7d5f2f45 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "check:compatibility": "node utils/check-compatibility.js --file main.js --ios 15", "check": "npm run tsc-check && npm run tsc-check:apps && npm run lint && npm run lint:community -- --quiet && npm run svelte-check && npm run check:compatibility", "unittest": "deno test -A --no-check --coverage=cov_profile --v8-flags=--expose-gc --trace-leaks ./src/", - "test": "vitest run", "test:unit": "vitest run --config vitest.config.unit.ts", "test:contract:contexts": "vitest run --config vitest.config.unit.ts src/modules/services/ObsidianServiceContext.unit.spec.ts src/apps/cli/services/NodeServiceContext.unit.spec.ts src/apps/browser/createLiveSyncBrowserServiceHub.unit.spec.ts", "test:contract:context:webapp": "vitest run --config vitest.config.unit.ts src/apps/browser/createLiveSyncBrowserServiceHub.unit.spec.ts", @@ -35,8 +34,6 @@ "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", - "test:install-dependencies": "npm run test:install-playwright", "test:e2e:obsidian:install-appimage": "tsx test/e2e-obsidian/scripts/install-appimage.ts", "test:e2e:obsidian:runner": "vitest run --config vitest.config.e2e-runner.ts", "test:e2e:obsidian:discover": "tsx test/e2e-obsidian/scripts/discover.ts", @@ -58,7 +55,6 @@ "test:e2e:obsidian:setting-markdown-export": "tsx test/e2e-obsidian/scripts/setting-markdown-export.ts", "test:e2e:obsidian:local-suite": "tsx test/e2e-obsidian/scripts/local-suite.ts", "test:e2e:obsidian:local-suite:services": "tsx test/e2e-obsidian/scripts/local-suite.ts --manage-services", - "test:coverage": "vitest run --coverage", "test:docker-couchdb:up": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/couchdb-start.sh", "test:docker-couchdb:init": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/couchdb-init.sh", "test:docker-couchdb:start": "npm run test:docker-couchdb:up && sleep 5 && npm run test:docker-couchdb:init", @@ -69,18 +65,11 @@ "test:docker-s3:start": "npm run test:docker-s3:up && sleep 3 && npm run test:docker-s3:init", "test:docker-s3:down": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/minio-stop.sh", "test:docker-s3:stop": "npm run test:docker-s3:down", - "test:docker-p2p:up": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/p2p-start.sh", - "test:docker-p2p:init": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/p2p-init.sh", - "test:docker-p2p:start": "npm run test:docker-p2p:up && sleep 3 && npm run test:docker-p2p:init", - "test:docker-p2p:down": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/p2p-stop.sh", - "test:docker-p2p:stop": "npm run test:docker-p2p:down", - "test:docker-all:up": "npm run test:docker-couchdb:up ; npm run test:docker-s3:up ; npm run test:docker-p2p:up", - "test:docker-all:init": "npm run test:docker-couchdb:init ; npm run test:docker-s3:init ; npm run test:docker-p2p:init", - "test:docker-all:down": "npm run test:docker-couchdb:down ; npm run test:docker-s3:down ; npm run test:docker-p2p:down", + "test:docker-all:up": "npm run test:docker-couchdb:up ; npm run test:docker-s3:up", + "test:docker-all:init": "npm run test:docker-couchdb:init ; npm run test:docker-s3:init", + "test:docker-all:down": "npm run test:docker-couchdb:down ; npm run test:docker-s3:down", "test:docker-all:start": "npm run test:docker-all:up && sleep 5 && npm run test:docker-all:init", "test:docker-all:stop": "npm run test:docker-all:down", - "test:full": "npm run test:docker-all:start && vitest run --coverage && npm run test:docker-all:stop", - "test:p2p": "bash test/suitep2p/run-p2p-tests.sh", "update-workspaces": "node update-workspaces.mjs", "version": "node version-bump.mjs && node update-workspaces.mjs && npm run pretty:json && git add manifest.json versions.json src/apps/cli/package.json src/apps/webpeer/package.json src/apps/webapp/package.json" }, @@ -109,8 +98,6 @@ "@types/pouchdb-replication": "^6.4.7", "@types/transform-pouch": "^1.0.6", "@typescript-eslint/parser": "8.56.1", - "@vitest/browser": "^4.1.8", - "@vitest/browser-playwright": "^4.1.8", "@vitest/coverage-v8": "^4.1.8", "@vrtmrz/obsidian-test-session": "0.2.2", "dotenv-cli": "^11.0.0", diff --git a/test/harness/obsidian-mock.ts b/src/apps/webapp/obsidianMock.ts similarity index 96% rename from test/harness/obsidian-mock.ts rename to src/apps/webapp/obsidianMock.ts index 262d45f3..e494d2a1 100644 --- a/test/harness/obsidian-mock.ts +++ b/src/apps/webapp/obsidianMock.ts @@ -1,12 +1,16 @@ -/* eslint-disable @typescript-eslint/no-unsafe-function-type */ +/** + * Legacy Obsidian API compatibility implementation used only by the Webapp build. + * + * This file moved from the retired browser test Harness to make its current ownership explicit. It is not a faithful + * Obsidian mock, and must not become a general test environment for the plug-in. When the Webapp compatibility boundary + * is redesigned, replace this implementation here rather than extending it as a shared Obsidian simulation. + */ export const SettingCache = new Map(); //@ts-ignore obsidian global globalThis.activeDocument = document; declare const hostPlatform: string | undefined; -// import { interceptFetchForLogging } from "../harness/utils/intercept"; -// interceptFetchForLogging(); globalThis.process = { platform: (hostPlatform || "win32") as any, } as any; @@ -110,7 +114,7 @@ export class Vault { private files: Map = new Map(); private contents: Map = new Map(); private root: TFolder; - private listeners: Map> = new Map(); + private listeners: Map any>> = new Map(); constructor(vaultName?: string) { if (vaultName) { @@ -392,10 +396,10 @@ class Events { } class Workspace extends Events { - getActiveFile() { + getActiveFile(): null { return null; } - getMostRecentLeaf() { + getMostRecentLeaf(): null { return null; } @@ -409,7 +413,7 @@ class Workspace extends Events { }, 200); // }); } - getLeavesOfType() { + getLeavesOfType(): never[] { return []; } getLeaf() { @@ -432,7 +436,7 @@ export class App { workspace: Workspace = new Workspace(); metadataCache: any = { on: (name: string, cb: any, ctx?: any) => {}, - getFileCache: () => null, + getFileCache: (): null => null, }; } @@ -894,6 +898,7 @@ export class FuzzySuggestModal { } export class MarkdownRenderer { static render(app: App, md: string, el: HTMLElement, path: string, component: Component) { + // eslint-disable-next-line no-unsanitized/property -- This compatibility method mirrors Obsidian's trusted Markdown renderer boundary. el.innerHTML = md; return Promise.resolve(); } @@ -905,6 +910,7 @@ export class WorkspaceLeaf {} export function sanitizeHTMLToDom(html: string) { const div = document.createElement("div"); + // eslint-disable-next-line no-unsanitized/property -- This compatibility method mirrors Obsidian's sanitised-HTML API contract. div.innerHTML = html; return div; } diff --git a/src/apps/webapp/obsidianMockExports.unit.spec.ts b/src/apps/webapp/obsidianMockExports.unit.spec.ts index c074763c..d2221c79 100644 --- a/src/apps/webapp/obsidianMockExports.unit.spec.ts +++ b/src/apps/webapp/obsidianMockExports.unit.spec.ts @@ -4,7 +4,7 @@ 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"); +const obsidianMockPath = path.resolve(repositoryRoot, "src/apps/webapp/obsidianMock.ts"); function parseSourceFile(filePath: string): ts.SourceFile { const source = ts.sys.readFile(filePath); diff --git a/src/apps/webapp/vite.config.ts b/src/apps/webapp/vite.config.ts index ca7173bd..2eddc644 100644 --- a/src/apps/webapp/vite.config.ts +++ b/src/apps/webapp/vite.config.ts @@ -38,7 +38,7 @@ export default defineConfig({ resolve: { alias: { "@": path.resolve(__dirname, "../../"), - obsidian: path.resolve(__dirname, "../../../test/harness/obsidian-mock.ts"), + obsidian: path.resolve(__dirname, "./obsidianMock.ts"), }, }, base: "./", diff --git a/test/harness/harness.ts b/test/harness/harness.ts deleted file mode 100644 index 732493f3..00000000 --- a/test/harness/harness.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { App } from "@/deps.ts"; -import ObsidianLiveSyncPlugin from "@/main"; -import { DEFAULT_SETTINGS, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types"; -import { LOG_LEVEL_VERBOSE, setGlobalLogFunction } from "@vrtmrz/livesync-commonlib/compat/common/logger"; -import { SettingCache } from "./obsidian-mock"; -import { delay, fireAndForget, promiseWithResolvers } from "octagonal-wheels/promises"; -import { EVENT_PLATFORM_UNLOADED } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents"; -import { EVENT_LAYOUT_READY, eventHub } from "@/common/events"; - -import { env } from "../suite/variables"; - -export type LiveSyncHarness = { - app: App; - plugin: ObsidianLiveSyncPlugin; - dispose: () => Promise; - disposalPromise: Promise; - isDisposed: () => boolean; -}; -const isLiveSyncLogEnabled = env?.PRINT_LIVESYNC_LOGS === "true"; -function overrideLogFunction(vaultName: string) { - setGlobalLogFunction((msg, level, key) => { - if (!isLiveSyncLogEnabled) { - return; - } - if (level && level < LOG_LEVEL_VERBOSE) { - return; - } - if (msg instanceof Error) { - console.error(msg.stack); - } else { - console.log( - `[${vaultName}] :: [${key ?? "Global"}][${level ?? 1}]: ${msg instanceof Error ? msg.stack : msg}` - ); - } - }); -} - -export async function generateHarness( - paramVaultName?: string, - settings?: Partial -): Promise { - // return await serialized("harness-generation-lock", async () => { - // Dispose previous harness to avoid multiple harness running at the same time - // if (previousHarness && !previousHarness.isDisposed()) { - // console.log(`Previous harness detected, waiting for disposal...`); - // await previousHarness.disposalPromise; - // previousHarness = null; - // await delay(100); - // } - const vaultName = paramVaultName ?? "TestVault" + Date.now(); - const setting = { - ...DEFAULT_SETTINGS, - ...settings, - }; - overrideLogFunction(vaultName); - //@ts-ignore Mocked in harness - const app = new App(vaultName); - // setting and vault name - SettingCache.set(app, setting); - SettingCache.set(app.vault, vaultName); - - //@ts-ignore - const manifest_version = `${MANIFEST_VERSION || "0.0.0-harness"}`; - overrideLogFunction(vaultName); - const manifest = { - id: "obsidian-livesync", - name: "Self-hosted LiveSync (Harnessed)", - version: manifest_version, - minAppVersion: "0.15.0", - description: "Testing", - author: "vrtmrz", - authorUrl: "", - isDesktopOnly: false, - }; - - const plugin = new ObsidianLiveSyncPlugin(app, manifest); - overrideLogFunction(vaultName); - // Initial load - await delay(100); - await plugin.onload(); - let isDisposed = false; - const waitPromise = promiseWithResolvers(); - eventHub.once(EVENT_PLATFORM_UNLOADED, () => { - fireAndForget(async () => { - console.log(`Harness for vault '${vaultName}' disposed.`); - await delay(100); - eventHub.offAll(); - isDisposed = true; - waitPromise.resolve(); - }); - }); - eventHub.once(EVENT_LAYOUT_READY, () => { - plugin.app.vault.trigger("layout-ready"); - }); - const harness: LiveSyncHarness = { - app, - plugin, - dispose: async () => { - await plugin.onunload(); - return waitPromise.promise; - }, - disposalPromise: waitPromise.promise, - isDisposed: () => isDisposed, - }; - await delay(100); - console.log(`Harness for vault '${vaultName}' is ready.`); - // previousHarness = harness; - return harness; -} -export async function waitForReady(harness: LiveSyncHarness): Promise { - for (let i = 0; i < 10; i++) { - if (harness.plugin.core.services.appLifecycle.isReady()) { - console.log("App Lifecycle is ready"); - return; - } - await delay(100); - } - throw new Error(`Initialisation Timed out!`); -} - -export async function waitForIdle(harness: LiveSyncHarness): Promise { - for (let i = 0; i < 20; i++) { - await delay(25); - const processing = - harness.plugin.core.services.replication.databaseQueueCount.value + - harness.plugin.core.services.fileProcessing.totalQueued.value + - harness.plugin.core.services.fileProcessing.batched.value + - harness.plugin.core.services.fileProcessing.processing.value + - harness.plugin.core.services.replication.storageApplyingCount.value; - - if (processing === 0) { - if (i > 0) { - console.log(`Idle after ${i} loops`); - } - return; - } - } -} -export async function waitForClosed(harness: LiveSyncHarness): Promise { - await delay(100); - for (let i = 0; i < 10; i++) { - if (harness.plugin.core.services.control.hasUnloaded()) { - console.log("App has unloaded"); - return; - } - await delay(100); - } -} diff --git a/test/harness/utils/intercept.ts b/test/harness/utils/intercept.ts deleted file mode 100644 index 098a924e..00000000 --- a/test/harness/utils/intercept.ts +++ /dev/null @@ -1,51 +0,0 @@ -export function interceptFetchForLogging() { - const originalFetch = globalThis.fetch; - globalThis.fetch = async (...params: any[]) => { - const paramObj = params[0]; - const initObj = params[1]; - const url = typeof paramObj === "string" ? paramObj : paramObj.url; - const method = initObj?.method || "GET"; - const headers = initObj?.headers || {}; - const body = initObj?.body || null; - const headersObj: Record = {}; - if (headers instanceof Headers) { - headers.forEach((value, key) => { - headersObj[key] = value; - }); - } - console.dir({ - mockedFetch: { - url, - method, - headers: headersObj, - }, - }); - try { - const res = await originalFetch.apply(globalThis, params as any); - console.log(`[Obsidian Mock] Fetch response: ${res.status} ${res.statusText} for ${method} ${url}`); - const resClone = res.clone(); - const contentType = resClone.headers.get("content-type") || ""; - const isJson = contentType.includes("application/json"); - if (isJson) { - const data = await resClone.json(); - console.dir({ mockedFetchResponseJson: data }); - } else { - const ab = await resClone.arrayBuffer(); - const text = new TextDecoder().decode(ab); - const isText = /^text\//.test(contentType); - if (isText) { - console.dir({ - mockedFetchResponseText: ab.byteLength < 1000 ? text : text.slice(0, 1000) + "...(truncated)", - }); - } else { - console.log(`[Obsidian Mock] Fetch response is of content-type ${contentType}, not logging body.`); - } - } - return res; - } catch (e) { - // console.error("[Obsidian Mock] Fetch error:", e); - console.error(`[Obsidian Mock] Fetch failed for ${method} ${url}, error:`, e); - throw e; - } - }; -} diff --git a/test/lib/commands.ts b/test/lib/commands.ts deleted file mode 100644 index c8655380..00000000 --- a/test/lib/commands.ts +++ /dev/null @@ -1,165 +0,0 @@ -import type { P2PSyncSetting } from "@vrtmrz/livesync-commonlib/compat/common/types"; -import { delay } from "octagonal-wheels/promises"; -import type { BrowserContext, Page } from "playwright"; -import type { Plugin } from "vitest/config"; -import type { BrowserCommand } from "vitest/node"; -import { serialized } from "octagonal-wheels/concurrency/lock"; -export const grantClipboardPermissions: BrowserCommand = async (ctx) => { - if (ctx.provider.name === "playwright") { - await ctx.context.grantPermissions(["clipboard-read", "clipboard-write"]); - console.log("Granted clipboard permissions"); - return; - } -}; -let peerPage: Page | undefined; -let peerPageContext: BrowserContext | undefined; -let previousName = ""; -async function setValue(page: Page, selector: string, value: string) { - const e = await page.waitForSelector(selector); - await e.fill(value); -} -async function closePeerContexts() { - const peerPageLocal = peerPage; - const peerPageContextLocal = peerPageContext; - if (peerPageLocal) { - await peerPageLocal.close(); - } - if (peerPageContextLocal) { - await peerPageContextLocal.close(); - } -} -export const openWebPeer: BrowserCommand<[P2PSyncSetting, serverPeerName: string]> = async ( - ctx, - setting: P2PSyncSetting, - serverPeerName: string = "p2p-livesync-web-peer" -) => { - if (ctx.provider.name === "playwright") { - const previousPage = ctx.page; - if (peerPage !== undefined) { - if (previousName === serverPeerName) { - console.log(`WebPeer for ${serverPeerName} already opened`); - return; - } - console.log(`Closing previous WebPeer for ${previousName}`); - await closePeerContexts(); - } - console.log(`Opening webPeer`); - return serialized("webpeer", async () => { - const browser = ctx.context.browser()!; - const context = await browser.newContext(); - peerPageContext = context; - peerPage = await context.newPage(); - previousName = serverPeerName; - console.log(`Navigating...`); - await peerPage.goto("http://localhost:8081"); - await peerPage.waitForLoadState(); - console.log(`Navigated!`); - await setValue(peerPage, "#app > main [placeholder*=wss]", setting.P2P_relays); - await setValue(peerPage, "#app > main [placeholder*=anything]", setting.P2P_roomID); - await setValue(peerPage, "#app > main [placeholder*=password]", setting.P2P_passphrase); - await setValue(peerPage, "#app > main [placeholder*=iphone]", serverPeerName); - // await peerPage.getByTitle("Enable P2P Replicator").setChecked(true); - await peerPage.getByRole("checkbox").first().setChecked(true); - // (await peerPage.waitForSelector("Save and Apply")).click(); - await peerPage.getByText("Save and Apply").click(); - await delay(100); - await peerPage.reload(); - await delay(500); - for (let i = 0; i < 10; i++) { - await delay(100); - const btn = peerPage.getByRole("button").filter({ hasText: /^connect/i }); - if ((await peerPage.getByText(/disconnect/i).count()) > 0) { - break; - } - await btn.click(); - } - await previousPage.bringToFront(); - ctx.context.on("close", async () => { - console.log("Browser context is closing, closing peer page if exists"); - await closePeerContexts(); - }); - console.log("Web peer page opened"); - }); - } -}; - -export const closeWebPeer: BrowserCommand = async (ctx) => { - if (ctx.provider.name === "playwright") { - return serialized("webpeer", async () => { - await closePeerContexts(); - peerPage = undefined; - peerPageContext = undefined; - previousName = ""; - console.log("Web peer page closed"); - }); - } -}; -export const acceptWebPeer: BrowserCommand = async (ctx) => { - if (peerPage) { - // Detect dialogue - const buttonsOnDialogs = await peerPage.$$("popup .buttons button"); - for (const b of buttonsOnDialogs) { - const text = (await b.innerText()).toLowerCase(); - // console.log(`Dialog button found: ${text}`); - if (text === "accept") { - console.log("Accepting dialog"); - await b.click({ timeout: 300 }); - await delay(500); - } - } - const buttons = peerPage.getByRole("button").filter({ hasText: /^accept$/i }); - const a = await buttons.all(); - for (const b of a) { - await b.click({ timeout: 300 }); - } - } - return false; -}; - -/** Write arbitrary text to a file on the Node.js host (used for phase handoff). */ -export const writeHandoffFile: BrowserCommand<[filePath: string, content: string]> = async ( - _ctx, - filePath: string, - content: string -) => { - const fs = await import("node:fs/promises"); - await fs.writeFile(filePath, content, "utf-8"); -}; - -/** Read a file from the Node.js host (used for phase handoff). */ -export const readHandoffFile: BrowserCommand<[filePath: string]> = async (_ctx, filePath: string): Promise => { - const fs = await import("node:fs/promises"); - return fs.readFile(filePath, "utf-8"); -}; - -export default function BrowserCommands(): Plugin { - return { - name: "vitest:custom-commands", - config() { - return { - test: { - browser: { - commands: { - grantClipboardPermissions, - openWebPeer, - closeWebPeer, - acceptWebPeer, - writeHandoffFile, - readHandoffFile, - }, - }, - }, - }; - }, - }; -} -declare module "vitest/browser" { - interface BrowserCommands { - grantClipboardPermissions: () => Promise; - openWebPeer: (setting: P2PSyncSetting, serverPeerName: string) => Promise; - closeWebPeer: () => Promise; - acceptWebPeer: () => Promise; - writeHandoffFile: (filePath: string, content: string) => Promise; - readHandoffFile: (filePath: string) => Promise; - } -} diff --git a/test/lib/ui.ts b/test/lib/ui.ts deleted file mode 100644 index b4071cf5..00000000 --- a/test/lib/ui.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { page } from "vitest/browser"; -import { delay } from "@vrtmrz/livesync-commonlib/compat/common/utils"; - -export async function waitForDialogShown(dialogText: string, timeout = 500) { - const ttl = Date.now() + timeout; - while (Date.now() < ttl) { - try { - await delay(50); - const dialog = page - .getByText(dialogText) - .elements() - .filter((e) => e.classList.contains("modal-title")) - .filter((e) => e.checkVisibility()); - if (dialog.length === 0) { - continue; - } - return true; - } catch (e) { - // Ignore - } - } - return false; -} -export async function waitForDialogHidden(dialogText: string | RegExp, timeout = 500) { - const ttl = Date.now() + timeout; - while (Date.now() < ttl) { - try { - await delay(50); - const dialog = page - .getByText(dialogText) - .elements() - .filter((e) => e.classList.contains("modal-title")) - .filter((e) => e.checkVisibility()); - if (dialog.length > 0) { - // console.log(`Still exist ${dialogText.toString()}`); - continue; - } - return true; - } catch (e) { - // Ignore - } - } - return false; -} - -export async function waitForButtonClick(buttonText: string | RegExp, timeout = 500) { - const ttl = Date.now() + timeout; - while (Date.now() < ttl) { - try { - await delay(100); - const buttons = page - .getByText(buttonText) - .elements() - .filter((e) => e.checkVisibility() && e.tagName.toLowerCase() == "button"); - if (buttons.length == 0) { - // console.log(`Could not found ${buttonText.toString()}`); - continue; - } - console.log(`Button detected: ${buttonText.toString()}`); - // console.dir(buttons[0]) - await page.elementLocator(buttons[0]).click(); - await delay(100); - return true; - } catch (e) { - console.error(e); - // Ignore - } - } - return false; -} diff --git a/test/lib/util.ts b/test/lib/util.ts deleted file mode 100644 index 23a8ce65..00000000 --- a/test/lib/util.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { delay } from "@vrtmrz/livesync-commonlib/compat/common/utils"; - -export async function waitTaskWithFollowups( - task: Promise, - followup: () => Promise, - timeout: number = 10000, - interval: number = 1000 -): Promise { - const symbolNotCompleted = Symbol("notCompleted"); - const isCompleted = () => Promise.race([task, Promise.resolve(symbolNotCompleted)]); - const ttl = Date.now() + timeout; - do { - const state = await isCompleted(); - if (state !== symbolNotCompleted) { - return state; - } - await followup(); - await delay(interval); - } while (Date.now() < ttl); - throw new Error("Task did not complete in time"); -} diff --git a/test/shell/p2p-init.sh b/test/shell/p2p-init.sh deleted file mode 100755 index dd865c9c..00000000 --- a/test/shell/p2p-init.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -echo "P2P Init - No additional initialization required." \ No newline at end of file diff --git a/test/shell/p2p-start.sh b/test/shell/p2p-start.sh deleted file mode 100755 index 8c86a45c..00000000 --- a/test/shell/p2p-start.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash -set -e -script_dir=$(dirname "$0") -webpeer_dir=$script_dir/../../src/apps/webpeer - -docker run -d --name relay-test -p 4000:7777 \ - --tmpfs /app/strfry-db:rw,size=256m \ - --entrypoint sh \ - ghcr.io/hoytech/strfry:latest \ - -lc 'cat > /tmp/strfry.conf <<"EOF" -db = "./strfry-db/" - -relay { - bind = "0.0.0.0" - port = 7777 - nofiles = 100000 - - info { - name = "livesync test relay" - description = "local relay for livesync p2p tests" - } - - maxWebsocketPayloadSize = 131072 - autoPingSeconds = 55 - - writePolicy { - plugin = "" - } -} -EOF -exec /app/strfry --config /tmp/strfry.conf relay' -npm run --prefix $webpeer_dir build -docker run -d --name webpeer-test -p 8081:8043 -v $webpeer_dir/dist:/srv/http pierrezemb/gostatic \ No newline at end of file diff --git a/test/shell/p2p-stop.sh b/test/shell/p2p-stop.sh deleted file mode 100755 index 22925ad4..00000000 --- a/test/shell/p2p-stop.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -docker stop relay-test -docker rm relay-test -docker stop webpeer-test -docker rm webpeer-test \ No newline at end of file diff --git a/test/suite/db_common.ts b/test/suite/db_common.ts deleted file mode 100644 index d6e82012..00000000 --- a/test/suite/db_common.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { compareMTime, EVEN } from "@/common/utils"; -import { TFile, type DataWriteOptions } from "@/deps"; -import type { FilePath } from "@vrtmrz/livesync-commonlib/compat/common/types"; -import { isDocContentSame, readContent } from "@vrtmrz/livesync-commonlib/compat/common/utils"; -import { waitForIdle, type LiveSyncHarness } from "../harness/harness"; -import { expect } from "vitest"; - -export const defaultFileOption = { - mtime: new Date(2026, 0, 1, 0, 1, 2, 3).getTime(), -} as const satisfies DataWriteOptions; -export async function storeFile( - harness: LiveSyncHarness, - path: string, - content: string | Blob, - deleteBeforeSend = false, - fileOptions = defaultFileOption -) { - if (deleteBeforeSend && harness.app.vault.getAbstractFileByPath(path)) { - console.log(`Deleting existing file ${path}`); - await harness.app.vault.delete(harness.app.vault.getAbstractFileByPath(path) as TFile); - } - // Create file via vault - if (content instanceof Blob) { - console.log(`Creating binary file ${path}`); - await harness.app.vault.createBinary(path, await content.arrayBuffer(), fileOptions); - } else { - await harness.app.vault.create(path, content, fileOptions); - } - - // Ensure file is created - const file = harness.app.vault.getAbstractFileByPath(path); - expect(file).toBeInstanceOf(TFile); - if (file instanceof TFile) { - expect(compareMTime(file.stat.mtime, fileOptions?.mtime ?? defaultFileOption.mtime)).toBe(EVEN); - if (content instanceof Blob) { - const readContent = await harness.app.vault.readBinary(file); - expect(await isDocContentSame(readContent, content)).toBe(true); - } else { - const readContent = await harness.app.vault.read(file); - expect(readContent).toBe(content); - } - } - await harness.plugin.core.services.fileProcessing.commitPendingFileEvents(); - await waitForIdle(harness); - return file; -} -export async function readFromLocalDB(harness: LiveSyncHarness, path: string) { - const entry = await harness.plugin.core.localDatabase.getDBEntry(path as FilePath); - expect(entry).not.toBe(false); - return entry; -} -export async function readFromVault( - harness: LiveSyncHarness, - path: string, - isBinary: boolean = false, - fileOptions = defaultFileOption -): Promise { - const file = harness.app.vault.getAbstractFileByPath(path); - expect(file).toBeInstanceOf(TFile); - if (file instanceof TFile) { - // console.log(`MTime: ${file.stat.mtime}, Expected: ${fileOptions.mtime}`); - if (fileOptions.mtime !== undefined) { - expect(compareMTime(file.stat.mtime, fileOptions.mtime)).toBe(EVEN); - } - const content = isBinary ? await harness.app.vault.readBinary(file) : await harness.app.vault.read(file); - return content; - } - - throw new Error("File not found in vault"); -} -export async function checkStoredFileInDB( - harness: LiveSyncHarness, - path: string, - content: string | Blob, - fileOptions = defaultFileOption -) { - const entry = await readFromLocalDB(harness, path); - if (entry === false) { - throw new Error("DB Content not found"); - } - const contentToCheck = content instanceof Blob ? await content.arrayBuffer() : content; - const isDocSame = await isDocContentSame(readContent(entry), contentToCheck); - if (fileOptions.mtime !== undefined) { - expect(compareMTime(entry.mtime, fileOptions.mtime)).toBe(EVEN); - } - expect(isDocSame).toBe(true); - return Promise.resolve(); -} -export async function testFileWrite( - harness: LiveSyncHarness, - path: string, - content: string | Blob, - skipCheckToBeWritten = false, - fileOptions = defaultFileOption -) { - const file = await storeFile(harness, path, content, false, fileOptions); - expect(file).toBeInstanceOf(TFile); - await harness.plugin.core.services.fileProcessing.commitPendingFileEvents(); - await waitForIdle(harness); - const vaultFile = await readFromVault(harness, path, content instanceof Blob, fileOptions); - expect(await isDocContentSame(vaultFile, content)).toBe(true); - await harness.plugin.core.services.fileProcessing.commitPendingFileEvents(); - await waitForIdle(harness); - if (skipCheckToBeWritten) { - return Promise.resolve(); - } - await checkStoredFileInDB(harness, path, content); - return Promise.resolve(); -} -export async function testFileRead( - harness: LiveSyncHarness, - path: string, - expectedContent: string | Blob, - fileOptions = defaultFileOption -) { - await waitForIdle(harness); - const file = await readFromVault(harness, path, expectedContent instanceof Blob, fileOptions); - const isDocSame = await isDocContentSame(file, expectedContent); - expect(isDocSame).toBe(true); - // Check local database entry - const entry = await readFromLocalDB(harness, path); - expect(entry).not.toBe(false); - if (entry === false) { - throw new Error("DB Content not found"); - } - const isDBDocSame = await isDocContentSame(readContent(entry), expectedContent); - expect(isDBDocSame).toBe(true); - return await Promise.resolve(); -} diff --git a/test/suite/onlylocaldb.test.ts b/test/suite/onlylocaldb.test.ts deleted file mode 100644 index a140dd58..00000000 --- a/test/suite/onlylocaldb.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { beforeAll, describe, expect, it, test } from "vitest"; -import { generateHarness, waitForIdle, waitForReady, type LiveSyncHarness } from "../harness/harness"; -import { TFile } from "@/deps.ts"; -import { DEFAULT_SETTINGS, type FilePath, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types"; -import { isDocContentSame, readContent } from "@vrtmrz/livesync-commonlib/compat/common/utils"; -import { DummyFileSourceInisialised, generateBinaryFile, generateFile, init } from "../utils/dummyfile"; - -const localdb_test_setting = { - ...DEFAULT_SETTINGS, - isConfigured: true, - handleFilenameCaseSensitive: false, -} as ObsidianLiveSyncSettings; - -describe.skip("Plugin Integration Test (Local Database)", async () => { - let harness: LiveSyncHarness; - const vaultName = "TestVault" + Date.now(); - - beforeAll(async () => { - await DummyFileSourceInisialised; - harness = await generateHarness(vaultName, localdb_test_setting); - await waitForReady(harness); - }); - - it("should be instantiated and defined", async () => { - expect(harness.plugin).toBeDefined(); - expect(harness.plugin.app).toBe(harness.app); - return await Promise.resolve(); - }); - - it("should have services initialized", async () => { - expect(harness.plugin.core.services).toBeDefined(); - return await Promise.resolve(); - }); - it("should have local database initialized", async () => { - expect(harness.plugin.core.localDatabase).toBeDefined(); - expect(harness.plugin.core.localDatabase.isReady).toBe(true); - return await Promise.resolve(); - }); - - it("should store the changes into the local database", async () => { - const path = "test-store6.md"; - const content = "Hello, World!"; - if (harness.app.vault.getAbstractFileByPath(path)) { - console.log(`Deleting existing file ${path}`); - await harness.app.vault.delete(harness.app.vault.getAbstractFileByPath(path) as TFile); - } - // Create file via vault - await harness.app.vault.create(path, content); - - const file = harness.app.vault.getAbstractFileByPath(path); - expect(file).toBeInstanceOf(TFile); - - if (file instanceof TFile) { - const readContent = await harness.app.vault.read(file); - expect(readContent).toBe(content); - } - await harness.plugin.core.services.fileProcessing.commitPendingFileEvents(); - await waitForIdle(harness); - // await delay(100); // Wait a bit for the local database to process - - const entry = await harness.plugin.core.localDatabase.getDBEntry(path as FilePath); - expect(entry).not.toBe(false); - if (entry) { - expect(readContent(entry)).toBe(content); - } - return await Promise.resolve(); - }); - test.each([10, 100, 1000, 10000, 50000, 100000])("should handle large file of size %i bytes", async (size) => { - const path = `test-large-file-${size}.md`; - const content = Array.from(generateFile(size)).join(""); - if (harness.app.vault.getAbstractFileByPath(path)) { - console.log(`Deleting existing file ${path}`); - await harness.app.vault.delete(harness.app.vault.getAbstractFileByPath(path) as TFile); - } - // Create file via vault - await harness.app.vault.create(path, content); - const file = harness.app.vault.getAbstractFileByPath(path); - expect(file).toBeInstanceOf(TFile); - if (file instanceof TFile) { - const readContent = await harness.app.vault.read(file); - expect(readContent).toBe(content); - } - await harness.plugin.core.services.fileProcessing.commitPendingFileEvents(); - await waitForIdle(harness); - - const entry = await harness.plugin.core.localDatabase.getDBEntry(path as FilePath); - expect(entry).not.toBe(false); - if (entry) { - expect(readContent(entry)).toBe(content); - } - return await Promise.resolve(); - }); - - const binaryMap = Array.from({ length: 7 }, (_, i) => Math.pow(2, i * 4)); - test.each(binaryMap)("should handle binary file of size %i bytes", async (size) => { - const path = `test-binary-file-${size}.bin`; - const content = new Blob([...generateBinaryFile(size)], { type: "application/octet-stream" }); - if (harness.app.vault.getAbstractFileByPath(path)) { - console.log(`Deleting existing file ${path}`); - await harness.app.vault.delete(harness.app.vault.getAbstractFileByPath(path) as TFile); - } - // Create file via vault - await harness.app.vault.createBinary(path, await content.arrayBuffer()); - const file = harness.app.vault.getAbstractFileByPath(path); - expect(file).toBeInstanceOf(TFile); - if (file instanceof TFile) { - const readContent = await harness.app.vault.readBinary(file); - expect(await isDocContentSame(readContent, content)).toBe(true); - } - - await harness.plugin.core.services.fileProcessing.commitPendingFileEvents(); - await waitForIdle(harness); - const entry = await harness.plugin.core.localDatabase.getDBEntry(path as FilePath); - expect(entry).not.toBe(false); - if (entry) { - const entryContent = await readContent(entry); - if (!(entryContent instanceof ArrayBuffer)) { - throw new Error("Entry content is not an ArrayBuffer"); - } - // const expectedContent = await content.arrayBuffer(); - expect(await isDocContentSame(entryContent, content)).toBe(true); - } - return await Promise.resolve(); - }); -}); diff --git a/test/suite/sync.senario.basic.ts b/test/suite/sync.senario.basic.ts deleted file mode 100644 index 2c8b301f..00000000 --- a/test/suite/sync.senario.basic.ts +++ /dev/null @@ -1,275 +0,0 @@ -// Functional Test on Main Cases -// This test suite only covers main functional cases of synchronisation. Event handling, error cases, -// and edge, resolving conflicts, etc. will be covered in separate test suites. -import { afterAll, beforeAll, describe, expect, it, test } from "vitest"; -import { generateHarness, waitForIdle, waitForReady, type LiveSyncHarness } from "../harness/harness"; -import { RemoteTypes, type FilePath, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types"; - -import { - DummyFileSourceInisialised, - FILE_SIZE_BINS, - FILE_SIZE_MD, - generateBinaryFile, - generateFile, -} from "../utils/dummyfile"; -import { checkStoredFileInDB, testFileRead, testFileWrite } from "./db_common"; -import { delay } from "@vrtmrz/livesync-commonlib/compat/common/utils"; -import { commands } from "vitest/browser"; -import { closeReplication, performReplication, prepareRemote } from "./sync_common"; -import type { DataWriteOptions } from "@/deps.ts"; - -type MTimedDataWriteOptions = DataWriteOptions & { mtime: number }; -export type TestOptions = { - setting: ObsidianLiveSyncSettings; - fileOptions: MTimedDataWriteOptions; -}; -function generateName(prefix: string, type: string, ext: string, size: number) { - return `${prefix}-${type}-file-${size}.${ext}`; -} -export function syncBasicCase(label: string, { setting, fileOptions }: TestOptions) { - describe("Replication Suite Tests - " + label, () => { - const nameFile = (type: string, ext: string, size: number) => generateName("sync-test", type, ext, size); - let serverPeerName = ""; - // TODO: Harness disposal may broke the event loop of P2P replication - // so we keep the harnesses alive until all tests are done. - // It may trystero's somethong, or not. - let harnessUpload: LiveSyncHarness; - let harnessDownload: LiveSyncHarness; - beforeAll(async () => { - await DummyFileSourceInisialised; - if (setting.remoteType === RemoteTypes.REMOTE_P2P) { - // await commands.closeWebPeer(); - serverPeerName = "t-" + Date.now(); - setting.P2P_AutoAcceptingPeers = serverPeerName; - setting.P2P_AutoSyncPeers = serverPeerName; - setting.P2P_DevicePeerName = "client-" + Date.now(); - await commands.openWebPeer(setting, serverPeerName); - } - }); - afterAll(async () => { - if (setting.remoteType === RemoteTypes.REMOTE_P2P) { - await commands.closeWebPeer(); - // await closeP2PReplicatorConnections(harnessUpload); - } - }); - - describe("Remote Database Initialization", () => { - let harnessInit: LiveSyncHarness; - const sync_test_setting_init = { - ...setting, - } as ObsidianLiveSyncSettings; - beforeAll(async () => { - const vaultName = "TestVault" + Date.now(); - console.log(`BeforeAll - Remote Database Initialization - Vault: ${vaultName}`); - harnessInit = await generateHarness(vaultName, sync_test_setting_init); - await waitForReady(harnessInit); - expect(harnessInit.plugin).toBeDefined(); - expect(harnessInit.plugin.app).toBe(harnessInit.app); - await waitForIdle(harnessInit); - }); - afterAll(async () => { - await harnessInit.plugin.core.services.replicator.getActiveReplicator()?.closeReplication(); - await harnessInit.dispose(); - await delay(1000); - }); - - it("should reset remote database", async () => { - // harnessInit = await generateHarness(vaultName, sync_test_setting_init); - await waitForReady(harnessInit); - await prepareRemote(harnessInit, sync_test_setting_init, true); - }); - it("should be prepared for replication", async () => { - await waitForReady(harnessInit); - if (setting.remoteType !== RemoteTypes.REMOTE_P2P) { - const status = await harnessInit.plugin.core.services.replicator - .getActiveReplicator() - ?.getRemoteStatus(sync_test_setting_init); - console.log("Connected devices after reset:", status); - expect(status).not.toBeFalsy(); - } - }); - }); - - describe("Replication - Upload", () => { - const sync_test_setting_upload = { - ...setting, - } as ObsidianLiveSyncSettings; - - beforeAll(async () => { - const vaultName = "TestVault" + Date.now(); - console.log(`BeforeAll - Replication Upload - Vault: ${vaultName}`); - if (setting.remoteType === RemoteTypes.REMOTE_P2P) { - sync_test_setting_upload.P2P_AutoAcceptingPeers = serverPeerName; - sync_test_setting_upload.P2P_AutoSyncPeers = serverPeerName; - sync_test_setting_upload.P2P_DevicePeerName = "up-" + Date.now(); - } - harnessUpload = await generateHarness(vaultName, sync_test_setting_upload); - await waitForReady(harnessUpload); - expect(harnessUpload.plugin).toBeDefined(); - expect(harnessUpload.plugin.app).toBe(harnessUpload.app); - await waitForIdle(harnessUpload); - }); - - afterAll(async () => { - await closeReplication(harnessUpload); - }); - - it("should be instantiated and defined", () => { - expect(harnessUpload.plugin).toBeDefined(); - expect(harnessUpload.plugin.app).toBe(harnessUpload.app); - }); - - it("should have services initialized", () => { - expect(harnessUpload.plugin.core.services).toBeDefined(); - }); - - it("should have local database initialized", () => { - expect(harnessUpload.plugin.core.localDatabase).toBeDefined(); - expect(harnessUpload.plugin.core.localDatabase.isReady).toBe(true); - }); - - it("should prepare remote database", async () => { - await prepareRemote(harnessUpload, sync_test_setting_upload, false); - }); - - // describe("File Creation", async () => { - it("should a file has been created", async () => { - const content = "Hello, World!"; - const path = nameFile("store", "md", 0); - await testFileWrite(harnessUpload, path, content, false, fileOptions); - // Perform replication - // await harness.plugin.core.services.replication.replicate(true); - }); - it("should different content of several files have been created correctly", async () => { - await testFileWrite(harnessUpload, nameFile("test-diff-1", "md", 0), "Content A", false, fileOptions); - await testFileWrite(harnessUpload, nameFile("test-diff-2", "md", 0), "Content B", false, fileOptions); - await testFileWrite(harnessUpload, nameFile("test-diff-3", "md", 0), "Content C", false, fileOptions); - }); - - test.each(FILE_SIZE_MD)("should large file of size %i bytes has been created", async (size) => { - const content = Array.from(generateFile(size)).join(""); - const path = nameFile("large", "md", size); - const isTooLarge = harnessUpload.plugin.core.services.vault.isFileSizeTooLarge(size); - if (isTooLarge) { - console.log(`Skipping file of size ${size} bytes as it is too large to sync.`); - expect(true).toBe(true); - } else { - await testFileWrite(harnessUpload, path, content, false, fileOptions); - } - }); - - test.each(FILE_SIZE_BINS)("should binary file of size %i bytes has been created", async (size) => { - const content = new Blob([...generateBinaryFile(size)], { type: "application/octet-stream" }); - const path = nameFile("binary", "bin", size); - await testFileWrite(harnessUpload, path, content, true, fileOptions); - const isTooLarge = harnessUpload.plugin.core.services.vault.isFileSizeTooLarge(size); - if (isTooLarge) { - console.log(`Skipping file of size ${size} bytes as it is too large to sync.`); - expect(true).toBe(true); - } else { - await checkStoredFileInDB(harnessUpload, path, content, fileOptions); - } - }); - - it("Replication after uploads", async () => { - await performReplication(harnessUpload); - await performReplication(harnessUpload); - }); - }); - - describe("Replication - Download", () => { - // Download into a new vault - const sync_test_setting_download = { - ...setting, - } as ObsidianLiveSyncSettings; - beforeAll(async () => { - const vaultName = "TestVault" + Date.now(); - console.log(`BeforeAll - Replication Download - Vault: ${vaultName}`); - if (setting.remoteType === RemoteTypes.REMOTE_P2P) { - sync_test_setting_download.P2P_AutoAcceptingPeers = serverPeerName; - sync_test_setting_download.P2P_AutoSyncPeers = serverPeerName; - sync_test_setting_download.P2P_DevicePeerName = "down-" + Date.now(); - } - harnessDownload = await generateHarness(vaultName, sync_test_setting_download); - await waitForReady(harnessDownload); - await prepareRemote(harnessDownload, sync_test_setting_download, false); - - await performReplication(harnessDownload); - await waitForIdle(harnessDownload); - await delay(1000); - await performReplication(harnessDownload); - await waitForIdle(harnessDownload); - }); - afterAll(async () => { - await closeReplication(harnessDownload); - }); - - it("should be instantiated and defined", () => { - expect(harnessDownload.plugin).toBeDefined(); - expect(harnessDownload.plugin.app).toBe(harnessDownload.app); - }); - - it("should have services initialized", () => { - expect(harnessDownload.plugin.core.services).toBeDefined(); - }); - - it("should have local database initialized", () => { - expect(harnessDownload.plugin.core.localDatabase).toBeDefined(); - expect(harnessDownload.plugin.core.localDatabase.isReady).toBe(true); - }); - - it("should a file has been synchronised", async () => { - const expectedContent = "Hello, World!"; - const path = nameFile("store", "md", 0); - await testFileRead(harnessDownload, path, expectedContent, fileOptions); - }); - it("should different content of several files have been synchronised", async () => { - await testFileRead(harnessDownload, nameFile("test-diff-1", "md", 0), "Content A", fileOptions); - await testFileRead(harnessDownload, nameFile("test-diff-2", "md", 0), "Content B", fileOptions); - await testFileRead(harnessDownload, nameFile("test-diff-3", "md", 0), "Content C", fileOptions); - }); - - test.each(FILE_SIZE_MD)("should the file %i bytes had been synchronised", async (size) => { - const content = Array.from(generateFile(size)).join(""); - const path = nameFile("large", "md", size); - const isTooLarge = harnessDownload.plugin.core.services.vault.isFileSizeTooLarge(size); - if (isTooLarge) { - const entry = await harnessDownload.plugin.core.localDatabase.getDBEntry(path as FilePath); - console.log(`Skipping file of size ${size} bytes as it is too large to sync.`); - expect(entry).toBe(false); - } else { - await testFileRead(harnessDownload, path, content, fileOptions); - } - }); - - test.each(FILE_SIZE_BINS)("should binary file of size %i bytes had been synchronised", async (size) => { - const path = nameFile("binary", "bin", size); - - const isTooLarge = harnessDownload.plugin.core.services.vault.isFileSizeTooLarge(size); - if (isTooLarge) { - const entry = await harnessDownload.plugin.core.localDatabase.getDBEntry(path as FilePath); - console.log(`Skipping file of size ${size} bytes as it is too large to sync.`); - expect(entry).toBe(false); - } else { - const content = new Blob([...generateBinaryFile(size)], { type: "application/octet-stream" }); - await testFileRead(harnessDownload, path, content, fileOptions); - } - }); - }); - afterAll(async () => { - if (harnessDownload) { - await closeReplication(harnessDownload); - await harnessDownload.dispose(); - await delay(1000); - } - if (harnessUpload) { - await closeReplication(harnessUpload); - await harnessUpload.dispose(); - await delay(1000); - } - }); - it("Wait for idle state", async () => { - await delay(100); - }); - }); -} diff --git a/test/suite/sync.single.test.ts b/test/suite/sync.single.test.ts deleted file mode 100644 index a03f3bb0..00000000 --- a/test/suite/sync.single.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -// Functional Test on Main Cases -// This test suite only covers main functional cases of synchronisation. Event handling, error cases, -// and edge, resolving conflicts, etc. will be covered in separate test suites. -import { describe } from "vitest"; -import { - PREFERRED_JOURNAL_SYNC, - PREFERRED_SETTING_SELF_HOSTED, - RemoteTypes, - type ObsidianLiveSyncSettings, -} from "@vrtmrz/livesync-commonlib/compat/common/types"; - -import { defaultFileOption } from "./db_common"; -import { syncBasicCase } from "./sync.senario.basic.ts"; -import { settingBase } from "./variables.ts"; -const sync_test_setting_base = settingBase; -export const env = (import.meta as any).env; -function* generateCase() { - const passpharse = "thetest-Passphrase3+9-for-e2ee!"; - const REMOTE_RECOMMENDED = { - [RemoteTypes.REMOTE_COUCHDB]: PREFERRED_SETTING_SELF_HOSTED, - [RemoteTypes.REMOTE_MINIO]: PREFERRED_JOURNAL_SYNC, - [RemoteTypes.REMOTE_P2P]: PREFERRED_SETTING_SELF_HOSTED, - }; - const remoteTypes = [RemoteTypes.REMOTE_COUCHDB]; - // const remoteTypes = [RemoteTypes.REMOTE_P2P]; - const e2eeOptions = [false]; - // const e2eeOptions = [true]; - for (const remoteType of remoteTypes) { - for (const useE2EE of e2eeOptions) { - yield { - setting: { - ...sync_test_setting_base, - ...REMOTE_RECOMMENDED[remoteType], - remoteType, - encrypt: useE2EE, - passphrase: useE2EE ? passpharse : "", - usePathObfuscation: useE2EE, - } as ObsidianLiveSyncSettings, - }; - } - } -} - -describe.skip("Replication Suite Tests (Single)", async () => { - const cases = Array.from(generateCase()); - const fileOptions = defaultFileOption; - describe.each(cases)("Replication Tests - Remote: $setting.remoteType, E2EE: $setting.encrypt", ({ setting }) => { - syncBasicCase(`Remote: ${setting.remoteType}, E2EE: ${setting.encrypt}`, { setting, fileOptions }); - }); -}); diff --git a/test/suite/sync.test.ts b/test/suite/sync.test.ts deleted file mode 100644 index 6873f121..00000000 --- a/test/suite/sync.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -// Functional Test on Main Cases -// This test suite only covers main functional cases of synchronisation. Event handling, error cases, -// and edge, resolving conflicts, etc. will be covered in separate test suites. -import { describe } from "vitest"; -import { - PREFERRED_JOURNAL_SYNC, - PREFERRED_SETTING_SELF_HOSTED, - RemoteTypes, - type ObsidianLiveSyncSettings, -} from "@vrtmrz/livesync-commonlib/compat/common/types"; - -import { defaultFileOption } from "./db_common"; -import { syncBasicCase } from "./sync.senario.basic.ts"; -import { settingBase } from "./variables.ts"; -const sync_test_setting_base = settingBase; -export const env = (import.meta as any).env; -function* generateCase() { - const passpharse = "thetest-Passphrase3+9-for-e2ee!"; - const REMOTE_RECOMMENDED = { - [RemoteTypes.REMOTE_COUCHDB]: PREFERRED_SETTING_SELF_HOSTED, - [RemoteTypes.REMOTE_MINIO]: PREFERRED_JOURNAL_SYNC, - [RemoteTypes.REMOTE_P2P]: PREFERRED_SETTING_SELF_HOSTED, - }; - const remoteTypes = [RemoteTypes.REMOTE_COUCHDB, RemoteTypes.REMOTE_MINIO]; - // const remoteTypes = [RemoteTypes.REMOTE_P2P]; - const e2eeOptions = [false, true]; - // const e2eeOptions = [true]; - for (const remoteType of remoteTypes) { - for (const useE2EE of e2eeOptions) { - yield { - setting: { - ...sync_test_setting_base, - ...REMOTE_RECOMMENDED[remoteType], - remoteType, - encrypt: useE2EE, - passphrase: useE2EE ? passpharse : "", - usePathObfuscation: useE2EE, - } as ObsidianLiveSyncSettings, - }; - } - } -} - -describe("Replication Suite Tests (Normal)", async () => { - const cases = Array.from(generateCase()); - const fileOptions = defaultFileOption; - describe.each(cases)("Replication Tests - Remote: $setting.remoteType, E2EE: $setting.encrypt", ({ setting }) => { - syncBasicCase(`Remote: ${setting.remoteType}, E2EE: ${setting.encrypt}`, { setting, fileOptions }); - }); -}); diff --git a/test/suite/sync_common.ts b/test/suite/sync_common.ts deleted file mode 100644 index 82647d78..00000000 --- a/test/suite/sync_common.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { expect } from "vitest"; -import { waitForIdle, type LiveSyncHarness } from "../harness/harness"; -import { RemoteTypes, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types"; - -import { delay, fireAndForget } from "@vrtmrz/livesync-commonlib/compat/common/utils"; -import { commands } from "vitest/browser"; -import { LiveSyncTrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/LiveSyncTrysteroReplicator"; -import { waitTaskWithFollowups } from "../lib/util"; -async function waitForP2PPeers(harness: LiveSyncHarness) { - if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) { - // Wait for peers to connect - const maxRetries = 20; - let retries = maxRetries; - const replicator = await harness.plugin.core.services.replicator.getActiveReplicator(); - if (!(replicator instanceof LiveSyncTrysteroReplicator)) { - throw new Error("Replicator is not an instance of LiveSyncTrysteroReplicator"); - } - while (retries-- > 0) { - fireAndForget(() => commands.acceptWebPeer()); - await delay(1000); - const peers = replicator.knownAdvertisements; - - if (peers && peers.length > 0) { - console.log("P2P peers connected:", peers); - return; - } - fireAndForget(() => commands.acceptWebPeer()); - console.log(`Waiting for any P2P peers to be connected... ${maxRetries - retries}/${maxRetries}`); - console.dir(peers); - await delay(1000); - } - console.log("Failed to connect P2P peers after retries"); - throw new Error("P2P peers did not connect in time."); - } -} -export async function closeP2PReplicatorConnections(harness: LiveSyncHarness) { - if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) { - const replicator = await harness.plugin.core.services.replicator.getActiveReplicator(); - if (!(replicator instanceof LiveSyncTrysteroReplicator)) { - throw new Error("Replicator is not an instance of LiveSyncTrysteroReplicator"); - } - replicator.closeReplication(); - await delay(30); - replicator.closeReplication(); - await delay(1000); - console.log("P2P replicator connections closed"); - // if (replicator instanceof LiveSyncTrysteroReplicator) { - // replicator.closeReplication(); - // await delay(1000); - // } - } -} - -export async function performReplication(harness: LiveSyncHarness) { - await waitForP2PPeers(harness); - await delay(500); - const p = harness.plugin.core.services.replication.replicate(true); - const task = - harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P - ? waitTaskWithFollowups( - p, - () => { - // Accept any peer dialogs during replication (fire and forget) - fireAndForget(() => commands.acceptWebPeer()); - return Promise.resolve(); - }, - 30000, - 500 - ) - : p; - const result = await task; - // await waitForIdle(harness); - // if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) { - // await closeP2PReplicatorConnections(harness); - // } - return result; -} - -export async function closeReplication(harness: LiveSyncHarness) { - if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) { - return await closeP2PReplicatorConnections(harness); - } - const replicator = await harness.plugin.core.services.replicator.getActiveReplicator(); - if (!replicator) { - console.log("No active replicator to close"); - return; - } - await replicator.closeReplication(); - await waitForIdle(harness); - console.log("Replication closed"); -} - -export async function prepareRemote(harness: LiveSyncHarness, setting: ObsidianLiveSyncSettings, shouldReset = false) { - if (setting.remoteType !== RemoteTypes.REMOTE_P2P) { - if (shouldReset) { - await delay(1000); - await harness.plugin.core.services.replicator - .getActiveReplicator() - ?.tryResetRemoteDatabase(harness.plugin.core.settings); - } else { - await harness.plugin.core.services.replicator - .getActiveReplicator() - ?.tryCreateRemoteDatabase(harness.plugin.core.settings); - } - await harness.plugin.core.services.replicator - .getActiveReplicator() - ?.markRemoteResolved(harness.plugin.core.settings); - // No exceptions should be thrown - const status = await harness.plugin.core.services.replicator - .getActiveReplicator() - ?.getRemoteStatus(harness.plugin.core.settings); - console.log("Remote status:", status); - expect(status).not.toBeFalsy(); - } -} diff --git a/test/suite/variables.ts b/test/suite/variables.ts deleted file mode 100644 index 6207c86a..00000000 --- a/test/suite/variables.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { DoctorRegulation } from "@vrtmrz/livesync-commonlib/compat/common/configForDoc"; -import { - DEFAULT_SETTINGS, - ChunkAlgorithms, - AutoAccepting, - type ObsidianLiveSyncSettings, -} from "@vrtmrz/livesync-commonlib/compat/common/types"; -export const env = (import.meta as any).env; -export const settingBase = { - ...DEFAULT_SETTINGS, - isConfigured: true, - handleFilenameCaseSensitive: false, - couchDB_URI: `${env.hostname}`, - couchDB_DBNAME: `${env.dbname}`, - couchDB_USER: `${env.username}`, - couchDB_PASSWORD: `${env.password}`, - bucket: `${env.bucketName}`, - region: "us-east-1", - endpoint: `${env.minioEndpoint}`, - accessKey: `${env.accessKey}`, - secretKey: `${env.secretKey}`, - useCustomRequestHandler: true, - forcePathStyle: true, - bucketPrefix: "", - usePluginSyncV2: true, - chunkSplitterVersion: ChunkAlgorithms.RabinKarp, - doctorProcessedVersion: DoctorRegulation.version, - notifyThresholdOfRemoteStorageSize: 800, - P2P_AutoAccepting: AutoAccepting.ALL, - P2P_AutoBroadcast: true, - P2P_AutoStart: true, - P2P_Enabled: true, - P2P_passphrase: "p2psync-test", - P2P_roomID: "p2psync-test", - P2P_DevicePeerName: "p2psync-test", - P2P_relays: "ws://localhost:4000/", - P2P_AutoAcceptingPeers: "p2p-livesync-web-peer", - P2P_SyncOnReplication: "p2p-livesync-web-peer", -} as ObsidianLiveSyncSettings; diff --git a/test/suitep2p/run-p2p-tests.sh b/test/suitep2p/run-p2p-tests.sh deleted file mode 100755 index 4d8a50c0..00000000 --- a/test/suitep2p/run-p2p-tests.sh +++ /dev/null @@ -1,194 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd -- "$SCRIPT_DIR/../.." && pwd)" -CLI_DIR="$REPO_ROOT/src/apps/cli" -CLI_TEST_HELPERS="$CLI_DIR/test/test-helpers.sh" - -source "$CLI_TEST_HELPERS" - -RUN_BUILD="${RUN_BUILD:-1}" -KEEP_TEST_DATA="${KEEP_TEST_DATA:-1}" -VERBOSE_TEST_LOGGING="${VERBOSE_TEST_LOGGING:-1}" - -RELAY="${RELAY:-ws://localhost:4000/}" -USE_INTERNAL_RELAY="${USE_INTERNAL_RELAY:-1}" -APP_ID="${APP_ID:-self-hosted-livesync-vitest-p2p}" -HOST_PEER_NAME="${HOST_PEER_NAME:-p2p-cli-host}" - -ROOM_ID="p2p-room-$(date +%s)-$RANDOM-$RANDOM" -PASSPHRASE="p2p-pass-$(date +%s)-$RANDOM-$RANDOM" -UPLOAD_PEER_NAME="p2p-upload-$(date +%s)-$RANDOM" -DOWNLOAD_PEER_NAME="p2p-download-$(date +%s)-$RANDOM" -UPLOAD_VAULT_NAME="TestVaultUpload-$(date +%s)-$RANDOM" -DOWNLOAD_VAULT_NAME="TestVaultDownload-$(date +%s)-$RANDOM" - -# ---- Build CLI ---- -if [[ "$RUN_BUILD" == "1" ]]; then - echo "[INFO] building CLI" - (cd "$CLI_DIR" && npm run build) -fi - -# ---- Temp directory ---- -WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/livesync-vitest-p2p.XXXXXX")" -VAULT_HOST="$WORK_DIR/vault-host" -SETTINGS_HOST="$WORK_DIR/settings-host.json" -HOST_LOG="$WORK_DIR/p2p-host.log" -# Handoff file: upload phase writes this; download phase reads it. -HANDOFF_FILE="$WORK_DIR/p2p-test-handoff.json" -mkdir -p "$VAULT_HOST" - -# ---- Setup CLI command (uses npm run cli from CLI_DIR) ---- -# Override run_cli to invoke the built binary directly from CLI_DIR -run_cli() { - (cd "$CLI_DIR" && node dist/index.cjs "$@") -} - -# ---- Create host settings ---- -echo "[INFO] relay=$RELAY room=$ROOM_ID app=$APP_ID host=$HOST_PEER_NAME" -cli_test_init_settings_file "$SETTINGS_HOST" -cli_test_apply_p2p_settings "$SETTINGS_HOST" "$ROOM_ID" "$PASSPHRASE" "$APP_ID" "$RELAY" "~.*" - -# Set host peer name -SETTINGS_HOST_FILE="$SETTINGS_HOST" HOST_PEER_NAME_VAL="$HOST_PEER_NAME" HOST_PASSPHRASE_VAL="$PASSPHRASE" node <<'NODE' -const fs = require("node:fs"); -const data = JSON.parse(fs.readFileSync(process.env.SETTINGS_HOST_FILE, "utf-8")); - -// Keep tweak values aligned with browser-side P2P test settings. -data.remoteType = "ONLY_P2P"; -data.encrypt = true; -data.passphrase = process.env.HOST_PASSPHRASE_VAL; -data.usePathObfuscation = true; -data.handleFilenameCaseSensitive = false; -data.customChunkSize = 50; -data.usePluginSyncV2 = true; -data.doNotUseFixedRevisionForChunks = false; - -data.P2P_DevicePeerName = process.env.HOST_PEER_NAME_VAL; -fs.writeFileSync(process.env.SETTINGS_HOST_FILE, JSON.stringify(data, null, 2), "utf-8"); -NODE - -# ---- Cleanup trap ---- -cleanup() { - local exit_code=$? - if [[ -n "${HOST_PID:-}" ]] && kill -0 "$HOST_PID" >/dev/null 2>&1; then - echo "[INFO] stopping CLI host (PID=$HOST_PID)" - kill -TERM "$HOST_PID" >/dev/null 2>&1 || true - wait "$HOST_PID" >/dev/null 2>&1 || true - fi - - if [[ "${P2P_RELAY_STARTED:-0}" == "1" ]]; then - cli_test_stop_p2p_relay - fi - - if [[ "$KEEP_TEST_DATA" != "1" ]]; then - rm -rf "$WORK_DIR" - else - echo "[INFO] KEEP_TEST_DATA=1, preserving artefacts at $WORK_DIR" - fi - - exit "$exit_code" -} -trap cleanup EXIT - -start_host() { - local attempt=0 - while [[ "$attempt" -lt 5 ]]; do - attempt=$((attempt + 1)) - echo "[INFO] starting CLI p2p-host (attempt $attempt/5)" - : >"$HOST_LOG" - (cd "$CLI_DIR" && node dist/index.cjs "$VAULT_HOST" --settings "$SETTINGS_HOST" -d p2p-host) >"$HOST_LOG" 2>&1 & - HOST_PID=$! - - local host_ready=0 - local exited_early=0 - for i in $(seq 1 30); do - if grep -qF "P2P host is running" "$HOST_LOG" 2>/dev/null; then - host_ready=1 - break - fi - if ! kill -0 "$HOST_PID" >/dev/null 2>&1; then - exited_early=1 - break - fi - echo "[INFO] waiting for p2p-host to be ready... ($i/30)" - sleep 1 - done - - if [[ "$host_ready" == "1" ]]; then - echo "[INFO] p2p-host is ready (PID=$HOST_PID)" - return 0 - fi - - wait "$HOST_PID" >/dev/null 2>&1 || true - HOST_PID= - - if grep -qF "Resource temporarily unavailable" "$HOST_LOG" 2>/dev/null; then - echo "[INFO] p2p-host database lock is still being released, retrying..." - sleep 2 - continue - fi - - if [[ "$exited_early" == "1" ]]; then - echo "[FAIL] CLI host process exited unexpectedly" >&2 - else - echo "[FAIL] p2p-host did not become ready within 30 seconds" >&2 - fi - cat "$HOST_LOG" >&2 - exit 1 - done - - echo "[FAIL] p2p-host could not be restarted after multiple attempts" >&2 - cat "$HOST_LOG" >&2 - exit 1 -} - -# ---- Start local relay if needed ---- -if [[ "$USE_INTERNAL_RELAY" == "1" ]]; then - if cli_test_is_local_p2p_relay "$RELAY"; then - cli_test_start_p2p_relay - P2P_RELAY_STARTED=1 - else - echo "[INFO] USE_INTERNAL_RELAY=1 but RELAY is not local ($RELAY), skipping" - fi -fi - -start_host - -# Common env vars passed to both vitest runs -P2P_ENV=( - P2P_TEST_ROOM_ID="$ROOM_ID" - P2P_TEST_PASSPHRASE="$PASSPHRASE" - P2P_TEST_HOST_PEER_NAME="$HOST_PEER_NAME" - P2P_TEST_RELAY="$RELAY" - P2P_TEST_APP_ID="$APP_ID" - P2P_TEST_HANDOFF_FILE="$HANDOFF_FILE" - P2P_TEST_UPLOAD_PEER_NAME="$UPLOAD_PEER_NAME" - P2P_TEST_DOWNLOAD_PEER_NAME="$DOWNLOAD_PEER_NAME" - P2P_TEST_UPLOAD_VAULT_NAME="$UPLOAD_VAULT_NAME" - P2P_TEST_DOWNLOAD_VAULT_NAME="$DOWNLOAD_VAULT_NAME" -) - -cd "$REPO_ROOT" - -# ---- Phase 1: Upload ---- -# Each vitest run gets a fresh browser process, so Trystero's module-level -# global state (occupiedRooms, didInit, etc.) is clean for every phase. -echo "[INFO] running P2P vitest — upload phase" -env "${P2P_ENV[@]}" \ - npx dotenv-cli -e .env -e .test.env -- \ - vitest run --config vitest.config.p2p.ts test/suitep2p/syncp2p.p2p-up.test.ts -echo "[INFO] upload phase completed" - -# ---- Phase 2: Download ---- -# Keep the same host process alive so its database handle and relay presence stay stable. -echo "[INFO] waiting 5s before download phase..." -sleep 5 -echo "[INFO] running P2P vitest — download phase" -env "${P2P_ENV[@]}" \ - npx dotenv-cli -e .env -e .test.env -- \ - vitest run --config vitest.config.p2p.ts test/suitep2p/syncp2p.p2p-down.test.ts -echo "[INFO] download phase completed" - -echo "[INFO] P2P vitest suite completed" diff --git a/test/suitep2p/sync_common_p2p.ts b/test/suitep2p/sync_common_p2p.ts deleted file mode 100644 index 3b37f814..00000000 --- a/test/suitep2p/sync_common_p2p.ts +++ /dev/null @@ -1,175 +0,0 @@ -/** - * P2P-specific sync helpers. - * - * Derived from test/suite/sync_common.ts but with all acceptWebPeer() calls - * removed. When using a CLI p2p-host with P2P_AutoAcceptingPeers="~.*", peer - * acceptance is automatic and no Playwright dialog interaction is needed. - */ -import { expect } from "vitest"; -import { waitForIdle, type LiveSyncHarness } from "../harness/harness"; -import { RemoteTypes, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types"; -import { delay } from "@vrtmrz/livesync-commonlib/compat/common/utils"; -import { LiveSyncTrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/LiveSyncTrysteroReplicator"; -import { waitTaskWithFollowups } from "../lib/util"; - -const P2P_REPLICATION_TIMEOUT_MS = 180000; - -async function testWebSocketConnection(relayUrl: string): Promise { - return new Promise((resolve, reject) => { - console.log(`[P2P Debug] Testing WebSocket connection to ${relayUrl}`); - try { - const ws = new WebSocket(relayUrl); - const timer = setTimeout(() => { - ws.close(); - reject(new Error(`WebSocket connection to ${relayUrl} timed out`)); - }, 5000); - ws.onopen = () => { - clearTimeout(timer); - console.log(`[P2P Debug] WebSocket connected to ${relayUrl} successfully`); - ws.close(); - resolve(); - }; - ws.onerror = (e) => { - clearTimeout(timer); - console.error(`[P2P Debug] WebSocket error connecting to ${relayUrl}:`, e); - reject(new Error(`WebSocket connection to ${relayUrl} failed`)); - }; - } catch (e) { - console.error(`[P2P Debug] WebSocket constructor threw:`, e); - reject(e); - } - }); -} - -async function waitForP2PPeers(harness: LiveSyncHarness) { - if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) { - const maxRetries = 20; - let retries = maxRetries; - const replicator = await harness.plugin.core.services.replicator.getActiveReplicator(); - console.log("[P2P Debug] replicator type:", replicator?.constructor?.name); - if (!(replicator instanceof LiveSyncTrysteroReplicator)) { - throw new Error("Replicator is not an instance of LiveSyncTrysteroReplicator"); - } - - // Ensure P2P is open (getActiveReplicator returns a fresh instance that may not be open yet) - if (!replicator.server?.isServing) { - console.log("[P2P Debug] P2P not yet serving, calling open()"); - // Test WebSocket connectivity first - const relay = harness.plugin.core.settings.P2P_relays?.split(",")[0]?.trim(); - if (relay) { - try { - await testWebSocketConnection(relay); - } catch (e) { - console.error("[P2P Debug] WebSocket connectivity test failed:", e); - } - } - try { - await replicator.open(); - console.log("[P2P Debug] open() completed, isServing:", replicator.server?.isServing); - } catch (e) { - console.error("[P2P Debug] open() threw:", e); - } - } - - // Wait for P2P server to actually start (room joined) - for (let i = 0; i < 30; i++) { - const serving = replicator.server?.isServing; - console.log(`[P2P Debug] isServing: ${serving} (${i}/30)`); - if (serving) break; - await delay(500); - if (i === 29) throw new Error("P2P server did not start in time."); - } - - while (retries-- > 0) { - await delay(1000); - const peers = replicator.knownAdvertisements; - if (peers && peers.length > 0) { - console.log("P2P peers connected:", peers); - return; - } - console.log(`Waiting for any P2P peers to be connected... ${maxRetries - retries}/${maxRetries}`); - console.dir(peers); - await delay(1000); - } - console.log("Failed to connect P2P peers after retries"); - throw new Error("P2P peers did not connect in time."); - } -} - -export async function closeP2PReplicatorConnections(harness: LiveSyncHarness) { - if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) { - const replicator = await harness.plugin.core.services.replicator.getActiveReplicator(); - if (!(replicator instanceof LiveSyncTrysteroReplicator)) { - throw new Error("Replicator is not an instance of LiveSyncTrysteroReplicator"); - } - replicator.closeReplication(); - await delay(30); - replicator.closeReplication(); - await delay(1000); - console.log("P2P replicator connections closed"); - } -} - -export async function performReplication(harness: LiveSyncHarness) { - await waitForP2PPeers(harness); - await delay(500); - if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) { - const replicator = await harness.plugin.core.services.replicator.getActiveReplicator(); - if (!(replicator instanceof LiveSyncTrysteroReplicator)) { - throw new Error("Replicator is not an instance of LiveSyncTrysteroReplicator"); - } - const knownPeers = replicator.knownAdvertisements; - - const targetPeer = knownPeers.find((peer) => peer.name.startsWith("vault-host")) ?? knownPeers[0] ?? undefined; - if (!targetPeer) { - throw new Error("No connected P2P peer to synchronise with"); - } - - const p = replicator.sync(targetPeer.peerId, true); - const result = await waitTaskWithFollowups(p, () => Promise.resolve(), P2P_REPLICATION_TIMEOUT_MS, 500); - if (result && typeof result === "object" && "error" in result && result.error) { - throw result.error; - } - return result; - } - - return await harness.plugin.core.services.replication.replicate(true); -} - -export async function closeReplication(harness: LiveSyncHarness) { - if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) { - return await closeP2PReplicatorConnections(harness); - } - const replicator = await harness.plugin.core.services.replicator.getActiveReplicator(); - if (!replicator) { - console.log("No active replicator to close"); - return; - } - await replicator.closeReplication(); - await waitForIdle(harness); - console.log("Replication closed"); -} - -export async function prepareRemote(harness: LiveSyncHarness, setting: ObsidianLiveSyncSettings, shouldReset = false) { - // P2P has no remote database to initialise — skip - if (setting.remoteType === RemoteTypes.REMOTE_P2P) return; - - if (shouldReset) { - await delay(1000); - await harness.plugin.core.services.replicator - .getActiveReplicator() - ?.tryResetRemoteDatabase(harness.plugin.core.settings); - } else { - await harness.plugin.core.services.replicator - .getActiveReplicator() - ?.tryCreateRemoteDatabase(harness.plugin.core.settings); - } - await harness.plugin.core.services.replicator - .getActiveReplicator() - ?.markRemoteResolved(harness.plugin.core.settings); - const status = await harness.plugin.core.services.replicator - .getActiveReplicator() - ?.getRemoteStatus(harness.plugin.core.settings); - console.log("Remote status:", status); - expect(status).not.toBeFalsy(); -} diff --git a/test/suitep2p/syncp2p.p2p-down.test.ts b/test/suitep2p/syncp2p.p2p-down.test.ts deleted file mode 100644 index 4d1c4e17..00000000 --- a/test/suitep2p/syncp2p.p2p-down.test.ts +++ /dev/null @@ -1,165 +0,0 @@ -/** - * P2P Replication Tests — Download phase (process 2 of 2) - * - * Executed by run-p2p-tests.sh as the second vitest process, after the - * upload phase has completed and the CLI host holds all the data. - * - * Reads the handoff JSON written by the upload phase to know which files - * to verify, then replicates from the CLI host and checks every file. - */ -import { afterAll, beforeAll, beforeEach, describe, expect, it, test } from "vitest"; -import { generateHarness, waitForIdle, waitForReady, type LiveSyncHarness } from "../harness/harness"; -import { - PREFERRED_SETTING_SELF_HOSTED, - RemoteTypes, - type FilePath, - type ObsidianLiveSyncSettings, - AutoAccepting, -} from "@vrtmrz/livesync-commonlib/compat/common/types"; -import { DummyFileSourceInisialised, generateBinaryFile, generateFile } from "../utils/dummyfile"; -import { defaultFileOption, testFileRead } from "../suite/db_common"; -import { delay } from "@vrtmrz/livesync-commonlib/compat/common/utils"; -import { closeReplication, performReplication } from "./sync_common_p2p"; -import { settingBase } from "../suite/variables"; - -const env = (import.meta as any).env; - -const ROOM_ID: string = env.P2P_TEST_ROOM_ID ?? "p2p-test-room"; -const PASSPHRASE: string = env.P2P_TEST_PASSPHRASE ?? "p2p-test-pass"; -const HOST_PEER_NAME: string = env.P2P_TEST_HOST_PEER_NAME ?? "p2p-cli-host"; -const RELAY: string = env.P2P_TEST_RELAY ?? "ws://localhost:4000/"; -const APP_ID: string = env.P2P_TEST_APP_ID ?? "self-hosted-livesync-vitest-p2p"; -const DOWNLOAD_PEER_NAME: string = env.P2P_TEST_DOWNLOAD_PEER_NAME ?? `p2p-download-${Date.now()}`; -const DOWNLOAD_VAULT_NAME: string = env.P2P_TEST_DOWNLOAD_VAULT_NAME ?? `TestVaultDownload-${Date.now()}`; -const HANDOFF_FILE: string = env.P2P_TEST_HANDOFF_FILE ?? "/tmp/p2p-test-handoff.json"; - -console.log("[P2P Down] ROOM_ID:", ROOM_ID, "HOST:", HOST_PEER_NAME, "RELAY:", RELAY, "APP_ID:", APP_ID); -console.log("[P2P Down] HANDOFF_FILE:", HANDOFF_FILE); - -const p2pSetting: ObsidianLiveSyncSettings = { - ...settingBase, - ...PREFERRED_SETTING_SELF_HOSTED, - showVerboseLog: true, - remoteType: RemoteTypes.REMOTE_P2P, - encrypt: true, - passphrase: PASSPHRASE, - usePathObfuscation: true, - P2P_Enabled: true, - P2P_AppID: APP_ID, - handleFilenameCaseSensitive: false, - P2P_AutoAccepting: AutoAccepting.ALL, - P2P_AutoBroadcast: true, - P2P_AutoStart: true, - P2P_passphrase: PASSPHRASE, - P2P_roomID: ROOM_ID, - P2P_relays: RELAY, - P2P_AutoAcceptingPeers: "~.*", - P2P_SyncOnReplication: HOST_PEER_NAME, -}; - -const fileOptions = defaultFileOption; -const nameFile = (type: string, ext: string, size: number) => `p2p-cli-test-${type}-file-${size}.${ext}`; - -/** Read the handoff JSON produced by the upload phase. */ -async function readHandoff(): Promise<{ fileSizeMd: number[]; fileSizeBins: number[] }> { - const { commands } = await import("@vitest/browser/context"); - const raw = await commands.readHandoffFile(HANDOFF_FILE); - return JSON.parse(raw); -} - -describe("P2P Replication — Download", () => { - let harnessDownload: LiveSyncHarness; - let fileSizeMd: number[] = []; - let fileSizeBins: number[] = []; - - const downloadSetting: ObsidianLiveSyncSettings = { - ...p2pSetting, - P2P_DevicePeerName: DOWNLOAD_PEER_NAME, - }; - - beforeAll(async () => { - await DummyFileSourceInisialised; - - const handoff = await readHandoff(); - fileSizeMd = handoff.fileSizeMd; - fileSizeBins = handoff.fileSizeBins; - console.log("[P2P Down] handoff loaded — md sizes:", fileSizeMd, "bin sizes:", fileSizeBins); - - const vaultName = DOWNLOAD_VAULT_NAME; - console.log(`[P2P Down] BeforeAll - Vault: ${vaultName}`); - console.log(`[P2P Down] Peer name: ${DOWNLOAD_PEER_NAME}`); - harnessDownload = await generateHarness(vaultName, downloadSetting); - await waitForReady(harnessDownload); - - await performReplication(harnessDownload); - await waitForIdle(harnessDownload); - await delay(1000); - await performReplication(harnessDownload); - await waitForIdle(harnessDownload); - await delay(3000); - }); - beforeEach(async () => { - await performReplication(harnessDownload); - await waitForIdle(harnessDownload); - }); - - afterAll(async () => { - await closeReplication(harnessDownload); - await harnessDownload.dispose(); - await delay(1000); - }); - - it("should be instantiated and defined", () => { - expect(harnessDownload.plugin).toBeDefined(); - expect(harnessDownload.plugin.app).toBe(harnessDownload.app); - }); - - it("should have services initialized", () => { - expect(harnessDownload.plugin.core.services).toBeDefined(); - }); - - it("should have local database initialized", () => { - expect(harnessDownload.plugin.core.localDatabase).toBeDefined(); - expect(harnessDownload.plugin.core.localDatabase.isReady).toBe(true); - }); - - it("should have synchronised the stored file", async () => { - await testFileRead(harnessDownload, nameFile("store", "md", 0), "Hello, World!", fileOptions); - }); - - it("should have synchronised files with different content", async () => { - await testFileRead(harnessDownload, nameFile("test-diff-1", "md", 0), "Content A", fileOptions); - await testFileRead(harnessDownload, nameFile("test-diff-2", "md", 0), "Content B", fileOptions); - await testFileRead(harnessDownload, nameFile("test-diff-3", "md", 0), "Content C", fileOptions); - }); - - // NOTE: test.each cannot use variables populated in beforeAll, so we use - // a single it() that iterates over the sizes loaded from the handoff file. - it("should have synchronised all large md files", async () => { - for (const size of fileSizeMd) { - const content = Array.from(generateFile(size)).join(""); - const path = nameFile("large", "md", size); - const isTooLarge = harnessDownload.plugin.core.services.vault.isFileSizeTooLarge(size); - if (isTooLarge) { - const entry = await harnessDownload.plugin.core.localDatabase.getDBEntry(path as FilePath); - expect(entry).toBe(false); - } else { - await testFileRead(harnessDownload, path, content, fileOptions); - } - } - }); - - it("should have synchronised all binary files", async () => { - for (const size of fileSizeBins) { - const path = nameFile("binary", "bin", size); - const isTooLarge = harnessDownload.plugin.core.services.vault.isFileSizeTooLarge(size); - if (isTooLarge) { - const entry = await harnessDownload.plugin.core.localDatabase.getDBEntry(path as FilePath); - expect(entry).toBe(false); - } else { - const content = new Blob([...generateBinaryFile(size)], { type: "application/octet-stream" }); - await testFileRead(harnessDownload, path, content, fileOptions); - } - } - }); -}); diff --git a/test/suitep2p/syncp2p.p2p-up.test.ts b/test/suitep2p/syncp2p.p2p-up.test.ts deleted file mode 100644 index de5d5cef..00000000 --- a/test/suitep2p/syncp2p.p2p-up.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -/** - * P2P Replication Tests — Upload phase (process 1 of 2) - * - * Executed by run-p2p-tests.sh as the first vitest process. - * Writes files into the local DB, replicates them to the CLI host, - * then writes a handoff JSON so the download process knows what to verify. - * - * Trystero has module-level global state (occupiedRooms, didInit, etc.) - * that cannot be safely reused across upload→download within the same - * browser process. Running upload and download as separate vitest - * invocations gives each phase a fresh browser context. - */ -import { afterAll, beforeAll, describe, expect, it, test } from "vitest"; -import { generateHarness, waitForIdle, waitForReady, type LiveSyncHarness } from "../harness/harness"; -import { - PREFERRED_SETTING_SELF_HOSTED, - RemoteTypes, - type ObsidianLiveSyncSettings, - AutoAccepting, -} from "@vrtmrz/livesync-commonlib/compat/common/types"; -import { - DummyFileSourceInisialised, - FILE_SIZE_BINS, - FILE_SIZE_MD, - generateBinaryFile, - generateFile, -} from "../utils/dummyfile"; -import { checkStoredFileInDB, defaultFileOption, testFileWrite } from "../suite/db_common"; -import { delay } from "@vrtmrz/livesync-commonlib/compat/common/utils"; -import { closeReplication, performReplication } from "./sync_common_p2p"; -import { settingBase } from "../suite/variables"; - -const env = (import.meta as any).env; - -const ROOM_ID: string = env.P2P_TEST_ROOM_ID ?? "p2p-test-room"; -const PASSPHRASE: string = env.P2P_TEST_PASSPHRASE ?? "p2p-test-pass"; -const HOST_PEER_NAME: string = env.P2P_TEST_HOST_PEER_NAME ?? "p2p-cli-host"; -const RELAY: string = env.P2P_TEST_RELAY ?? "ws://localhost:4000/"; -const APP_ID: string = env.P2P_TEST_APP_ID ?? "self-hosted-livesync-vitest-p2p"; -const UPLOAD_PEER_NAME: string = env.P2P_TEST_UPLOAD_PEER_NAME ?? `p2p-upload-${Date.now()}`; -const UPLOAD_VAULT_NAME: string = env.P2P_TEST_UPLOAD_VAULT_NAME ?? `TestVaultUpload-${Date.now()}`; -// Path written by run-p2p-tests.sh; the download phase reads it back. -const HANDOFF_FILE: string = env.P2P_TEST_HANDOFF_FILE ?? "/tmp/p2p-test-handoff.json"; - -console.log("[P2P Up] ROOM_ID:", ROOM_ID, "HOST:", HOST_PEER_NAME, "RELAY:", RELAY, "APP_ID:", APP_ID); -console.log("[P2P Up] HANDOFF_FILE:", HANDOFF_FILE); - -const p2pSetting: ObsidianLiveSyncSettings = { - ...settingBase, - ...PREFERRED_SETTING_SELF_HOSTED, - showVerboseLog: true, - remoteType: RemoteTypes.REMOTE_P2P, - encrypt: true, - passphrase: PASSPHRASE, - usePathObfuscation: true, - P2P_Enabled: true, - P2P_AppID: APP_ID, - handleFilenameCaseSensitive: false, - P2P_AutoAccepting: AutoAccepting.ALL, - P2P_AutoBroadcast: true, - P2P_AutoStart: true, - P2P_passphrase: PASSPHRASE, - P2P_roomID: ROOM_ID, - P2P_relays: RELAY, - P2P_AutoAcceptingPeers: "~.*", - P2P_SyncOnReplication: HOST_PEER_NAME, -}; - -const fileOptions = defaultFileOption; -const nameFile = (type: string, ext: string, size: number) => `p2p-cli-test-${type}-file-${size}.${ext}`; - -/** Write the handoff JSON so the download phase knows which files to verify. */ -async function writeHandoff() { - const handoff = { - fileSizeMd: FILE_SIZE_MD, - fileSizeBins: FILE_SIZE_BINS, - }; - const { commands } = await import("@vitest/browser/context"); - await commands.writeHandoffFile(HANDOFF_FILE, JSON.stringify(handoff)); - console.log("[P2P Up] handoff written to", HANDOFF_FILE); -} - -describe("P2P Replication — Upload", () => { - let harnessUpload: LiveSyncHarness; - - const uploadSetting: ObsidianLiveSyncSettings = { - ...p2pSetting, - P2P_DevicePeerName: UPLOAD_PEER_NAME, - }; - - beforeAll(async () => { - await DummyFileSourceInisialised; - const vaultName = UPLOAD_VAULT_NAME; - console.log(`[P2P Up] BeforeAll - Vault: ${vaultName}`); - console.log(`[P2P Up] Peer name: ${UPLOAD_PEER_NAME}`); - harnessUpload = await generateHarness(vaultName, uploadSetting); - await waitForReady(harnessUpload); - expect(harnessUpload.plugin).toBeDefined(); - await waitForIdle(harnessUpload); - }); - - afterAll(async () => { - await closeReplication(harnessUpload); - await harnessUpload.dispose(); - await delay(1000); - }); - - it("should be instantiated and defined", () => { - expect(harnessUpload.plugin).toBeDefined(); - expect(harnessUpload.plugin.app).toBe(harnessUpload.app); - }); - - it("should have services initialized", () => { - expect(harnessUpload.plugin.core.services).toBeDefined(); - }); - - it("should have local database initialized", () => { - expect(harnessUpload.plugin.core.localDatabase).toBeDefined(); - expect(harnessUpload.plugin.core.localDatabase.isReady).toBe(true); - }); - - it("should create a file", async () => { - await testFileWrite(harnessUpload, nameFile("store", "md", 0), "Hello, World!", false, fileOptions); - }); - - it("should create several files with different content", async () => { - await testFileWrite(harnessUpload, nameFile("test-diff-1", "md", 0), "Content A", false, fileOptions); - await testFileWrite(harnessUpload, nameFile("test-diff-2", "md", 0), "Content B", false, fileOptions); - await testFileWrite(harnessUpload, nameFile("test-diff-3", "md", 0), "Content C", false, fileOptions); - }); - - test.each(FILE_SIZE_MD)("should create large md file of size %i bytes", async (size) => { - const content = Array.from(generateFile(size)).join(""); - const path = nameFile("large", "md", size); - const isTooLarge = harnessUpload.plugin.core.services.vault.isFileSizeTooLarge(size); - if (isTooLarge) { - expect(true).toBe(true); - } else { - await testFileWrite(harnessUpload, path, content, false, fileOptions); - } - }); - - test.each(FILE_SIZE_BINS)("should create binary file of size %i bytes", async (size) => { - const content = new Blob([...generateBinaryFile(size)], { type: "application/octet-stream" }); - const path = nameFile("binary", "bin", size); - await testFileWrite(harnessUpload, path, content, true, fileOptions); - const isTooLarge = harnessUpload.plugin.core.services.vault.isFileSizeTooLarge(size); - if (!isTooLarge) { - await checkStoredFileInDB(harnessUpload, path, content, fileOptions); - } - }); - - it("should replicate uploads to CLI host", async () => { - await performReplication(harnessUpload); - await performReplication(harnessUpload); - }); - - it("should write handoff file for download phase", async () => { - await writeHandoff(); - }); -}); diff --git a/test/suitep2p/syncp2p.test.ts b/test/suitep2p/syncp2p.test.ts deleted file mode 100644 index 416ab1f1..00000000 --- a/test/suitep2p/syncp2p.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -// Functional Test on Main Cases -// This test suite only covers main functional cases of synchronisation. Event handling, error cases, -// and edge, resolving conflicts, etc. will be covered in separate test suites. -import { describe } from "vitest"; -import { - PREFERRED_JOURNAL_SYNC, - PREFERRED_SETTING_SELF_HOSTED, - RemoteTypes, - type ObsidianLiveSyncSettings, -} from "@vrtmrz/livesync-commonlib/compat/common/types"; - -import { settingBase } from "../suite/variables.ts"; -import { defaultFileOption } from "../suite/db_common"; -import { syncBasicCase } from "../suite/sync.senario.basic.ts"; - -export const env = (import.meta as any).env; -function* generateCase() { - const sync_test_setting_base = settingBase; - const passpharse = "thetest-Passphrase3+9-for-e2ee!"; - const REMOTE_RECOMMENDED = { - [RemoteTypes.REMOTE_COUCHDB]: PREFERRED_SETTING_SELF_HOSTED, - [RemoteTypes.REMOTE_MINIO]: PREFERRED_JOURNAL_SYNC, - [RemoteTypes.REMOTE_P2P]: PREFERRED_SETTING_SELF_HOSTED, - }; - // const remoteTypes = [RemoteTypes.REMOTE_COUCHDB, RemoteTypes.REMOTE_MINIO, RemoteTypes.REMOTE_P2P]; - const remoteTypes = [RemoteTypes.REMOTE_P2P]; - // const e2eeOptions = [false, true]; - const e2eeOptions = [true]; - for (const remoteType of remoteTypes) { - for (const useE2EE of e2eeOptions) { - yield { - setting: { - ...sync_test_setting_base, - ...REMOTE_RECOMMENDED[remoteType], - remoteType, - encrypt: useE2EE, - passphrase: useE2EE ? passpharse : "", - usePathObfuscation: useE2EE, - } as ObsidianLiveSyncSettings, - }; - } - } -} - -describe("Replication Suite Tests (P2P)", async () => { - const cases = Array.from(generateCase()); - const fileOptions = defaultFileOption; - describe.each(cases)("Replication Tests - Remote: $setting.remoteType, E2EE: $setting.encrypt", ({ setting }) => { - syncBasicCase(`Remote: ${setting.remoteType}, E2EE: ${setting.encrypt}`, { setting, fileOptions }); - }); -}); diff --git a/test/testtest/dummyfile.test.ts b/test/testtest/dummyfile.test.ts deleted file mode 100644 index 32349963..00000000 --- a/test/testtest/dummyfile.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { writeFile } from "../utils/fileapi.vite"; -import { DummyFileSourceInisialised, generateBinaryFile, generateFile } from "../utils/dummyfile"; -import { describe, expect, it } from "vitest"; - -describe("Test File Teet", async () => { - await DummyFileSourceInisialised; - - it("should generate binary file correctly", async () => { - const size = 5000; - let generatedSize = 0; - const chunks: Uint8Array[] = []; - const generator = generateBinaryFile(size); - const blob = new Blob([...generator], { type: "application/octet-stream" }); - const buf = await blob.arrayBuffer(); - const hexDump = new Uint8Array(buf) - //@ts-ignore - .toHex() - .match(/.{1,32}/g) - ?.join("\n"); - const secondDummy = generateBinaryFile(size); - const secondBlob = new Blob([...secondDummy], { type: "application/octet-stream" }); - const secondBuf = await secondBlob.arrayBuffer(); - const secondHexDump = new Uint8Array(secondBuf) - //@ts-ignore - .toHex() - .match(/.{1,32}/g) - ?.join("\n"); - if (hexDump !== secondHexDump) { - throw new Error("Generated binary files do not match"); - } - expect(hexDump).toBe(secondHexDump); - // await writeFile("test/testtest/dummyfile.test.bin", buf); - // await writeFile("test/testtest/dummyfile.test.bin.hexdump.txt", hexDump || ""); - }); - it("should generate text file correctly", async () => { - const size = 25000; - let generatedSize = 0; - let content = ""; - const generator = generateFile(size); - const out = [...generator]; - // const blob = new Blob(out, { type: "text/plain" }); - content = out.join(""); - - const secondDummy = generateFile(size); - const secondOut = [...secondDummy]; - const secondContent = secondOut.join(""); - if (content !== secondContent) { - throw new Error("Generated text files do not match"); - } - expect(content).toBe(secondContent); - // await writeFile("test/testtest/dummyfile.test.txt", await blob.text()); - }); -}); diff --git a/test/unit/dialog.test.ts b/test/unit/dialog.test.ts deleted file mode 100644 index 8ebbf12c..00000000 --- a/test/unit/dialog.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -// Dialog Unit Tests -import { beforeAll, describe, expect, it } from "vitest"; -import { commands } from "vitest/browser"; - -import { generateHarness, waitForIdle, waitForReady, type LiveSyncHarness } from "../harness/harness"; -import { ChunkAlgorithms, DEFAULT_SETTINGS, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types"; - -import { DummyFileSourceInisialised } from "../utils/dummyfile"; - -import { page } from "vitest/browser"; -import { DoctorRegulation } from "@vrtmrz/livesync-commonlib/compat/common/configForDoc"; -import { waitForDialogHidden, waitForDialogShown } from "../lib/ui"; -const env = (import.meta as any).env; -const dialog_setting_base = { - ...DEFAULT_SETTINGS, - isConfigured: true, - handleFilenameCaseSensitive: false, - couchDB_URI: `${env.hostname}`, - couchDB_DBNAME: `${env.dbname}`, - couchDB_USER: `${env.username}`, - couchDB_PASSWORD: `${env.password}`, - bucket: `${env.bucketName}`, - region: "us-east-1", - endpoint: `${env.minioEndpoint}`, - accessKey: `${env.accessKey}`, - secretKey: `${env.secretKey}`, - useCustomRequestHandler: true, - forcePathStyle: true, - bucketPrefix: "", - usePluginSyncV2: true, - chunkSplitterVersion: ChunkAlgorithms.RabinKarp, - doctorProcessedVersion: DoctorRegulation.version, - notifyThresholdOfRemoteStorageSize: 800, -} as ObsidianLiveSyncSettings; - -function checkDialogVisibility(dialogText: string, shouldBeVisible: boolean): void { - const dialog = page.getByText(dialogText); - expect(dialog).toHaveClass(/modal-title/); - if (!shouldBeVisible) { - expect(dialog).not.toBeVisible(); - } else { - expect(dialog).toBeVisible(); - } - return; -} -function checkDialogShown(dialogText: string) { - checkDialogVisibility(dialogText, true); -} -function checkDialogHidden(dialogText: string) { - checkDialogVisibility(dialogText, false); -} - -describe("Dialog Tests", async () => { - // describe.each(cases)("Replication Tests - Remote: $setting.remoteType, E2EE: $setting.encrypt", ({ setting }) => { - const setting = dialog_setting_base; - beforeAll(async () => { - await DummyFileSourceInisialised; - await commands.grantClipboardPermissions(); - }); - let harness: LiveSyncHarness; - const vaultName = "TestVault" + Date.now(); - beforeAll(async () => { - harness = await generateHarness(vaultName, setting); - await waitForReady(harness); - expect(harness.plugin).toBeDefined(); - expect(harness.plugin.app).toBe(harness.app); - await waitForIdle(harness); - }); - it("should show copy to clipboard dialog and confirm", async () => { - const testString = "This is a test string to copy to clipboard."; - const title = "Copy Test"; - const result = harness.plugin.core.services.UI.promptCopyToClipboard(title, testString); - const isDialogShown = await waitForDialogShown(title, 500); - expect(isDialogShown).toBe(true); - const copyButton = page.getByText("📋"); - expect(copyButton).toBeDefined(); - expect(copyButton).toBeVisible(); - await copyButton.click(); - const copyResultButton = page.getByText("✔️"); - expect(copyResultButton).toBeDefined(); - expect(copyResultButton).toBeVisible(); - const clipboardText = await navigator.clipboard.readText(); - expect(clipboardText).toBe(testString); - const okButton = page.getByText("OK"); - expect(okButton).toBeDefined(); - expect(okButton).toBeVisible(); - await okButton.click(); - const resultValue = await result; - expect(resultValue).toBe(true); - // Check that the dialog is closed - const isDialogHidden = await waitForDialogHidden(title, 500); - expect(isDialogHidden).toBe(true); - }); -}); diff --git a/test/utils/dummyfile.ts b/test/utils/dummyfile.ts deleted file mode 100644 index 66cf6878..00000000 --- a/test/utils/dummyfile.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { DEFAULT_SETTINGS } from "@vrtmrz/livesync-commonlib/compat/common/types"; -import { readFile } from "../utils/fileapi.vite.ts"; -let charset = ""; -export async function init() { - console.log("Initializing dummyfile utils..."); - - charset = (await readFile("test/utils/testcharvariants.txt")).toString(); - console.log(`Loaded charset of length ${charset.length}`); - console.log(charset); -} -export const DummyFileSourceInisialised = init(); -function* indexer(range: number = 1000, seed: number = 0): Generator { - let t = seed | 0; - while (true) { - t = (t + 0x6d2b79f5) | 0; - let z = t; - z = Math.imul(z ^ (z >>> 15), z | 1); - z ^= z + Math.imul(z ^ (z >>> 7), z | 61); - const float = ((z ^ (z >>> 14)) >>> 0) / 4294967296; - yield Math.floor(float * range); - } -} - -export function* generateFile(size: number): Generator { - const chunkSourceStr = charset; - const chunkStore = [...chunkSourceStr]; // To support indexing avoiding multi-byte issues - const bufSize = 1024; - let buf = ""; - let generated = 0; - const indexGen = indexer(chunkStore.length); - while (generated < size) { - const f = indexGen.next().value; - buf += chunkStore[f]; - generated += 1; - if (buf.length >= bufSize) { - yield buf; - buf = ""; - } - } - if (buf.length > 0) { - yield buf; - } -} -export function* generateBinaryFile(size: number): Generator> { - let generated = 0; - const pattern = Array.from({ length: 256 }, (_, i) => i); - const indexGen = indexer(pattern.length); - const bufSize = 1024; - const buf = new Uint8Array(bufSize); - let bufIdx = 0; - while (generated < size) { - const f = indexGen.next().value; - buf[bufIdx] = pattern[f]; - bufIdx += 1; - generated += 1; - if (bufIdx >= bufSize) { - yield buf; - bufIdx = 0; - } - } - if (bufIdx > 0) { - yield buf.subarray(0, bufIdx); - } -} - -// File size for markdown test files (10B to 1MB, roughly logarithmic scale) -export const FILE_SIZE_MD = [10, 100, 1000, 10000, 100000, 1000000]; -// File size for test files (10B to 40MB, roughly logarithmic scale) -export const FILE_SIZE_BINS = [ - 10, - 100, - 1000, - 50000, - 100000, - 5000000, - DEFAULT_SETTINGS.syncMaxSizeInMB * 1024 * 1024 + 1, -]; diff --git a/test/utils/fileapi.vite.ts b/test/utils/fileapi.vite.ts deleted file mode 100644 index e84f5bde..00000000 --- a/test/utils/fileapi.vite.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { server } from "vitest/browser"; -const { readFile, writeFile } = server.commands; -export { readFile, writeFile }; diff --git a/test/utils/testcharvariants.txt b/test/utils/testcharvariants.txt deleted file mode 100644 index b517a531..00000000 --- a/test/utils/testcharvariants.txt +++ /dev/null @@ -1,17 +0,0 @@ -國破山河在,城春草木深。 -感時花濺淚,恨別鳥驚心。 -烽火連三月,家書抵萬金。 -白頭搔更短,渾欲不勝簪。 -«Nel mezzo del cammin di nostra vita -mi ritrovai per una selva oscura, -ché la diritta via era smarrita.» -Духовной жаждою томим, -В пустыне мрачной я влачился, — -И шестикрылый серафим -На перепутье мне явился. -Shall I compare thee to a summer’s day? -Thou art more lovely and more temperate: -Rough winds do shake the darling buds of May, -And summer’s lease hath all too short a date: - -📜🖋️ 🏺 🏛️ 春望𠮷‌ché🇷🇺Аa‮RTLO🏳️‍🌈👨‍👩‍👧‍👦lʼanatraアイウエオ \ No newline at end of file diff --git a/updates.md b/updates.md index 165cc305..8321536f 100644 --- a/updates.md +++ b/updates.md @@ -19,6 +19,8 @@ Earlier releases remain available in the [0.25 release history](docs/releases/0. ### Improved - Removed the ineffective 'Use the trash bin' toggle from the settings interface. Remote deletions continue to follow Obsidian's deletion preference, while the legacy setting key remains accepted for compatibility. +- Kept content-derived chunk revisions permanently enabled, as they have been since 0.25.6, and removed the obsolete stored key from recommendations, database-maintenance prerequisites, and review tooling. +- Aligned new-Vault, full-reset, and CLI-generated settings with the 1.0 recommendations. New Vaults use cross-platform case-insensitive file-name handling, while an existing Vault with no saved case policy remains paused until case-sensitive legacy behaviour is explicitly retained or a case-insensitive database rebuild is planned. - Improved P2P restart and settings-reapplication handling by serialising transport start and stop, keeping event handlers bound to the active replicator, and using one package-owned Trystero transport generation. - Kept the release history available in the settings while removing automatic unread-version tracking and redirection. Release versions are no longer treated as a data-compatibility signal. - Internal database and settings compatibility reviews now use a dedicated explanation and an explicit, case-specific resume action. They block replication without rewriting the user's automatic synchronisation choices, and older installations cannot dismiss state created by a newer version. @@ -37,6 +39,7 @@ Earlier releases remain available in the [0.25 release history](docs/releases/0. ### Testing +- Retired the obsolete mocked browser Harness, its root-level P2P relay helpers, and its manually dispatched `harness-ci` workflow. Unit and integration suites, the CLI two-Vault and Compose P2P scenarios, and real Obsidian E2E now own the maintained verification paths. - Added packed-package and downstream checks for Commonlib entry points, including isolated Node and browser File System Access API storage implementations. - Added reusable Context result contracts for Obsidian, CLI, and Webapp compositions, including a real-Obsidian smoke assertion that every service retains the host-provided Context. - Added Commonlib stream-contract tests and downstream CLI unit and Deno E2E coverage for injected text, binary, prompt, and error channels. diff --git a/vitest.config.p2p.ts b/vitest.config.p2p.ts deleted file mode 100644 index 9478db52..00000000 --- a/vitest.config.p2p.ts +++ /dev/null @@ -1,107 +0,0 @@ -/** - * @file vitest.config.p2p.ts - * @description Configuration for running browser-based Peer-to-Peer (P2P) replication tests - * in Playwright (Chromium) using Trystero and Nostr relays. - * This is executed via the `npm run test:p2p` command (which runs `test/suitep2p/run-p2p-tests.sh` internally). - */ -import { defineConfig, mergeConfig } from "vitest/config"; -import { playwright } from "@vitest/browser-playwright"; -import viteConfig from "./vitest.config.common"; -import path from "path"; -import { existsSync, readFileSync } from "node:fs"; -import { parseEnv } from "node:util"; -import { grantClipboardPermissions, writeHandoffFile, readHandoffFile } from "./test/lib/commands"; - -// P2P test environment variables -// Configure these in .env or .test.env, or inject via shell before running tests. -// Shell-injected values take precedence over dotenv files. -// -// Required: -// P2P_TEST_ROOM_ID - Shared room identifier for peers to discover each other -// P2P_TEST_PASSPHRASE - Encryption passphrase shared between test peers -// -// Optional: -// P2P_TEST_HOST_PEER_NAME - Name used to identify the host peer (default varies) -// P2P_TEST_RELAY - Nostr relay server URL used for peer signalling/discovery -// P2P_TEST_APP_ID - Application ID scoping the P2P session -// P2P_TEST_HANDOFF_FILE - File path used to pass state between up/down test phases -// -// General test options (also read from env): -// ENABLE_DEBUGGER - Set to "true" to attach a debugger and pause before tests -// ENABLE_UI - Set to "true" to open a visible browser window during tests -const loadEnvFile = (path: string) => (existsSync(path) ? parseEnv(readFileSync(path, "utf-8")) : undefined); -const defEnv = loadEnvFile(".env"); -const testEnv = loadEnvFile(".test.env"); -// Merge: dotenv files < process.env (so shell-injected vars like P2P_TEST_* take precedence) -const p2pEnv: Record = {}; -if (process.env.P2P_TEST_ROOM_ID) p2pEnv.P2P_TEST_ROOM_ID = process.env.P2P_TEST_ROOM_ID; -if (process.env.P2P_TEST_PASSPHRASE) p2pEnv.P2P_TEST_PASSPHRASE = process.env.P2P_TEST_PASSPHRASE; -if (process.env.P2P_TEST_HOST_PEER_NAME) p2pEnv.P2P_TEST_HOST_PEER_NAME = process.env.P2P_TEST_HOST_PEER_NAME; -if (process.env.P2P_TEST_RELAY) p2pEnv.P2P_TEST_RELAY = process.env.P2P_TEST_RELAY; -if (process.env.P2P_TEST_APP_ID) p2pEnv.P2P_TEST_APP_ID = process.env.P2P_TEST_APP_ID; -if (process.env.P2P_TEST_HANDOFF_FILE) p2pEnv.P2P_TEST_HANDOFF_FILE = process.env.P2P_TEST_HANDOFF_FILE; -const env = Object.assign({}, defEnv, testEnv, p2pEnv); -const debuggerEnabled = env?.ENABLE_DEBUGGER === "true"; -const enableUI = env?.ENABLE_UI === "true"; -const headless = !debuggerEnabled && !enableUI; - -export default mergeConfig( - viteConfig, - defineConfig({ - resolve: { - alias: { - obsidian: path.resolve(__dirname, "./test/harness/obsidian-mock.ts"), - }, - }, - test: { - env: env, - testTimeout: 240000, - hookTimeout: 240000, - fileParallelism: false, - isolate: true, - watch: false, - // Run all CLI-host P2P test files (*.p2p.test.ts, *.p2p-up.test.ts, *.p2p-down.test.ts) - include: ["test/suitep2p/**/*.p2p*.test.ts"], - browser: { - isolate: true, - // Only grantClipboardPermissions is needed; no openWebPeer/acceptWebPeer - commands: { - grantClipboardPermissions, - writeHandoffFile, - readHandoffFile, - }, - provider: playwright({ - launchOptions: { - args: [ - "--js-flags=--expose-gc", - "--allow-insecure-localhost", - "--disable-web-security", - "--ignore-certificate-errors", - ], - }, - }), - enabled: true, - screenshotFailures: false, - instances: [ - { - execArgv: ["--js-flags=--expose-gc"], - browser: "chromium", - headless, - isolate: true, - inspector: debuggerEnabled ? { waitForDebugger: true, enabled: true } : undefined, - printConsoleTrace: true, - onUnhandledError(error) { - const msg = error.message || ""; - if (msg.includes("Cannot create so many PeerConnections")) { - return false; - } - }, - }, - ], - headless, - fileParallelism: false, - ui: debuggerEnabled || enableUI ? true : false, - }, - }, - }) -); diff --git a/vitest.config.ts b/vitest.config.ts deleted file mode 100644 index 6b632b72..00000000 --- a/vitest.config.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * @file vitest.config.ts - * @description Configuration for running browser-based end-to-end (E2E) integration tests - * using Playwright (Chromium) to test replication and synchronisation scenarios. - * This is executed when running the full test suite via `npm run test` or `npm run test:full`. - */ -import { defineConfig, mergeConfig } from "vitest/config"; -import { playwright } from "@vitest/browser-playwright"; -import viteConfig from "./vitest.config.common"; -import path from "path"; -import { existsSync, readFileSync } from "node:fs"; -import { parseEnv } from "node:util"; -import { grantClipboardPermissions, openWebPeer, closeWebPeer, acceptWebPeer } from "./test/lib/commands"; - -const loadEnvFile = (path: string) => (existsSync(path) ? parseEnv(readFileSync(path, "utf-8")) : undefined); -const defEnv = loadEnvFile(".env"); -const testEnv = loadEnvFile(".test.env"); -const env = Object.assign({}, defEnv, testEnv); -const debuggerEnabled = env?.ENABLE_DEBUGGER === "true"; -const enableUI = env?.ENABLE_UI === "true"; -const headless = !debuggerEnabled && !enableUI; -export default mergeConfig( - viteConfig, - defineConfig({ - resolve: { - alias: { - obsidian: path.resolve(__dirname, "./test/harness/obsidian-mock.ts"), - }, - }, - test: { - env: env, - testTimeout: 40000, - hookTimeout: 50000, - fileParallelism: false, - isolate: true, - watch: false, - - // environment: "browser", - include: ["test/**/*.test.ts"], - coverage: { - include: ["src/**/*.ts", "src/**/*.svelte"], - exclude: ["**/*.test.ts"], - provider: "v8", - reporter: ["text", "json", "html"], - // ignoreEmptyLines: true, - }, - browser: { - isolate: true, - commands: { - grantClipboardPermissions, - openWebPeer, - closeWebPeer, - acceptWebPeer, - }, - provider: playwright({ - launchOptions: { - args: ["--js-flags=--expose-gc"], - // chromiumSandbox: true, - }, - }), - enabled: true, - screenshotFailures: false, - instances: [ - { - execArgv: ["--js-flags=--expose-gc"], - browser: "chromium", - headless, - isolate: true, - inspector: debuggerEnabled - ? { - waitForDebugger: true, - enabled: true, - } - : undefined, - printConsoleTrace: debuggerEnabled, - onUnhandledError(error) { - // Ignore certain errors - const msg = error.message || ""; - if (msg.includes("Cannot create so many PeerConnections")) { - return false; - } - }, - }, - ], - headless, - fileParallelism: false, - ui: debuggerEnabled || enableUI ? true : false, - }, - }, - }) -);