Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 58 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Most experienced developers (90%+) follow these practices:
| Use `laststance` GitHub/Vercel org | Deploying to wrong account, billing issues |
| Run `pnpm validate` before commit | Breaking production builds |
| Build before E2E tests (`pnpm build`) | False test failures |
| **QA after implementing, before ship** | Shipping code you never confirmed works |
| Check Context7 for library APIs | Using deprecated patterns |
| Never use `any` type | Runtime type errors |
| Write all code and comments in English | Consistency across codebase |
Expand All @@ -29,12 +30,39 @@ Most experienced developers (90%+) follow these practices:
| Skip pre-commit checks | CI failures, broken releases |
| Mock auth/DB in E2E tests | Tests pass locally, fail in production |
| Commit with ESLint warnings | husky blocks commit |
| Ship (merge/deploy) before QA passes | Broken feature in prod the user finds |

> **Scope note:** the `ryota-murakami` prohibition is about the **Vercel** (deploy +
> billing) account only. The `ryota-murakami` **GitHub** account is the repo
> admin/owner — it is the correct identity for SSH pushes and PR creation on
> `laststance/corelive`. Deploys must always target the `laststance` Vercel org.

## Definition of Done — QA before ship (必須 / NON-NEGOTIABLE)
Comment thread
ryota-murakami marked this conversation as resolved.

Writing the code is **not** "done". The order is always:

> **implement → QA (confirm it actually works) → only then ship (merge / deploy).**

You never ship something you have not confirmed works. After **any** implementation,
**both** of these are required before you merge or deploy:

1. **E2E tests** — run the E2E covering the changed paths (`pnpm e2e:web`,
`pnpm e2e:electron`). When a path can only be proven on CI (e.g. web E2E can't
boot locally on this Mac), the **green CI E2E run is the gate** — wait for it.
2. **Local QA** — actually launch the app and exercise the change by hand / eye:
- Web / renderer → drive it in the browser (or the renderer MCP).
- Electron **native** surfaces (menu / tray / dock / traffic-lights /
always-on-top / multi-window / **cross-window sync** / deep links) → run the
**packaged** app and drive the real desktop with `mcp__computer-use__*`
(see **Electron Native QA** below).

**Terminology**: call this **QA** (or "local QA" / "native QA"). Do **NOT** call it a
"smoke test" / "smoke" — that word is rejected.

If local QA genuinely cannot run yet (e.g. a native multi-window path with no local
harness), **do not silently defer it and ship anyway, and do not present unverified
work as shipped**. Say so plainly and get the user's explicit OK before merging.

## Services

| Service | Environment | URL/Identifier |
Expand Down Expand Up @@ -182,13 +210,13 @@ xvfb-run -a pnpm e2e:electron

macOS users do **not** need xvfb — Electron uses the native display. Just run `pnpm e2e:electron` directly.

> **Coverage note**: Linux + xvfb does **not** cover Cocoa-specific paths (dock/menu bar, `app.setActivationPolicy`, `open-url` deep links, vibrancy, notarized .app/asar paths). Run a manual macOS smoke before tag pushes.
> **Coverage note**: Linux + xvfb does **not** cover Cocoa-specific paths (dock/menu bar, `app.setActivationPolicy`, `open-url` deep links, vibrancy, notarized .app/asar paths). Run a manual macOS QA before tag pushes.

## Electron Native QA (macOS, computer-use)

Electron ships as a **native macOS app**. Its Cocoa chrome — menu bar (`MenuManager`), system tray (`SystemTrayManager`), dock + `app.setActivationPolicy` (`main.ts`), `open-url` deep links (`DeepLinkManager`), traffic-light window controls, vibrancy, and the floating / braindump / startup-pill windows (`WindowManager`) — is **invisible to the Playwright and `mcp__electron__*` suites**: those drive the **renderer (web content) only**, identical to the web app. The Linux + xvfb CI is renderer coverage; the native surfaces have **no automated coverage at all**.

So QA those surfaces **locally on macOS with the `mcp__computer-use__*` MCP** — it screenshots and drives the real desktop, so it can click the menu bar / tray / dock / traffic lights that DOM tools cannot reach. This IS the "manual macOS smoke" the Coverage note above asks for.
So QA those surfaces **locally on macOS with the `mcp__computer-use__*` MCP** — it screenshots and drives the real desktop, so it can click the menu bar / tray / dock / traffic lights that DOM tools cannot reach. This IS the "manual macOS QA" the Coverage note above asks for.

**When**: before any tag push / release, and after touching Electron main-process or native-integration code (menu, tray, dock, window controls, deep links, auto-update, vibrancy, `setActivationPolicy`).

Expand All @@ -204,6 +232,34 @@ So QA those surfaces **locally on macOS with the `mcp__computer-use__*` MCP**

> **Tooling split**: `mcp__electron__*` & Playwright = renderer/DOM (same paths as web). `mcp__computer-use__*` = native Cocoa chrome (macOS only). Reach for computer-use whenever the thing under test lives **outside** the web content.

### Dev (`electron:dev`) vs Production: which Electron is running?

The `pnpm electron:dev` / `pnpm dev`-launched Electron and the packaged/installed app
can run **side by side** — they're given **distinct bundle IDs on purpose** so their
`corelive://` deep-link handlers never collide (`electron/devProtocol.ts:43`,
`DEV_BUNDLE_ID`). Tell them apart by any of these:

| Signal | `electron:dev` (dev) | Production (packaged / installed) |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| **Bundle ID** | `com.corelive.app.dev` | `com.corelive.app` (`electron-builder.json` appId) |
| **App / proc name** | **Electron** — runs from `node_modules/electron/dist/Electron.app` | **CoreLive** — runs from `…/CoreLive.app` |
| **Renderer URL** | `http://localhost:4991/home` (dev `serverUrl`, `WindowManager.ts:476`) | `https://corelive.app/home` |
| **`app.isPackaged`** | `false` (`SystemTrayManager.ts:405`) | `true` |
| **`NODE_ENV`** | `development` (`WindowManager.ts:148`) | **unset** — gate on `app.isPackaged`, never `NODE_ENV` |
| **computer-use** | ❌ **not grantable** (`request_access` → `not_installed`; unregistered in LaunchServices) → **filtered out of every screenshot** | ✅ grantable as **CoreLive** (full tier) |

Identify a live one by bundle ID: `osascript -e 'id of app "CoreLive"'`, or
`lsappinfo info -only bundleid <pid>`.

**Why this matters for native QA (the reason step 1 builds the packaged app):**
`mcp__computer-use__*` can only screenshot apps macOS considers _installed_. The dev
Electron runs from `node_modules`, so neither `com.corelive.app.dev` nor the display
name "Electron" can be granted — its floating / braindump / main windows are
**invisible to every screenshot**. The packaged `CoreLive.app` is the only build the
compositor will capture. (Always-on-top / z-order is pure main-process `NSWindow`
behavior, identical dev≡packaged — but only packaged is screenshot-testable, so the
native QA MUST run against the packaged app.)

## Electron Production Build

**Prerequisites**: `APPLE_ID` / `APPLE_APP_SPECIFIC_PASSWORD` / `APPLE_TEAM_ID` in `.env`, "Developer ID Application" cert in Keychain, (optional) `GH_TOKEN` for releases.
Expand Down
125 changes: 125 additions & 0 deletions e2e/electron/braindump-window.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* End-to-end coverage for the BrainDump window IPC surface.
*
* Verifies that the renderer→IPC→main-process path wires the brainDump
* namespace correctly: window toggle/show, opacity read/write, sync mode,
* and shortcut config. Native Cocoa behaviors (always-on-top, vibrancy) are
* covered by local macOS native QA.
*
* Note: tests share one Electron app instance (beforeAll) and depend on
* execution order (toggle-open → then check visibility). This is an
* intentional tradeoff — Electron app launch is expensive per spec file.
*/

import { expect, test } from '@playwright/test'
import type { ElectronApplication, Page } from 'playwright'

import { LOAD_TIMEOUT_MS, setupElectronTest } from './_helpers/launch'

let electronApp: ElectronApplication
let mainWindow: Page

test.beforeAll(async () => {
;({ electronApp, mainWindow } = await setupElectronTest('braindump-window'))
})

test.afterAll(async () => {
await electronApp?.close()
})

test('braindump opacity is within valid 0.30–1.00 bounds by default', async () => {
// Arrange + Act
const opacity = await mainWindow.evaluate(async () => {
const getFn = window.electronAPI?.brainDump?.getOpacity
if (!getFn) throw new Error('brainDump.getOpacity not in preload bridge')
return getFn()
})

// Assert: main process clamps opacity to 0.30–1.00 on read
expect(opacity).toBeGreaterThanOrEqual(0.3)
expect(opacity).toBeLessThanOrEqual(1.0)
})

test('braindump sync mode is readable after app start', async () => {
// Arrange + Act
const syncMode = await mainWindow.evaluate(async () => {
const getFn = window.electronAPI?.brainDump?.getSyncMode
if (!getFn) throw new Error('brainDump.getSyncMode not in preload bridge')
return getFn()
})

// Assert: sync mode is a boolean (on or off)
expect(typeof syncMode).toBe('boolean')
})

test('braindump keyboard shortcut setting is readable after app start', async () => {
// Arrange + Act
const shortcut = await mainWindow.evaluate(async () => {
const getFn = window.electronAPI?.brainDump?.getShortcut
if (!getFn) throw new Error('brainDump.getShortcut not in preload bridge')
return getFn()
})

// Assert: shortcut is a string (empty string when no shortcut is configured)
expect(typeof shortcut).toBe('string')
})

test('opening braindump creates a new browser window', async () => {
// Arrange: watch for the new window before calling toggle
const newWindowPromise = electronApp.waitForEvent('window', {
timeout: LOAD_TIMEOUT_MS,
})

// Act
await mainWindow.evaluate(async () => {
const toggleFn = window.electronAPI?.brainDump?.toggle
if (!toggleFn) throw new Error('brainDump.toggle not in preload bridge')
await toggleFn()
})

const braindumpWindow = await newWindowPromise

// Assert: a second window was opened
expect(braindumpWindow).toBeTruthy()
expect(electronApp.windows().length).toBeGreaterThanOrEqual(2)
})

test('aux visibility reports the braindump as visible after opening', async () => {
// Arrange: braindump was shown by the previous test
// Act
const visibility = await mainWindow.evaluate(async () => {
const getFn = window.electronAPI?.window?.getAuxVisibility
if (!getFn) throw new Error('getAuxVisibility not in preload bridge')
return getFn()
})

// Assert
expect(visibility.braindump).toBe(true)
})

test('setting braindump opacity to 0.75 persists the exact value', async () => {
// Arrange: 0.75 is in the valid 0.30–1.00 range, so it must not be clamped
const targetOpacity = 0.75

// Act
const appliedOpacity = await mainWindow.evaluate(async (opacity) => {
const setFn = window.electronAPI?.brainDump?.setOpacity
if (!setFn) throw new Error('brainDump.setOpacity not in preload bridge')
return setFn(opacity)
}, targetOpacity)

// Assert: main process returns the exact value when it is in range
expect(appliedOpacity).toBe(0.75)
})

test('updating braindump sync mode persists without error', async () => {
// Arrange + Act: disable sync mode
const result = await mainWindow.evaluate(async () => {
const setFn = window.electronAPI?.brainDump?.setSyncMode
if (!setFn) throw new Error('brainDump.setSyncMode not in preload bridge')
return setFn(false)
})

// Assert: IPC handler returns true on success
expect(result).toBe(true)
})
114 changes: 114 additions & 0 deletions e2e/electron/floating-window.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* End-to-end coverage for the Floating Navigator window IPC surface.
*
* Verifies that the renderer→IPC→main-process path creates/shows/hides the
* floating window, and that getAuxVisibility reflects the current state.
* Native Cocoa chrome (always-on-top, vibrancy, space membership) is covered
* by local macOS native QA; only DOM/IPC-layer behaviors are tested here.
*
* Note: tests in this file share one Electron app instance (beforeAll) and
* depend on execution order because each window toggle changes shared state.
* This is an intentional tradeoff — Electron app launch is expensive, and
* the toggle sequence (hidden → shown → hidden) is itself the feature path.
*/

import { expect, test } from '@playwright/test'
import type { ElectronApplication, Page } from 'playwright'

import { LOAD_TIMEOUT_MS, setupElectronTest } from './_helpers/launch'

let electronApp: ElectronApplication
let mainWindow: Page

test.beforeAll(async () => {
;({ electronApp, mainWindow } = await setupElectronTest('floating-window'))
})

test.afterAll(async () => {
await electronApp?.close()
})

test('auxiliary windows start hidden on a fresh app launch', async () => {
// Arrange + Act: fresh app — no toggle has been called
const visibility = await mainWindow.evaluate(async () => {
const getFn = window.electronAPI?.window?.getAuxVisibility
if (!getFn) throw new Error('getAuxVisibility not in preload bridge')
return getFn()
})

// Assert: neither floating nor braindump are visible at startup
expect(visibility.floating).toBe(false)
expect(visibility.braindump).toBe(false)
})

test('opening the floating navigator creates a new browser window', async () => {
// Arrange: listen for a new window before calling toggle
const newWindowPromise = electronApp.waitForEvent('window', {
timeout: LOAD_TIMEOUT_MS,
})

// Act
await mainWindow.evaluate(async () => {
const toggleFn = window.electronAPI?.window?.toggleFloatingNavigator
if (!toggleFn)
throw new Error('toggleFloatingNavigator not in preload bridge')
await toggleFn()
})

const floatingWindow = await newWindowPromise

// Assert: a second window was created
expect(floatingWindow).toBeTruthy()
expect(electronApp.windows().length).toBeGreaterThanOrEqual(2)
})

test('aux visibility reports the floating navigator as visible after opening', async () => {
// Arrange: floating window was shown by the previous test (same electronApp instance)
// Act
const visibility = await mainWindow.evaluate(async () => {
const getFn = window.electronAPI?.window?.getAuxVisibility
if (!getFn) throw new Error('getAuxVisibility not in preload bridge')
return getFn()
})

// Assert
expect(visibility.floating).toBe(true)
})

test('showing the floating navigator when already open does not create a duplicate window', async () => {
// Arrange: floating is already visible (from previous test)
const windowsBefore = electronApp.windows().length

// Act — showFloatingNavigator should not create a second floating window
await mainWindow.evaluate(async () => {
const showFn = window.electronAPI?.window?.showFloatingNavigator
if (!showFn) throw new Error('showFloatingNavigator not in preload bridge')
await showFn()
})

// Assert: window count did not increase
expect(electronApp.windows().length).toBe(windowsBefore)
})

test('hiding the floating navigator makes aux visibility report it as hidden', async () => {
// Arrange: floating is visible
const windowCountBefore = electronApp.windows().length

// Act
await mainWindow.evaluate(async () => {
const hideFn = window.electronAPI?.window?.hideFloatingNavigator
if (!hideFn) throw new Error('hideFloatingNavigator not in preload bridge')
await hideFn()
})

// Assert: aux visibility reports hidden
const visibility = await mainWindow.evaluate(async () => {
if (!window.electronAPI?.window?.getAuxVisibility)
throw new Error('getAuxVisibility not in preload bridge')
return window.electronAPI.window.getAuxVisibility()
})
expect(visibility.floating).toBe(false)

// Window count must stay exactly the same (hidden, not destroyed or re-created)
expect(electronApp.windows().length).toBe(windowCountBefore)
})
Loading