feat(e2e): scaffold Playwright Electron suite + CI job (PR-E)#277
Conversation
Lands the infrastructure for end-to-end testing the desktop app, plus two initial specs and a Linux CI job. The CI job is marked continue-on-error: true while we stabilize the suite — flip the flag off in a follow-up once it's reliably green on develop. Setup: - apps/desktop/playwright.config.ts: serial workers (Electron app state is shared), generous timeouts, retain-on-failure trace/video. - apps/desktop/e2e/fixtures.ts: launchApp() helper that spins up a fresh Electron instance with an isolated userData directory per test, so the SQLite DB, settings, license cache, etc. don't leak between runs. Uses Playwright's _electron.launch. - apps/desktop/e2e/tsconfig.json: dedicated tsconfig so the specs don't pull in renderer/main types they don't need. - apps/desktop/package.json: adds @playwright/test devDep, e2e + e2e:headed scripts, typecheck:e2e step folded into typecheck. - apps/desktop/e2e/README.md: how to run locally, what's tested, what's deliberately out of scope. Specs: - smoke.spec.ts: app launches, main window renders, IPC bridge is present, no uncaught console errors during initial mount. This is the regression catch for #266 (editor mount crashes that produced blank windows). - notes.spec.ts: notes IPC contract — create / list / get roundtrip, FTS5 search returns freshly-created notes. We drive the preload bridge (window.readied.notes.*) rather than the editor UI on purpose: selectors churn, contracts are stable. CI: - .github/workflows/ci.yml: new `e2e` job, ubuntu-latest, xvfb, uploads playwright-report artifact on failure. continue-on-error: true initially. - .gitignore: excludes test-results/, playwright-report/, playwright/.cache/ - knip.json: registers playwright.config.ts + e2e/**/*.ts so they don't show up as unused. What is deliberately NOT in this PR: - Editor UI interactions (typing, formatting, hotkeys) — CodeMirror surface is too prone to flake; revisit once the editor is split. - AI panel streaming — better as a vitest test against ai-core. - Sync flows — need a fake server. I scaffolded all of this but could not execute the suite end-to-end against a real Electron build in this session (no display attached, no built bundle in this branch). The CI's continue-on-error gate and the local README acknowledge that the first verifier on a real machine may need to tweak the specs once they meet a real renderer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (2)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
abb6a37
into
feat/license-ed25519-verification
…es (PR-G) (#278) ## Summary **First scoped slice of the PR-G \"split god files\" effort.** \`apps/desktop/src/main/index.ts\` goes from **1065 → 950 lines** (-11%). No behavior change. All extractions are self-contained and surface-area preserving — no API changes, no runtime touch. ## What moved | New file | Was at | Notes | |---|---|---| | \`services/fileLicenseStorage.ts\` | \`index.ts:146–216\` | Three \`readJsonOrNull\` helpers fold the repeated try/catch/return-null pattern into one private function. Header comment points at PR-F3 so the next step (move to signed envelopes) is unambiguous. | | \`services/windowState.ts\` | \`index.ts:218–254\` | Header comment now documents *why* the file I/O is synchronous (called during \`BrowserWindow\` construction before the renderer mounts, and during close where handlers don't await). | ## What this PR DELIBERATELY does NOT touch Per your earlier guidance about merge-conflict risk: | File | Why deferred | |---|---| | \`packages/storage-sqlite/src/repositories/SQLiteNoteRepository.ts\` (1121 L) | Splitting into \`NoteCrudRepository\` + \`NoteTagRepository\` + \`NoteArchiveRepository\` + \`NoteSyncRepository\` needs runtime verification against a real DB. Risk of merge conflicts with every other in-flight PR. Separate effort. | | \`apps/desktop/src/renderer/components/MarkdownEditor.tsx\` (724 L) | The CodeMirror surface is too entangled to split without an E2E suite to catch regressions. Defer until PR-E (#277) is stabilized; then refactor under test coverage. | The principle is established here: surgical extractions of self-contained pieces, each verifying typecheck+tests, no behavior change. Future PRs can keep slicing under the same discipline. ## Test plan - [x] \`pnpm -r typecheck\` — green - [x] \`pnpm test\` — 17/17 packages - [x] \`pnpm --filter @readied/desktop typecheck\` — green (includes the e2e tsconfig from #277) - [ ] Manual smoke: launch the app, confirm window remembers its position after close+reopen, confirm license/trial files still read/write correctly ## Stack context **PR-G** in the audit stack. Stacked on top of #277 (PR-E) → #276 (PR-F3) → #275 (PR-F2) → #274 (PR-F5) → #273 (PR-F4) → #272 (PR-F1) → #271 (PR-I) → #270 (PR-J) → #269 (PR-D) → #268 (PR-H) → #267 (PR-C) → #266 (PR-A) → #265 (PR-B). **14 PRs deep.** 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary First slice of the knip cleanup. Knip surfaced 28 unused files in #267. After per-file verification (greps, import tracing, barrel resolution), **12 were genuinely orphaned** — those are deleted here. The rest stay for reasons documented below. ## Deleted (12 files, 659 lines) ### \`apps/desktop\` | File | Why safe | |---|---| | \`src/renderer/analytics.ts\` | No imports anywhere | | \`src/renderer/hooks/useTheme.ts\` | No imports anywhere | | \`src/renderer/settings.tsx\` | No imports anywhere (the real settings entry is \`pages/settings/SettingsApp.tsx\`, loaded via dynamic import in \`main.tsx\`) | | \`src/renderer/ui/patterns/Modal.tsx\` + \`index.ts\` + \`Modal.module.css\` + \`.gitkeep\` | Only the barrel imported \`Modal\`, and the barrel itself was unused — both go together | ### \`packages\` | File | Why safe | |---|---| | \`plugin-api/src/editor/types.ts\` | Not re-exported by \`plugin-api/src/index.ts\` | | \`storage-core/src/{interfaces,migrations,repositories,types}/index.ts\` | Main package index imports directly from concrete files, not these barrels | | \`storage-sqlite/src/repositories/index.ts\` | Same — concrete imports, no barrel use | ### \`scripts\` | File | Why safe | |---|---| | \`scripts/bump-version.js\` | Not referenced by any \`package.json\` script or CI workflow | ## Kept (Knip false positives) ### Auto-discovered files Knip can't see | File | Why kept | |---|---| | \`apps/desktop/src/renderer/vite-env.d.ts\` | \`/// <reference types="vite/client" />\` — required by Vite | | \`apps/desktop/src/renderer/css-modules.d.ts\` | TypeScript module shim for CSS module imports | | \`apps/desktop/src/renderer/turndown-plugin-gfm.d.ts\` | TypeScript module shim for an untyped npm dep | | \`apps/web/mdx-components.tsx\` | Next.js convention — auto-discovered, never imported | ### Knip wrong about being unused | File | Reality | |---|---| | \`renderer/pages/settings/components/controls/{NumberInput,Select,TextInput,Toggle}.tsx\` | Imported by EditorSection / AiSection / UpdatesSection **through** the \`controls/index.ts\` barrel. Knip flagged both the components and the barrel as unused because it doesn't follow the chain. | ## Skipped (out of audit scope) apps/web cleanup: \`magicui/*\`, \`NavDropdown\`, \`ui/separator\`. The audit excluded apps/web; leaving them for a separate marketing-site pass. ## Test plan - [x] \`pnpm -r typecheck\` — green - [x] \`pnpm test\` — 17/17 packages - [ ] Manual smoke after merge ## What's next in this cleanup track - **PR-Knip-2**: remove unused production + dev dependencies (the \`Unused dependencies (9)\` block from knip) - **PR-Knip-3**: remove unused exports (the \`Unused exports (~100)\` block) — much more careful, since some exports may be public API contracts for plugins or external consumers ## Stack context Stacked on top of #278 (PR-G) → #277 (PR-E) → #276 (PR-F3) → #275 (PR-F2) → #274 (PR-F5) → #273 (PR-F4) → #272 (PR-F1) → #271 (PR-I) → #270 (PR-J) → #269 (PR-D) → #268 (PR-H) → #267 (PR-C) → #266 (PR-A) → #265 (PR-B). **15 PRs deep.** 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary Phase 2 of knip cleanup. Knip flagged 11 unused dependencies in #267; per-dep verification confirmed **6 are genuinely unused** in our scope. Removing them shrinks the lockfile by ~120 lines. ## Removed ### \`apps/desktop\` production | Dep | Why safe | |---|---| | \`highlight.js\` | We use \`rehype-highlight\`, which depends on \`lowlight\` (NOT \`highlight.js\` directly). \`lowlight\` ships its own grammars. | | \`pino-roll\` | \`logger.ts\` writes directly via \`createWriteStream\` — no pino transport / no rolling pipeline | | \`react-resizable-panels\` | No imports anywhere | | \`unist-util-visit\` | No imports anywhere (the only consumer is \`@readied/wikilinks\`, which declares it itself) | ### \`apps/desktop\` dev | Dep | Why safe | |---|---| | \`@types/mdast\` | No imports anywhere | | \`pino-pretty\` | \`logger.ts\` doesn't pipe through a transport; dev output is raw JSON via the synchronous pino factory | ### \`packages\` | Package | Dep removed | Why | |---|---|---| | \`@readied/sync-core\` | \`@readied/core\` | No imports of \`@readied/core\` in \`src/*\` — workspace link was stale | | \`@readied/wikilinks\` | \`unified\` | \`src/*\` uses \`unist-util-visit\` directly (kept) but never \`unified\` | ## Skipped (out of audit scope) \`apps/web\`: \`@radix-ui/react-separator\`, \`next-themes\`, \`tailwindcss-animate\` — marketing site, separate pass. ## Test plan - [x] \`pnpm install --ignore-scripts\` — succeeds. Pre-existing peer warnings about electron-builder / electron-vite are unrelated. - [x] \`pnpm -r typecheck\` — green. - [x] \`pnpm test\` — 17/17 packages. - [ ] Manual smoke on the desktop app after merge — verify nothing transitively depended on highlight.js / pino-roll / etc. that grep missed. ## Stack context Stacked on **PR-Knip-1** (#279) → PR-G (#278) → PR-E (#277) → ... down to PR-B (#265). 16 PRs deep. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ge layer (PR-F3-Wiring) (#281) ## Summary Wires the verification primitive from #276 into the desktop's \`FileLicenseStorage\`. The path from server → disk → in-memory now has a real Ed25519 verification step at read time, with **lenient fallthrough** during the migration window. ## Changes ### \`packages/licensing/src/types.ts\` \`StoredSubscriptionData\` now allows an optional \`signedEnvelope\`: \`\`\`ts interface StoredSubscriptionData { readonly subscription: SubscriptionInfo; readonly lastVerified: string; readonly cacheExpiresAt: string; readonly signedEnvelope?: SignedSubscriptionEnvelope; // NEW } \`\`\` Both can coexist during migration. Long-term, the unsigned \`subscription\` field becomes a derived view of the envelope payload. ### \`apps/desktop/src/main/services/fileLicenseStorage.ts\` \`readSubscriptionData\` now branches on envelope presence: | State on disk | Behavior | |---|---| | **Envelope present + signature valid** | Return cache as-is | | **Envelope present + signature invalid** | Log error, **refuse the cache** (return null). Next caller fetches fresh from the API | | **Envelope absent** | Log warning, accept the cache (lenient migration mode) | Plus a \`SUBSCRIPTION_PUBLIC_KEY\` constant at the top of the file with a **REPLACE BEFORE SHIPPING** note. The all-zeros placeholder means any real envelope will fail verification — that's the *correct* failure mode while the placeholder is in place. The lenient "no envelope" branch is what runs today. ## What's NOT in this PR (still pending the server team) - \`ApiClient.getSubscriptionStatus\` does not yet return an envelope. When it does, \`mapApiToSubscriptionData\` in \`licenseHandlers.ts\` grows one more field (\`signedEnvelope\`) and \`writeSubscriptionData\` persists it. That change is one-liner plumbing, blocked only on the API contract being updated. - The placeholder public key gets swapped for the real server key in the release that first ships signed envelopes. ## Behavior matrix | Disk state | Today (placeholder key) | After server rollout (real key) | |---|---|---| | Empty | null returned, no log | null returned, no log | | Old shape (no envelope) | Warning logged, accepted | Warning logged, accepted (until strict mode) | | Real envelope, valid sig | Cannot occur (server isn't signing yet) | Silently accepted | | Real envelope, invalid sig | Will fail (placeholder key) → refused → refetch | Refused → refetch | | Tampered envelope | Refused → refetch | Refused → refetch | ## Test plan - [x] \`pnpm -r typecheck\` — green - [x] \`pnpm test\` — 17/17 packages (the 18 signature tests from #276 cover the verify path; this PR just calls into them) - [ ] Manual: edit \`subscription.json\` to flip a single byte in the cached \`subscription.subscriptionId\` field. Reopen the app. Once server signs envelopes: expect error log + refetch. Today: warning log + cache accepted (no envelope to verify). ## Stack context Stacked on **PR-Knip-2** (#280) → PR-Knip-1 (#279) → PR-G (#278) → PR-E (#277) → ... down to PR-B (#265). **17 PRs deep.** 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Repository (#282) ## Summary **Phase 1 of the SQLiteNoteRepository split.** Pure helpers come out to their own file so future sub-repositories (sync, tag, archive) can reuse them without inheriting from the 1000-line class. | Metric | Before | After | |---|---|---| | \`SQLiteNoteRepository.ts\` | 1121 lines | **1038 lines** (-7%) | | \`noteMapping.ts\` | — | 133 lines (new) | ## Extracted | Symbol | Kind | Notes | |---|---|---| | \`NoteRow\`, \`TagRow\`, \`TagWithColorRow\`, \`BacklinkInfo\` | Row types | Re-exported by SQLiteNoteRepository so external imports keep working | | \`rowToNote(row, tags) -> Note\` | Pure mapper | Reconstructs a domain Note from a SQLite row + its tags | | \`prepareFtsQuery(input) -> string\` | Pure helper | FTS5 query escaper + tokenizer | | \`archivedConditionSql(filter, alias) -> string\` | Pure helper | SQL fragment for archived filtering | Call sites swapped from \`this.<helper>()\` to plain function imports. **The public class signature is unchanged** — \`BacklinkInfo\` is re-exported so external consumers (e.g. \`apps/desktop/src/main/handlers/types.ts\`) keep working without edits. ## What this PR DELIBERATELY does NOT do - **Extract sync methods** (\`getPendingChanges\` through \`getSyncHistory\` ~430 lines) into a \`SQLiteNoteSyncRepository\`. Those share state — tag queries, transactions, FTS sync triggers — with the main class in ways that need real-DB integration coverage to refactor safely. The helpers extracted here are the foundation: a follow-up PR can build the sync sub-repo on top of them without touching the helpers again. - **Extract tag methods** (\`setManualTags\`, \`renameTag\`, \`getAllTagsWithColors\`, etc.) for the same reason. The audit aspired to a 4-way split (NoteCrudRepository + NoteTagRepository + NoteArchiveRepository + NoteSyncRepository). That remains the destination. This PR ships the **foundation** that makes those splits low-risk; each can ride its own PR with focused review. ## Test plan - [x] \`pnpm -r typecheck\` — green - [x] \`pnpm test\` — 17/17 packages - [ ] Manual: launch the desktop, exercise notes CRUD + search + tag operations. Behavior should be identical (no observable change). ## Stack context Stacked on **PR-F3 wiring** (#281) → PR-Knip-2 (#280) → PR-Knip-1 (#279) → PR-G (#278) → PR-E (#277) → ... down to PR-B (#265). **18 PRs deep.** 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
**Phase 1 of the MarkdownEditor split.** Pure theme + markdown
HighlightStyle move to their own file so further extractions (extensions
array, keymap bindings) can ride on top without merging against a
churn-prone component shell.
| Metric | Before | After |
|---|---|---|
| \`MarkdownEditor.tsx\` | 737 lines | **612 lines** (-17%) |
| \`editorTheme.ts\` | — | 139 lines (new) |
## Extracted
| Symbol | Kind |
|---|---|
| \`SCROLL_PAST_END_PADDING\` | constant |
| \`createEditorTheme(fontSize, fontFamily, lineHeight)\` | factory
returning \`EditorView.theme({...})\` |
| \`markdownHighlighting\` | \`HighlightStyle.define([...])\` with all
tag styles for markdown |
## Imports cleaned up
- Drop \`HighlightStyle\` from \`@codemirror/language\` import (no
longer referenced in this file)
- Drop \`tags\` from \`@lezer/highlight\` (moved into editorTheme.ts)
## What this PR DELIBERATELY does NOT do
- **Extract the extensions array** (~96 lines of \`createExtensions\`).
It closes over user settings (\`lineNumbersCompartment\` etc.) and mixes
context-coupled values like \`wikilinkAutocomplete\` from a hook. Safely
pulling that out requires either passing the closure context via a
builder, or moving the whole \`useMemo\` into its own hook. **Better
done under Playwright coverage (PR-E #277) so renderer regressions
surface.**
- **Extract the keymap.** Same reason — bindings reference
view-imperatives + the command-registry which are constructed inside the
React tree.
## Test plan
- [x] \`pnpm -r typecheck\` — green (renderer + e2e tsconfigs)
- [x] \`pnpm test\` — 17/17 packages
- [ ] Manual: open the editor, type in a markdown note with headings,
emphasis, lists, code blocks. The look must be identical (theme is
byte-for-byte the same; just moved).
## Stack context
Stacked on **note repo split** (#282) → PR-F3 wiring (#281) → PR-Knip-2
(#280) → PR-Knip-1 (#279) → PR-G (#278) → PR-E (#277) → ... down to PR-B
(#265). **19 PRs deep.**
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Why this PR The audit stack (#266 through #284) was structured as 19 stacked PRs, each one's base pointing at its predecessor in the chain. Each merged into **its own parent branch**, not into \`develop\`. Net result: \`develop\` only contains #265, and the other 19 PRs are stranded on their branch tips. This PR ships the head of the stack (\`chore/set-subscription-public-key\`) into \`develop\` so the work actually lands. It's a single PR with **21 commits** — the linear chain of the stack — preserving each individual PR's conventional commit message so semantic-release can categorise them for the next release. ## What's in this PR (in merge order) | # | Commit | Title | |---|---|---| | 1 | \`3412e4e\` | fix(typecheck): unblock pnpm -r typecheck after TS 6.x bump (already merged as squash in develop — no-op overlap) | | 2 | \`03da9cb\` | fix(editor): stop runtime crashes in MarkdownEditor | | 3 | \`07a76d6\` | chore(tooling): replace husky with lefthook, add knip, tighten lint-staged | | 4 | \`e27f1ba\` | refactor(stores): use named selectors instead of destructuring full state | | 5 | \`c519fc3\` | chore(test): add coverage baseline + smoke tests for @readied/commands | | 6 | \`5b1cc55\` | chore(mcp-server): migrate to registerTool API + FTS5 for read_note | | 7 | \`0eb49a5\` | fix(backup): integrity-check restored db and roll back on failure | | 8 | \`402e280\` | refactor(ipc): add typed IPC registry, migrate aiKeyHandlers as proof | | 9 | \`dd4823d\` | refactor(ipc): migrate light handlers to defineIpcHandler | | 10 | \`5607100\` | refactor(ipc): migrate heavy data handlers to defineIpcHandler | | 11 | \`c51c4d3\` | fix(aiKeyStorage): stop deleting encrypted keys on transient decrypt errors | | 12 | \`d5f33ef\` | feat(licensing): add Ed25519 subscription envelope sign + verify | | 13 | \`5ba96d4\` | feat(e2e): scaffold Playwright Electron suite + CI job | | 14 | \`2bd1396\` | refactor(main): extract FileLicenseStorage and window state to services | | 15 | \`fd9b809\` | chore(knip): delete verified-unused files (phase 1) | | 16 | \`a3d7c1b\` | chore(knip): remove unused dependencies (phase 2) | | 17 | \`0ec48b3\` | feat(license): wire Ed25519 signed-envelope verification at the storage layer | | 18 | \`cca7e04\` | refactor(storage-sqlite): extract noteMapping helpers from SQLiteNoteRepository | | 19 | \`1a0df95\` | refactor(editor): extract theme + highlight from MarkdownEditor | | 20 | \`9034c71\` | chore(license): set real SUBSCRIPTION_PUBLIC_KEY | | 21 | \`390503c\` | docs(api): document LICENSE_SIGNING_PRIVATE_KEY secret requirement | Each is already individually reviewed and merged on GitHub (#266–#284). They appear here as their original commits because the stack used rebase-based stacking, not merge commits. ## Pre-merge verification (local, this branch) - ✅ \`pnpm -r typecheck\` — green across 18 workspace projects - ✅ \`pnpm test\` — 17/17 packages - ✅ \`pnpm build\` — 6/6 packages ## After this PR merges Per the release flow in \`CLAUDE.md\`: 1. Open \`develop → main\` PR 2. Click \"Run workflow\" on Release action — \`semantic-release\` analyses these conventional commits and bumps the version 3. Tag push triggers Build workflow — mac/win/linux in parallel 4. All builds green → release un-drafts → electron-updater serves the update ## Notable behaviour changes for users - **Editor**: no more blank-window crash on notes with tables (#266) — root cause was \`Decoration.replace\` over multi-line ranges from a ViewPlugin instead of a StateField - **AI keys**: no more silent deletion of keys when keychain is temporarily locked after sleep/wake (#275) - **Backups**: corrupt restored DB is now refused and rolled back to a safety copy (#271) - **Subscriptions**: client now verifies Ed25519 server signatures (#281, #284); server-side signing rollout still needed for full effect - **Tooling**: lefthook replaces husky, knip available for dead-code audits (#267) - **Infrastructure**: full IPC surface now validated with Zod at the boundary (#272 + #273 + #274), Playwright scaffold in place (#277) ## Notable for reviewers - The placeholder \`SUBSCRIPTION_PUBLIC_KEY\` in #281 was replaced with a real key in #284. The matching private key is set in Cloudflare as \`LICENSE_SIGNING_PRIVATE_KEY\` (prod + staging). This keypair was generated in a Claude session and is dev/staging-grade — rotate before serving real paid customers. - 50% of audit findings were Knip false positives or already-fixed (documented per PR). Stack reflects real debt, not the audit verbatim. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added end-to-end testing for the desktop application with comprehensive smoke and notes testing. * Implemented IPC handler validation using Zod schemas for improved type safety. * Added subscription envelope signing and verification using Ed25519 cryptography. * **Bug Fixes** * Improved encryption error handling and recovery logic. * **Tests** * Established centralized test coverage configuration across all packages. * Expanded test suites for markdown commands and licensing functionality. * **Chores** * Transitioned from Husky to Lefthook for Git hooks management. * Refactored internal IPC architecture and service modules. * Updated build and deployment configurations. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary Promotes `develop` to `main` for **v0.15.0**. Originally opened 2026-04-24; now refreshed with the 19-PR tech-debt audit shipped via #285 plus the accumulated dependabot bumps reconciled. `semantic-release` will pick the version bump. Expected: **minor (v0.14.x → v0.15.0)** because of multiple \`feat:\` commits. ## What ships (audit highlights, from #285) ### Runtime fixes (user-facing) - **Editor no longer crashes on table-containing notes** (#266) — \`Decoration.replace\` over multi-line ranges moved from a ViewPlugin to a StateField, plus an EditorView.exceptionSink so any future plugin error no longer tears down the EditorView. - **AI keys survive sleep/wake** (#275) — \`aiKeyStorage\` stopped silently deleting the encrypted store when the keychain was temporarily locked after macOS sleep. - **Backup restore is now safe** (#271) — restored DBs go through \`PRAGMA integrity_check\` before being swapped in; corrupt backups roll back to the safety copy. - **MCP server runs without electron-builder rebuilds** (#264 → #270) — migrated from native \`better-sqlite3\` to built-in \`node:sqlite\` (Node 22.5+), updated to the new \`registerTool\` MCP SDK API. ### Security - **Typed IPC boundary** (#272 + #273 + #274) — 130+ IPC channels now validated with Zod tuples at the main↔renderer boundary. Garbage in fails fast with \`IpcValidationError\` instead of corrupting downstream code. - **Ed25519 license verification scaffolding** (#276 + #281 + #284) — \`signSubscriptionPayload\` / \`verifySubscriptionSignature\` helpers ship in \`@readied/licensing\`, wired into \`FileLicenseStorage.readSubscriptionData\` with lenient fallthrough during migration. Real public key embedded (\`d04901…\`). Server-side \`LICENSE_SIGNING_PRIVATE_KEY\` already set in Cloudflare staging + production. ### Developer experience - **Husky → Lefthook** (#267) plus lint-staged that now runs ESLint, not just Prettier. - **\`knip\` added** (#267) + 12 unused files deleted (#279) + 6 unused deps dropped (#280). - **Playwright Electron E2E scaffold** (#277) with smoke + notes-IPC specs and a Linux+xvfb CI job (\`continue-on-error: true\` while it stabilises). - **Vitest coverage baseline** (#269) — 12 packages share a coverage config; smoke tests added for \`@readied/commands\`. ### Refactor (no behavior change) - **Zustand selectors migration** (#268) — 3 components stopped destructuring entire stores. - **God-file extractions**: - \`main/index.ts\` 1065 → 950 lines (#278) — \`FileLicenseStorage\`, \`windowState\` extracted to services - \`SQLiteNoteRepository.ts\` 1121 → 1038 lines (#282) — pure helpers extracted to \`noteMapping.ts\` - \`MarkdownEditor.tsx\` 737 → 612 lines (#283) — theme + markdownHighlighting extracted to \`editorTheme.ts\` ## Deploys triggered | Workflow | Trigger | What happens | |---|---|---| | \`deploy-api.yml\` | Auto on \`push\` to main affecting \`packages/api/**\` | Tests + deploys \`@readied/api\` to Cloudflare Workers (\`readied-api-production\`). This stack only touched \`wrangler.toml\` + \`.dev.vars\` docs, no production code change. | | \`release.yml\` | Manual \`workflow_dispatch\` post-merge | \`semantic-release\` analyses conventional commits, bumps version, creates GitHub Release draft + tag | | \`build.yml\` | Auto on tag push from release.yml | mac / windows / linux parallel builds, artefacts attached to the GitHub Release | ## Pre-merge verification (local, this branch) - ✅ \`pnpm -r typecheck\` — green across 18 workspace projects - ✅ \`pnpm test\` — 17/17 packages - ✅ Merge resolved: take develop versions for 19 conflicted package.jsons (develop has equal or newer deps than main's dependabot bumps) ## Post-merge action items (operator) 1. **Deploy API to staging first** (smoke test): \`\`\` gh workflow run deploy-api.yml -f environment=staging \`\`\` 2. Confirm staging API responds correctly (subscription endpoint with new \`LICENSE_SIGNING_PRIVATE_KEY\` secret already set in CF). 3. Merge this PR → auto-deploys API to production. 4. Trigger Release workflow: GitHub → Actions → Release → "Run workflow" → main. 5. Watch Build workflow for mac/win/linux completion. 6. Confirm the release un-drafts itself. ## Known risks / follow-ups - **Pre-existing Vercel preview failure for \`apps/web\`** — marketing site, scheduled to be extracted to its own repo (P3 in the roadmap). - **\`SUBSCRIPTION_PUBLIC_KEY\` is dev-grade** — generated in a Claude session. Before the licensing server emits envelopes for real paid users, rotate the keypair from a trusted machine and ship a follow-up release. - **Branch protection should require CodeRabbit completion before automerge** — added to the roadmap as a process item; this very PR was BLOCKED correctly because of that policy gap being closed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Summary
Lands the infrastructure for end-to-end testing the desktop app: a Playwright + Electron setup with per-test isolation, two initial specs, and a Linux CI job. The CI job ships as `continue-on-error: true` while we stabilize — flip it off in a follow-up once it's reliably green on develop.
Files
Why drive the preload bridge, not the editor UI
`notes.spec.ts` calls `window.readied.notes.*` directly via `page.evaluate` instead of typing in the CodeMirror surface. Reasons:
Future UI-level specs land once `MarkdownEditor.tsx` is split (PR-G follow-up).
Specs in this PR
`smoke.spec.ts`
`notes.spec.ts`
What's deliberately NOT here
Honest disclaimer
I scaffolded all of this but could NOT execute the suite end-to-end against a real Electron build in this session (no display attached, no built bundle in this branch). The CI's `continue-on-error` gate and the local README acknowledge that the first verifier on a real machine may need to tweak the specs once they meet a real renderer.
Test plan
Stack context
PR-E in the audit stack. Stacked on top of #276 (PR-F3) → #275 (PR-F2) → #274 (PR-F5) → #273 (PR-F4) → #272 (PR-F1) → #271 (PR-I) → #270 (PR-J) → #269 (PR-D) → #268 (PR-H) → #267 (PR-C) → #266 (PR-A) → #265 (PR-B). 13 PRs deep.
🤖 Generated with Claude Code