Skip to content

sync + coverage: measure every suite, and open the app at desktop size - #76

Open
alichherawalla wants to merge 128 commits into
mainfrom
release/sync-cross-platform
Open

sync + coverage: measure every suite, and open the app at desktop size#76
alichherawalla wants to merge 128 commits into
mainfrom
release/sync-cross-platform

Conversation

@alichherawalla

@alichherawalla alichherawalla commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

The desktop side of the same story: your Mac joins the Personal Mesh, so chats, files, clipboard and models move between it and your phone over your own network. Plus a portable backup you own, and the coverage plumbing that makes the e2e tour count.

106 commits, 172 files, +10,551 / -1,107. Pairs with desktop-pro#39, which carries the private half.

What this gives you

Chat that reflects the mesh as it happens. A message synced from another device appears when it arrives, not when something else triggers a reload. The streaming reply is published as one observable snapshot, so the window cannot render half a state. Empty synced thinking placeholders stay hidden, and the conversation list shows the last message under each chat.

A backup you can carry. A portable desktop archive engine plus the settings to drive it: your data leaves in a format you own, and comes back.

Model transfers you can verify. Device transfers are registered and symlinked transfer files are rejected outright rather than followed.

Projects that stop losing work. Deleting a project preserves its chats; assigning one refreshes them. Short non-empty documents are indexed instead of silently skipped.

Devices beyond macOS. The Devices surface is available on Windows too, not only macOS. Nested screen history is retained, so going back does what you expect.

Licensing that behaves at the cap. Cached access is verified at launch, activation outcomes are rendered from the shared result rather than re-derived, the stalest seat is replaced when you hit the device cap, and there is a local Pro reset.

Verification

  • 429 test files, 4,053 tests passing (1 skipped, 2 skipped tests) via npm run test:coverage, which is the same gate the pre-push hook runs.
  • The full Playwright tour runs on the built app in CI under xvfb. Screenshots and the e2e coverage report are uploaded as run artifacts (e2e-screenshots, e2e-coverage); the tour remains advisory while headless-Electron launch stability on the ubuntu runner is the open question, which is recorded in docs/GAPS_BACKLOG.md along with the note that it once hid four real spec defects.
  • OFFGRID_E2E_COVERAGE makes the tour count: 25 specs drive the real app, devices-sync alone stands up a synthetic peer with a real SyncEngine, and none of it reached a coverage report before because Playwright launches Electron as its own process.
  • Coverage floor is 80 on every metric, set deliberately. It was 85, and pro branches sat on the line: 85.5% locally but ~85.2% as CI measures it, because CI legitimately skips the native-dependency ambient journeys. A 0.3% environment swing decided whether the gate was red, which is a gate reporting the runner rather than the code. Statements, functions and lines measure 91-93% in pro and higher in core, so 80 loosens branches alone.

One ci job

This repo used to report two checks. e2e was a second parallel job whose first nine steps were byte-identical duplicates of verify: the same pro checkout with its main fallback, the same shared monorepo provisioning, a second npm ci, all to feed a second runner. Those are deleted, and typecheck, tests with the coverage floor, dependency boundaries, lint and the e2e tour now report as one ci check.

The tour runs after the unit gates instead of beside them, so a PR takes about 7 minutes longer to go green. An earlier attempt at this was reverted for blowing a "25-minute job cap" (PR #68); that cap was this workflow's own timeout-minutes, so it is raised to 50 deliberately rather than worked around.

Note for anyone with branch protection: the check renames from verify to ci.

Tests worth calling out

approval-lifecycle.ts went from 0% coverage to covered. Its only tests were .dbtest.ts files, which the default vitest project excludes because they need better-sqlite3 rebuilt for the node ABI, and which CI never runs at all, so the highest-stakes logic in the "act" pillar was both measured as untested and actually untested in CI. It now runs against a real in-memory SQLite with the real approval queue, audit log, execution claim and preference store; only the MCP connector call is stood in for, because that is a separate process reaching a third-party API and the only way to observe whether it actually acted:

  • approving acts ONCE, and a second approval of the same row does not send a second email. Two clicks on Approve is the most ordinary thing a user does when a UI takes a moment.
  • a missing connector, or saved arguments that no longer parse, are RECORDED as failures with a reason rather than acted on or left sitting in the queue looking sent.
  • a 5,000-character connector response does not become the audit log.
  • rejecting learns from the reason once, and learns nothing from an empty one.

The desktop macos-proximity suite now stands up a real file at the bin resolver's path instead of mocking ../lib/bin-resolution, verified by hiding the gitignored binary, which is the CI condition.

Known gaps, recorded not hidden

docs/GAPS_BACKLOG.md, notably: 103 DB tests never run in CI (excluded from the default project, no test:db step), which is why a file with tests measured as 0%; and the ChatScreen journeys left uncovered by deleting a 155-case suite that stubbed fourteen of our own modules, with the measured 8-point statement drop and the four named journeys that now want rendered tests.

Replaces #75, which GitHub closed when the branch was renamed to release/sync-cross-platform.

Greptile Summary

The PR adds cross-device synchronization, portable backup and restore, desktop presentation updates, licensing changes, and expanded unit, database, and Electron coverage.

  • Adds backup archive creation and additive restoration for chats and knowledge documents.
  • Integrates real-time mesh updates and model-transfer registration.
  • Consolidates verification into one CI job with database journeys and advisory Electron coverage.
  • Expands desktop navigation, settings, permission, and Pro-feature integration surfaces.

Confidence Score: 4/5

The PR does not yet appear safe to merge because additive restore can still silently omit distinct archived messages that collide under its content-derived identity.

The reply says the archived-message omission was fixed by deriving identity from conversation, role, text, and timestamp, but the current implementation compares a second-precision timestamp and therefore treats two legitimate identical messages created in the same second as one, leaving the original incomplete-history failure reachable.

Files Needing Attention: src/main/backup/data-port.ts

Important Files Changed

Filename Overview
src/main/backup/data-port.ts Adds portable backup export and additive restore, including precomputed chunk embeddings and content-derived message deduplication.
src/main/backup/archive.ts Implements archive serialization and validation for portable desktop backups.
src/main/backup/ipc.ts Exposes backup and restore operations through the desktop IPC boundary.
src/main/sync-mutation.ts Publishes local mutations into the synchronization layer.
src/main/chat-stream-state.ts Centralizes observable chat-stream snapshots for consistent renderer updates.
.github/workflows/ci.yml Consolidates verification and adds database journeys plus advisory Electron coverage collection.

Sequence Diagram

sequenceDiagram
  participant Archive as Backup archive
  participant Restore as DesktopDataPort
  participant Embedder as RAG embedder
  participant DB as SQLite
  Archive->>Restore: Validated chats, messages, documents, chunks
  Restore->>Embedder: Compute chunk vectors before transaction
  Embedder-->>Restore: Embeddings or null fallback
  Restore->>DB: Begin additive restore
  Restore->>DB: Merge conversations and messages
  Restore->>DB: Insert documents and chunks
  Restore->>DB: Commit restored data
Loading

Reviews (3): Last reviewed commit: "test(e2e): stop the Devices specs failin..." | Re-trigger Greptile

M0 of docs/SYNC_INTEGRATION_PLAN.md. Brings the public sync engine in so the pro integration can
consume it; no behaviour wired yet.

- Vendors shared/packages/sync -> desktop/packages/sync following the existing convention
  (@offgrid/clipboard|design|models|rag are git-tracked copies consumed via file: deps). Records
  provenance (offgridVendoredFrom: commit 9b671b5) in the vendored package.json, because the
  existing copies have silently DRIFTED from shared/ and that should be visible.
- Adds runtime deps the engine needs: bonjour-service (pure-JS mDNS, no native build, used by the
  node-discovery adapter), tweetnacl, tweetnacl-util, js-sha512.
- Engine is consumed UNCHANGED: the mobile lane is working in the same package and two sessions
  editing it is the one guaranteed conflict. Engine changes go through the plan's 'Engine asks'.

Gate: 24/24 package tests pass from the vendored copy; tsc clean on tsconfig.node.json and
tsconfig.web.json; ./node + ./node-discovery subpath exports resolve (NodeTcpTransport,
NodeDiscovery).

Plan correction in the same commit: two engine asks were withdrawn after checking the real build.
Streaming/HTTP transfer (createFileRequestStreaming/Http, createFileCompleteStreaming,
verifyFileIntegrity) and ACK (createFileAck) already exist, so large-model transfer is NOT blocked
on the other lane — those are host-wiring rules this lane owns instead.
…e the engine directly

Two things, both prerequisites for cross-device message sync.

1) CORE SCHEMA — rag_messages.uuid (src/main/database.ts)
   rag_messages.id is INTEGER AUTOINCREMENT and therefore DEVICE-LOCAL. Live sync keys records by
   (entity, entityId) across devices, so device A's row 7 and device B's row 7 would look like the
   SAME message and silently overwrite each other. The autoincrement id stays the local primary key;
   uuid is the cross-device identity. Includes a JS backfill for existing profiles (SQLite has no
   uuid()), a UNIQUE index so a replayed remote op upserts instead of duplicating, and uuid on every
   new insert. Mobile adds the equivalent to its message store under the same names.

   Verified by src/main/__tests__/rag-message-uuid.dbtest.ts against a REAL legacy profile on disk
   (4/4): the column appears, every pre-existing row is backfilled with a DISTINCT uuid, uniqueness
   is enforced, the production writer populates it, and the migration is IDEMPOTENT — rewriting
   uuids on each launch would orphan the record on every other device.

2) CORRECTION — stop vendoring @offgrid/sync
   M0 vendored shared/packages/sync into desktop/packages/sync, copying the existing
   @offgrid/clipboard|design|models|rag convention. shared/docs/DESKTOP_SYNC_INTEGRATION_PLAN.md §1
   says explicitly NOT to duplicate this package: reference it directly, as mobile does. Now
   'file:../shared/packages/sync'; the copy is removed and the plan doc records the correction.
   (The other desktop/packages/* copies have already silently drifted from shared/, which is the
   argument for the direct ref.)

Full suite: 377 files / 3084 tests. The renderer integration failures seen while running this are
load-dependent flakes, not regressions — a clean tree fails a DIFFERENT test, and all five pass in
isolation with these changes applied.
Same review point as mobile's: `npm ci || npm install` masked a lock mismatch by resolving a
different dependency graph, then let typecheck, tests and e2e run against dependencies nobody
committed. The drift it existed for is fixed in shared, so both jobs now do a locked install and a
future drift stops the build instead of quietly changing what is being tested.
@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 151 files, which is 51 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7cf2db76-bab6-40c8-a158-73cc898ba48b

📥 Commits

Reviewing files that changed from the base of the PR and between efcb0e9 and 3187f50.

⛔ Files ignored due to path filters (32)
  • e2e/screenshots/devices-free-upgrade.png is excluded by !**/*.png
  • e2e/screenshots/devices-model-transfer.png is excluded by !**/*.png
  • e2e/screenshots/devices-pro.png is excluded by !**/*.png
  • e2e/screenshots/devices-state-sync.png is excluded by !**/*.png
  • e2e/screenshots/devices-sync-settings.png is excluded by !**/*.png
  • e2e/screenshots/onboarding-pro-grid.png is excluded by !**/*.png
  • e2e/screenshots/packaged-app-shell.png is excluded by !**/*.png
  • e2e/screenshots/qh-integrations-byo-google-setup.png is excluded by !**/*.png
  • e2e/screenshots/qh-integrations-overview.png is excluded by !**/*.png
  • e2e/screenshots/qh-models-fit-chip.png is excluded by !**/*.png
  • e2e/screenshots/qh-replay-not-reached.png is excluded by !**/*.png
  • e2e/screenshots/qh-settings-capture-optin.png is excluded by !**/*.png
  • e2e/screenshots/qh-settings-model-pipeline.png is excluded by !**/*.png
  • e2e/screenshots/settings-collapsed-again.png is excluded by !**/*.png
  • e2e/screenshots/settings-collapsed.png is excluded by !**/*.png
  • e2e/screenshots/settings-opening-midframe.png is excluded by !**/*.png
  • e2e/screenshots/settings-section-open.png is excluded by !**/*.png
  • e2e/screenshots/smoke-chat.png is excluded by !**/*.png
  • e2e/screenshots/smoke-clipboard.png is excluded by !**/*.png
  • e2e/screenshots/smoke-entities.png is excluded by !**/*.png
  • e2e/screenshots/smoke-gateway.png is excluded by !**/*.png
  • e2e/screenshots/smoke-integrations.png is excluded by !**/*.png
  • e2e/screenshots/smoke-models.png is excluded by !**/*.png
  • e2e/screenshots/smoke-replay.png is excluded by !**/*.png
  • e2e/screenshots/smoke-settings.png is excluded by !**/*.png
  • e2e/screenshots/smoke-vault.png is excluded by !**/*.png
  • e2e/screenshots/smoke-voice.png is excluded by !**/*.png
  • package-lock.json is excluded by !**/package-lock.json
  • packages/rag/dist/index.d.mts is excluded by !**/dist/**
  • packages/rag/dist/index.d.ts is excluded by !**/dist/**
  • packages/rag/dist/index.js is excluded by !**/dist/**
  • packages/rag/dist/index.mjs is excluded by !**/dist/**
📒 Files selected for processing (151)
  • .coderabbit.yaml
  • .github/workflows/ci.yml
  • .gitignore
  • .sonarcloud.properties
  • AGENTS.md
  • CLAUDE.md
  • ROADMAP_DESKTOP.md
  • docs/GAPS_BACKLOG.md
  • docs/SYNC_INTEGRATION_PLAN.md
  • e2e/chat-actions.spec.ts
  • e2e/chat-large-collection.spec.ts
  • e2e/chat-memory.spec.ts
  • e2e/desktop-polish.spec.ts
  • e2e/devices-sync.spec.ts
  • e2e/helpers/launch.ts
  • e2e/onboarding-resume.spec.ts
  • e2e/projects-layout.spec.ts
  • e2e/screenshots-quality-hardening.spec.ts
  • e2e/smoke.spec.ts
  • e2e/tour.spec.ts
  • electron-builder.yml
  • electron.vite.config.ts
  • integration-tests/concurrent-workload-recovery.integration.dbtest.ts
  • integration-tests/memory-chat-tts.ui.integration.dbtest.ts
  • integration-tests/permission-recovery.test.ts
  • integration-tests/workspace-production-bridge.ui.integration.dbtest.tsx
  • package.json
  • packages/rag/src/bridges.ts
  • packages/rag/src/chunking.ts
  • packages/rag/src/index.ts
  • packages/rag/src/service.ts
  • packages/rag/src/types.ts
  • pro/main/licensing/__tests__/device-fingerprint.test.ts
  • pro/main/licensing/__tests__/keygen-parse.test.ts
  • pro/main/licensing/__tests__/keygen-validate.integration.test.ts
  • pro/main/licensing/__tests__/license-cache.test.ts
  • pro/main/licensing/__tests__/license-logic.test.ts
  • pro/main/licensing/__tests__/license-seat-replacement.integration.test.ts
  • scripts/coverage-all.sh
  • scripts/e2e-on-box.sh
  • scripts/hooks/pre-push
  • scripts/physical-sync/README.md
  • scripts/physical-sync/desktopKnowledgeSyncAdapter.mjs
  • scripts/physical-sync/iosMacKnowledgeSync.mjs
  • scripts/seed-e2e-license.mjs
  • scripts/stage-pro-native.mjs
  • scripts/test-db.sh
  • src/main/__tests__/chat-stream-state.test.ts
  • src/main/__tests__/desktop-backup-roundtrip.integration.dbtest.ts
  • src/main/__tests__/fresh-setup-first-use.integration.dbtest.ts
  • src/main/__tests__/harness/db-teardown.ts
  • src/main/__tests__/image-generation-job-owner.integration.test.ts
  • src/main/__tests__/knowledge-document-sync-owner.integration.dbtest.ts
  • src/main/__tests__/local-network-permission.integration.test.ts
  • src/main/__tests__/macos-artifact-integrity.integration.test.ts
  • src/main/__tests__/model-download-tts.integration.dbtest.ts
  • src/main/__tests__/permissions.test.ts
  • src/main/__tests__/project-delete-cascade.dbtest.ts
  • src/main/__tests__/rag-ipc-project-create.dbtest.ts
  • src/main/__tests__/rag-message-uuid.dbtest.ts
  • src/main/__tests__/shutdown-registry.test.ts
  • src/main/__tests__/sync-knowledge-document.test.ts
  • src/main/__tests__/sync-mutation-hooks.dbtest.ts
  • src/main/__tests__/system-status-ipc.test.ts
  • src/main/backup/__tests__/archive.test.ts
  • src/main/backup/__tests__/data-port.test.ts
  • src/main/backup/__tests__/file-mapper.test.ts
  • src/main/backup/__tests__/sink.test.ts
  • src/main/backup/archive.ts
  • src/main/backup/data-port.ts
  • src/main/backup/file-mapper.ts
  • src/main/backup/index.ts
  • src/main/backup/ipc.ts
  • src/main/backup/sink.ts
  • src/main/backup/types.ts
  • src/main/bootstrap/__tests__/window-presentation.test.ts
  • src/main/bootstrap/hookRegistry.ts
  • src/main/bootstrap/loadProFeaturesMain.ts
  • src/main/bootstrap/window-presentation.ts
  • src/main/chat-stream-state.ts
  • src/main/database.ts
  • src/main/imagegen/job-service.ts
  • src/main/index.ts
  • src/main/ipc.ts
  • src/main/license-ipc.ts
  • src/main/licensing/device-fingerprint.ts
  • src/main/licensing/keygen-client.ts
  • src/main/licensing/keygen-config.ts
  • src/main/licensing/license-cache.ts
  • src/main/licensing/license-service.ts
  • src/main/llm.ts
  • src/main/llm/__tests__/settings-sync.integration.test.ts
  • src/main/models-manager.ts
  • src/main/models/__tests__/model-transfer-registration.integration.test.ts
  • src/main/permissions.ts
  • src/main/rag-ipc.ts
  • src/main/rag/index.ts
  • src/main/rag/store.ts
  • src/main/shutdown.ts
  • src/main/stream-guards.ts
  • src/main/sync-knowledge-document.ts
  • src/main/sync-mutation.ts
  • src/main/sync-shared-file.ts
  • src/main/system-status-ipc.ts
  • src/preload/__tests__/preload-bridge.test.ts
  • src/preload/index.ts
  • src/renderer/src/App.tsx
  • src/renderer/src/__tests__/App.locked-pro-tabs.integration.test.tsx
  • src/renderer/src/__tests__/App.navigation.integration.test.tsx
  • src/renderer/src/__tests__/harness/app-boundary.ts
  • src/renderer/src/assets/main.css
  • src/renderer/src/bootstrap/__tests__/loadProFeaturesRenderer.test.ts
  • src/renderer/src/bootstrap/loadProFeaturesRenderer.ts
  • src/renderer/src/bootstrap/proView.ts
  • src/renderer/src/bootstrap/slotRegistry.ts
  • src/renderer/src/components/BackupRestoreSection.tsx
  • src/renderer/src/components/CommandPalette.tsx
  • src/renderer/src/components/MemoryChat.tsx
  • src/renderer/src/components/Onboarding.tsx
  • src/renderer/src/components/PermissionGate.tsx
  • src/renderer/src/components/ProjectsScreen.tsx
  • src/renderer/src/components/Settings.tsx
  • src/renderer/src/components/__tests__/BackupRestoreSection.integration.test.tsx
  • src/renderer/src/components/__tests__/BackupRestoreSection.test.tsx
  • src/renderer/src/components/__tests__/CommandPalette.integration.test.tsx
  • src/renderer/src/components/__tests__/MemoryChat.chat-lifecycle.test.tsx
  • src/renderer/src/components/__tests__/MemoryChat.clipboard-overlay.test.tsx
  • src/renderer/src/components/__tests__/MemoryChat.image.test.tsx
  • src/renderer/src/components/__tests__/MemoryChat.project-inheritance.test.tsx
  • src/renderer/src/components/__tests__/PermissionGate.local-network.integration.test.tsx
  • src/renderer/src/components/__tests__/harness/chat-boundary.tsx
  • src/renderer/src/components/pro/UpgradeScreen.tsx
  • src/renderer/src/components/pro/__tests__/UpgradeScreen.windows-notice.test.ts
  • src/renderer/src/components/pro/proCatalog.ts
  • src/renderer/src/components/pro/proSettingsCatalog.ts
  • src/renderer/src/components/setup/__tests__/HealthPanel.integration.test.tsx
  • src/renderer/src/env.d.ts
  • src/renderer/src/hooks/NotificationProvider.tsx
  • src/renderer/src/hooks/__tests__/notification-state.test.ts
  • src/renderer/src/hooks/notification-state.ts
  • src/renderer/src/hooks/useNotifications.integration.test.tsx
  • src/renderer/src/lib/__tests__/paletteScreens.test.ts
  • src/renderer/src/lib/__tests__/proCatalog.lookup.test.ts
  • src/renderer/src/lib/notification-hooks.ts
  • src/renderer/src/lib/paletteScreens.ts
  • src/shared/backup-contracts.ts
  • src/shared/ipc-contracts.ts
  • vitest.config.ts
  • vitest.db.ci.config.ts
  • vitest.db.config.ts
  • vitest.db.coverage.config.ts

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Per the maintainer, down from 85. The reason it mattered: pro BRANCHES sat right on
the old line - 85.5% locally but ~85.2% as CI measures it, because CI legitimately
skips the native-dep ambient journeys (ax-text and friends). A 0.3% environment
swing decided whether the gate was red, which is a gate reporting the runner rather
than the code.

What 80 actually loosens is branches alone: statements, functions and lines measure
91-93% in pro and higher in core, so they stay far above either line. It stays a
floor against regression rather than a target - the 85% standard in CLAUDE.md is
unchanged, every change that adds logic still adds tests, and this number only
moves back up.

The tests written to clear the old line are kept, not reverted: approval-lifecycle
went from 0% to covered, and four shared-file journeys are now real.
desktop reported TWO checks (verify + e2e) where mobile and mobile-pro report one.
Now it is one `ci` job covering typecheck (core + pro), the vitest suite with the
coverage floor, dependency boundaries, lint, and the Playwright tour on the built
app.

The e2e job's first nine steps were byte-identical duplicates of verify's - the same
pro checkout with its main fallback, the same shared monorepo provisioning, a second
`npm ci` - existing only to feed a second runner. Those are deleted, not moved.

The tradeoff, stated plainly: the tour now runs after the unit gates instead of
beside them, so a PR takes about 7 minutes longer to go green. An earlier attempt at
this was reverted because appending e2e "blew the 25-min job cap" (PR #68) - that cap
was this workflow's own timeout-minutes, so it is raised to 50 deliberately rather
than worked around.

e2e stays advisory (continue-on-error) for the reason already recorded above the
step: headless-Electron launch instability on the ubuntu runner, not product
failures. Its full comment block, its coverage capture and all three artifact
uploads are preserved verbatim.

NOTE: the check RENAMES from `verify` to `ci`. Any branch-protection rule requiring
`verify` on this repo needs repointing at `ci`, or it will wait forever on a check
that no longer reports.
SonarCloud flagged five BLOCKERs in e2e/screenshots-quality-hardening.spec.ts: "Add
at least one assertion to this test case". It was right, and the problem is worse than
a missing expect. Every case swallowed its own failures - `.catch(() => false)` on the
navigation, then `if (!reached) { shot; return }` - so a screenshot of a blank window,
or of whatever screen happened to be open, passed exactly like a good one. The PR body
then carried that image as evidence.

Each case now asserts it reached its surface before shooting, and the Replay case
asserts the control it exists to photograph is actually visible. The not-reached
screenshot is still taken, for diagnosis, and then the case fails instead of passing.

This is the repo's own rule from CLAUDE.md ("a screenshot that disproves the fix is
worse than no screenshot at all") enforced by the test rather than left to a human
pass, which the file's old header explicitly relied on.

Also adds .coderabbit.yaml. On these release PRs CodeRabbit reported a GREEN check
having reviewed NOTHING - "Review skipped: 140 files exceed the limit of 100" - and a
passing check that means "not reviewed" is the worst kind. The config excludes
screenshots, lockfiles, build output and vendored trees from review, so the file count
reflects code a reviewer would actually read. It does not rescue a release-sized PR
(desktop is still 140 code files against a limit of 100); the fix for those is smaller
PRs.
The default vitest project excludes *.dbtest.ts because they load the real native
SQLite and need better-sqlite3-multiple-ciphers rebuilt for the test runner's node ABI
(the app builds it for Electron's). scripts/test-db.sh performs that swap and restores
Electron's build afterwards, so this is its own step - the rebuild mutates node_modules.

Nothing ever ran them here, and that had a measurable cost: the default config EXCLUDES
database.ts, rag/store.ts, prompt-store and runtime-residency with the note "covered by
the tests in *.dbtest.ts via npm run test:db", a claim nothing verified.
approval-lifecycle.ts measured 0% for exactly this reason while having tests all along -
which is why 10 of the tests added on this branch were written against a real in-memory
SQLite instead.

Runs the coverage variant (OFFGRID_DB_VITEST_CONFIG), which is "the db journeys that
pass today". Four files are skipped, each with its cause written down in
vitest.db.coverage.config.ts: the control-center projection bug still awaiting a
decision, an Entity Graph route whose screen is gone while its IPC remains, a
profile-upgrade decrypt the safeStorage stand-in does not satisfy, and one journey
coupled to the model port. vitest emits no report at all when any test fails, so the
variant is what makes a report possible; the exclusions stay visible in that file rather
than hidden in this workflow, and each is meant to be deleted as its cause is resolved.

NOT verified locally on purpose: test:db rebuilds a native module for the node ABI and
restores it for Electron afterwards, and there is a dev app running on this machine that
would be left unable to load sqlite if the restore raced. CI is the right place to prove
this step, and it is bounded at 12 minutes.
Comment thread src/main/backup/data-port.ts Outdated
Comment thread src/main/backup/data-port.ts Outdated
The first CI run of this suite (OGAD 31067953547) measured it honestly: 71 of 75 files
and 243 of 248 cases pass on ubuntu. Four fail for reasons that are the RUNNER, not the
code, each verified from that log rather than guessed:

- multimodal-rag-lifecycle: resources/bin/ffmpeg is a bundled macOS binary. On ubuntu it
  exits 1 immediately, so the fixture audio the journey imports is never created.
- image-runtime-reliability: needs a live local engine on its port ("fetch failed /
  connect ECONNRESET 127.0.0.1:38119"). CI has no llama-server, and the journey is about
  reachability rather than anything the runner can stand up.
- update-check: reads real release history, so with no network it sees an empty list where
  it expects 0.0.102.
- clipboard-popup-journey: fails on the runner only, cause not yet diagnosed - named
  separately for that reason rather than folded into one of the above.

They stay in the local `npm run test:db`, where they pass on a Mac. Keeping the other 243
is the whole point: they cover database.ts, rag/store.ts, prompt-store and
runtime-residency, which the default project excludes with the note "covered by the tests
in *.dbtest.ts via npm run test:db" - a claim nothing verified until this step existed.

Every exclusion carries its evidence in vitest.db.ci.config.ts and is meant to be deleted
as its cause goes away, rather than being made advisory with continue-on-error, which
would have turned the whole step into a green light that means nothing.
…keeps history

Two Greptile P1s on the restore path, both verified before changing anything.

RESTORED CHUNKS HAD NO EMBEDDINGS. They landed with embedding = NULL, retrieval requires
a non-null one ("WHERE d.enabled = 1 AND c.embedding IS NOT NULL" in rag/store.ts), and
NOTHING ever re-embedded them - the background backfill feeds universal search from
observations, frames and transcripts, never rag_chunks. So a restored knowledge document
sat in its project looking enabled and could never inform an answer, permanently.

The archive carries no vectors (DesktopBackupChunk is content + position), so they are
recomputed on restore, using the same MiniLM the RAG indexer uses. Computed BEFORE the
write transaction, because better-sqlite3 transactions are synchronous and cannot await;
the embedder is injected and defaulted, so a test can supply a deterministic one and a
restore that touches no documents never pulls the model in. A model that will not load
does not fail the restore - those chunks keep their null, exactly as before this change.

AN EXISTING CONVERSATION SKIPPED ITS ARCHIVED MESSAGES. `if (exists) continue` dropped
the whole conversation, so a conversation already here - synced from another device, or
half-restored by an earlier run - silently lost every message only the archive had, while
the restore reported success. The archive has no message id, which is why skipping was
chosen; identity now comes from the content instead (same conversation, role, text and
timestamp IS the same message), so a restore fills gaps without producing a second copy
of everything.

Three regression cases: the vector reaches the row; a restore with no embedder still
lands the document; and a conversation that already exists gains the missing message
without duplicating the one it had. 84 backup tests pass.
…iour

CI's DB journeys caught this, which is exactly why that step now exists: 226 of 227 cases
passed and the one failure was this assertion expecting `embedding: null` on a restored
chunk - the old behaviour, encoded as the expectation.

The same test asserts two lines earlier that the restored document is ENABLED, so together
they described a document that shows as usable and can never answer: retrieval requires a
non-null embedding, and nothing re-embedded a restored chunk (f694a59).

Now asserts the SHAPE - a 384-element array of finite numbers - rather than the exact
values, which belong to the model rather than to this test. The real MiniLM produced them
on the runner, so this also confirms the fix end to end with the actual model, not a
deterministic stand-in.
The roundtrip journey asserted a 384-float shape from the REAL embedder, so it
described two different outcomes depending on the machine: null where MiniLM
cannot load, and a vector where it can. That is not a journey, it is a coin
flip - and it is what left CI red while the suite passed locally.

The port now takes its embedder as a constructor argument (default: the real
one), so the test injects a fixed vector and asserts that vector REACHES the
rag_chunks row. That is what the journey is about: a restored, enabled document
whose chunk has no embedding can never inform an answer, because retrieval
filters on "embedding IS NOT NULL".

Also records the verified state of the desktop mesh in the roadmap.
…ing it

Playwright has no headless mode for Electron - `headless` governs browsers it
launches itself, while an Electron app creates its own BrowserWindows. Linux CI
hides that behind xvfb; on a Mac there is no equivalent, so every `npm run
test:e2e` (and every pre-push) opened ~25 maximized windows and took the keyboard
away from whoever was working. The standing workaround was OFFGRID_SKIP_E2E=1:
quiet bought by not running the tests.

No window is needed. Playwright drives the webContents, not the screen, and the
renderer loads and paints regardless - so a headless run is simply never calling
show(), plus app.dock.hide() so the app cannot become frontmost either (the half
of the interruption that is not the window). `npm run test:e2e` now sets
OFFGRID_E2E_HEADLESS=1, which every spec inherits because each spreads
process.env, including the two that launch the packaged app directly.

Measured, not assumed: 80 passed headless, and screenshots come out fully
rendered at 3024x1670 (a hidden window still paints, so page.screenshot works).
Six specs failed in the full headless run; five of them fail on a real display
too, and the sixth passes 3/3 headless in isolation, so it was interference
between parallel workers rather than visibility. Headless costs nothing.

The decision is a pure resolver with its own tests, because the direction that
matters is the other one: a real user's app must never resolve to hidden, which
would be indistinguishable from failing to launch. OFFGRID_E2E_HEADED=1 wins
over headless so a failing spec can still be watched.
sonar-project.properties is ignored in Automatic Analysis mode - provable, not
assumed: PR #76 reported issues in scripts/, e2e/ and .github/workflows/ci.yml,
all three of which that file already excludes. Automatic Analysis reads
.sonarcloud.properties instead, and this repo has no CI scan step.

The cost was a gate grading the wrong code. Every BUG and VULNERABILITY it
reported was in a developer script or in CI YAML - zero in product source - and
one BLOCKER in a physical-sync script put new-code reliability at E, so the check
failed for reasons no user could encounter. Scripts, e2e and test files are now
out of the analysis; they answer to lint, typecheck and the coverage ratchet.
…t just the main one

Hiding the main window was half the job. pro opens several windows that call
show() and focus() themselves - the clipboard quick-open popup, the tray and CRM
notification surfaces, the meeting notice - and core re-focuses the running copy
on a second-instance launch, which resilience-single-instance.spec.ts triggers on
purpose. So a headless run still stole focus, once per spec that touched any of
them, which is what the developer actually noticed.

Every window created in a headless run is now non-focusable. That is the technique
pro's dictation overlay already uses deliberately (non-focusable + showInactive, so
the user's target app keeps the keys); this applies it to all of them, from the one
place that knows the launch is headless - so pro needs no changes and a window
added later is covered by default.

Visibility is deliberately left alone. Popups still appear and their
isVisible()-gated logic keeps working, which is what lets the clipboard quick-open
journey pass headless - it simply cannot take focus now. The presentation is also
resolved ONCE at module scope instead of per call site, because the main window,
the Dock tile, second-instance focus and pro's windows disagreeing is exactly how
this bug happened.

Measured: with the pro and single-instance specs running headless, the frontmost
macOS application was only ever Slack and Brave - Off Grid never took the keyboard.
…lete harness

Three pro-tier Devices specs were red in CI and locally both, invisible because the
e2e step is advisory. Two of them were never about the app.

`renders the real Devices screen with live sync status` asserted the text
"LAN + nearby ready" and a heading "Personal mesh". Neither string exists anywhere
in src/, pro/ or shared/packages/ - the first was never shipped and the second is
now "Licensed devices". The screen reports per ROUTE ("LAN: ready", or
listen/advertise/browse states when it is not), so that is what it now asserts. The
spec after it was inheriting the broken screen state rather than failing on its own.

The pairing spec died inside its own setup: it built a ClipboardSyncCoordinator with
no deliveryPersistence, so loadPendingDeliveries threw before the app was touched.
The synthetic peer now keeps deliveries in memory beside its history, standing in
for the peer's disk and none of our logic.

5 of 6 in the file pass headless now. The pairing spec still fails, and for a real
reason: pairing requires an 8-character code from the other device and the synthetic
peer mints none. Recorded in the gaps doc against the standing pairing-harness work
instead of being bodged or deleted.
… Grid AI

Seen on the Windows build, but it was never platform-specific: these strings live
in the core renderer, so every platform showed them. The brand is Off Grid AI
everywhere - window titles, OAuth clients, about screens, badges - and the tier
was being announced as "Off Grid Pro" on the upgrade screen (badge, heading,
platform note) and in onboarding, with the mobile app called "the Off Grid phone
app".

Conversational prose that uses "Off Grid" as the subject ("Off Grid reads your
calendar") is left alone: that reads as the short name in a sentence rather than a
product being misnamed. What changed is only where a PRODUCT or TIER is named.
Windows is an enabled platform now, so "Off Grid AI Pro is macOS-tested today" and
"support for this Windows PC will be enabled once it is tested" are simply untrue -
and they were the first thing a Windows user read on the upgrade screen.

The notice stays per-FEATURE, because that part was already right and is derived
from data: proCatalog declares platforms per feature, and several genuinely are
macOS-only (capture via ScreenCaptureKit, meetings via the macOS recorder,
dictation via its hotkey helper). So the sweeping platform claim is gone and what
remains is the honest, narrower one - this feature runs on Mac today, and the
license covers Mac, Windows and the phone app up to 5 devices.

Not touched: which features declare win32 support. That is a claim about ported
code, not copy, and inventing it here would put a feature on a screen that cannot
run.
…oes not exist

This is the spec that "gets stuck on the cmd+K search" - and nothing was stuck. It
looked for the placeholder 'Search everything…', which is not a substring of what
the palette actually renders ('Search everything, or jump to a screen…'), so the
locator matched no element and every focus assertion waited out its full timeout
before failing. ~17s per attempt, three attempts with retries, on every run.

Now anchored on the stable half of the string, so a copy tweak to the tail cannot
resurrect this. The spec passes in 1.7s. The palette was always right: it is a
Radix Dialog around cmdk's CommandInput, which takes focus on open.

Also here, because they landed in the same pass:
- the pre-push e2e now runs on the TEST BOX whenever it answers (scripts/e2e-on-box.sh),
  falling back to a local headless run only when the box is unreachable (exit 20),
  and bringing coverage + screenshots home so the coverage gate still counts them;
- licence copy says "desktop and mobile" rather than enumerating platforms.
Rebranding the badge to "Off Grid AI Pro · Available now" broke the two tour specs
that assert that exact string - the upgrade-screen tour and the purchase-link check.
A copy change and the tests that depend on it belong in the same pass; I missed
these, and the test-box run is what caught them, which is the argument for having
that run in the hook at all.
V8 writes the absolute path of the machine that produced the coverage, so every
entry came home saying file:///Users/admin/ogad-e2e/... and c8 could map none of it
to this checkout. The whole e2e contribution vanished from the coverage gate -
measured: 3823 box paths against 69 local ones, and the gate dropped from 72.8% to
63.5% the first time a push used the box.

The prefix is now rewritten as the files arrive, which is enough for c8 to resolve
them through the bundle sourcemaps: 342 of 342 files in the report map to this
checkout afterwards. Moving the run to another machine must not quietly reduce what
the gate can see.
…d on the box

Without this, checking a single spec meant running it locally "just to check" - which
is the exact habit the box exists to remove, and it puts app windows back on the
developer's screen. Now:

  bash scripts/e2e-on-box.sh devices-sync.spec.ts -g "pairs a real peer"
…itself

Pairing consumes a licensed mesh seat, so a spec that pairs needs a real
entitlement - and the dev target was never seeded, only the packaged one. Two facts
made this harder than a file copy, and both are worth writing down:

- A licence cache is sealed with macOS safeStorage, and a Keychain item is ACL'd to
  the application that created it. A licence activated by the SIGNED PACKAGED app is
  therefore unreadable by `electron .`: it fails to decrypt, the DB key fails with
  it, setupIPC dies on "file is not a database", and the app reports itself
  unlicensed while a perfectly valid licence sits on disk. Seeding the packaged
  profile is actively worse than seeding nothing.

- So the fixture has to be created BY the dev build. scripts/seed-e2e-license.mjs
  does that through the licence IPC - no UI, no clicking - and keeps the resulting
  profile as the fixture launch.ts seeds from. It retries while activation reports
  network_unavailable, because that is what activation says when pro's entitlement
  owner has not registered yet (a 5s readiness wait, not a network fault).

Run once per machine: node scripts/seed-e2e-license.mjs
This spec was written against a pairing flow that no longer exists, and every run
spent 15s discovering that. Five separate things were stale or missing; each one hid
the next, which is why it read as "pairing is broken":

- The codes were 'synthetic-pair-code' / 'different-pair-code'. A code is 8
  characters from a confusable-free alphabet, so the field rejected both outright -
  the "mismatch" case was failing VALIDATION, not proving a mismatch is refused. Now
  it presents the code the Mac is actually showing, read from its status, the way a
  person reads it off the other screen.
- There is no "X wants to pair" heading, no "Incoming pairing code" textbox and no
  Accept button any more; those strings exist nowhere in the renderer. The host
  compares the presented code itself, and the dialog that remains is informational
  with Cancel as its only action.
- Pairing is a licensed transaction on BOTH sides: without an entitlement adapter the
  peer refused its own handshake with entitlement_unavailable. The peer now carries
  the real-world shape of a joining phone - unlicensed, sponsored by the Mac, so only
  the import half is implemented and the export half refuses out loud.
- The secure-store gate was released before the peer had reached it, so the peer
  blocked forever in begin() and the attempt died as "Pairing was cancelled" a minute
  later. It now waits for the gate to exist first.
- The clipboard coordinator asks for pairedDeviceIds AND connectedDeviceIds; the
  harness only answered the second, which surfaced the moment pairing began working.

Pairing now completes for real: the app reports paired + connectedIds carrying the
peer and the header counts "1 connected", which is what this step asserts. It does
NOT assert a device row: that list renders licence-registry devices and discovered
devices, and a stub entitlement adapter registers no machine - a real phone does, as
part of pairing. Asserting the row would be asserting a side effect the harness
deliberately does not have.
Both of these asserted behaviour that was changed on purpose, and both were my own
misses - a change and the tests that depend on it belong in the same commit.

- UpgradeScreen.windows-notice asserted /macOS-tested/. That claim is gone because
  Windows is live: telling a Windows user Pro is "macOS-tested today" and will be
  enabled "once it is tested" was false. It now asserts the honest pair - Windows is
  live with features arriving one at a time, and the licence covers desktop and
  mobile - and asserts the old claim is absent.

- keygen-personal-mesh-registry asserted that an incomplete machine record REFUSES
  the whole roster. That refusal is exactly what locked a real licence out of every
  device, so such a record is now kept as a seat attributed to no device, which the
  eviction order releases first.
Every push was running all 25 spec files - minutes of wall clock to check a one-line
edit, which is how a gate becomes something people skip. CI runs the full suite
regardless, so the local gate does not need to.

The rule: specs changed and no app code touched -> just those specs. Anything under
src/, pro/ or shared/ can break any surface, so that still earns the full run.
Explicit arguments always win, and E2E_FULL=1 forces everything.
@greptile-apps

greptile-apps Bot commented Aug 6, 2026

Copy link
Copy Markdown

Too many files changed for review (183 files, 100 file limit).

Bypass the limit by tagging @greptile-apps to review.

It is a six-minute suite and CI runs it on every PR regardless. Wiring it into every
local push made the gate something to work around instead of something to trust -
and it put a stream of app launches and minutes of waiting between the developer and
a one-line commit.

Now opt-in: OFFGRID_E2E_ON_PUSH=1 git push. The box runner it calls is unchanged, so
asking for it still runs on the test box rather than the developer's machine.
@sonarqubecloud

sonarqubecloud Bot commented Aug 6, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant