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
35 changes: 28 additions & 7 deletions .cursor/rules/og-playwright.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ alwaysApply: true

Social preview images (1200×630 WebP) are generated by the **isolated** `website/og/` package. Playwright is intentionally **not** in the root `package.json` (CI stays lean).

## One-time human setup (Holger's machine)
## One-time human setup (Holger's machine only)

From `feelyourprotocol/website/`:

Expand All @@ -16,16 +16,37 @@ npm run og:setup # npm install in og/ + download Chromium (~once per machine)
npm run og:check # should print: OK: Playwright Chromium is installed and launchable.
```

Re-run `og:setup` after Playwright major upgrades or on a new machine. Browsers live in the normal OS cache (`~/Library/Caches/ms-playwright` on macOS), not in the repo.
Re-run `og:setup` after Playwright major upgrades or on a new machine. Browsers live in the normal OS cache (`~/Library/Caches/ms-playwright` on macOS), not in the repo. Successful `og:check` writes `og/.chromium-ready.json` (gitignored) so agents can tell setup already happened.

## Agent rules
**Holger has already run this.** Do not run setup again unless he explicitly asks.

1. **Never** run ad-hoc `npx playwright install`, `node -e` with playwright, or GenerateImage as a substitute for OG pipeline output.
2. **Always** use the npm scripts from `website/`:
`og/` CLIs auto-redirect Cursor's sandbox `PLAYWRIGHT_BROWSERS_PATH` to the developer's real browser cache — that is why `required_permissions: ["all"]` plus **never** running `og:setup` is the correct agent workflow.

## Agent rules (mandatory)

### NEVER do these

- `npm run og:setup`, `npm run setup --prefix og`, or any `playwright install`
- Ad-hoc `npx playwright install`, sandbox browser downloads, or `GenerateImage` as a substitute for OG pipeline output
- Retry setup when capture fails — that is almost never the fix

### ALWAYS do these

1. Run OG/capture commands with **`required_permissions: ["all"]`** so the agent uses the developer's installed browsers — the Cursor sandbox cache does not have Chromium.
2. Use the npm scripts from `website/`:
- `npm run generate:og:exploration -- <id>`
- `npm run generate:og:topic -- <id>`
- `npm run generate:og:all`
3. Run OG commands with **`required_permissions: ["all"]`** so the agent uses the developer's installed browsers — the Cursor sandbox cache does not have Chromium.
4. If generation fails with "Playwright Chromium is not ready", **stop and ask the user** to run `npm run og:setup` locally. Do not attempt to install browsers inside the agent sandbox.
- `npm run capture:social [-- hero timeline board]` — roadmap Twitter cards (see `roadmap/social/README.md`)
3. Optional fast check (no browser launch): `npm run og:preflight` with `required_permissions: ["all"]`
- `status: ready` → proceed with capture/generate
- `status: needs_agent_permissions` → re-run the **same** command with `required_permissions: ["all"]`, not setup
- `status: needs_human_setup` → stop and ask the user to run `npm run og:check` in their terminal

### When capture/generate fails

1. If the error mentions **`required_permissions`** or **`Do NOT run og:setup`**: re-run with `required_permissions: ["all"]`.
2. If it still fails after `all` permissions: ask the user to run `npm run og:check` locally and paste the output. **Do not** run setup yourself.
3. If the error is a **selector timeout** or **404 on assets**: that is a code/path bug — fix the app or `og/src/social/config.ts`, not Playwright setup.

See `og/README.md` for full usage.
39 changes: 39 additions & 0 deletions .cursor/rules/tests-with-new-code.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
description: Add focused tests when introducing new modules or tooling
alwaysApply: true
---

# Tests with new code

When you add **new modules, scripts, or non-trivial features** (not one-line fixes or copy-only edits), include a **solid but proportionate** test layer before calling the task done.

## What to test

- **Pure logic** — parsers, URL builders, config, data transforms: unit tests with clear inputs/outputs.
- **Vue components** — mount with `@vue/test-utils`; stub heavy children; assert structure, links, and key copy.
- **Cross-package contracts** — one source of truth (e.g. shared ids/enums); test that consumers stay aligned.
- **Skip** — one-off scripts, pure config/docs, or UI tweaks with no testable logic.

## Where tests live

Mirror production code:

```
src/libs/foo.ts → src/libs/__tests__/foo.spec.ts
roadmap/social/src/… → roadmap/social/src/__tests__/…spec.ts
og/src/social/… → og/src/social/__tests__/…spec.ts
```

## Before finishing

1. Run targeted tests: `npx vitest run path/to/__tests__/`
2. Run the **full CI suite locally**: `npm run test:unit:ci` (all `src/`, `community-token/`, `roadmap/`, `og/` specs).
3. Run `npm run lf:ci` from `feelyourprotocol/website/`.

Prefer **few well-named tests** over exhaustive coverage — test behavior that would hurt if it regressed.

GitHub Actions (`.github/workflows/unit.yml`) runs `npm run test:unit:ci` on every push/PR — no per-folder CI wiring; match the include globs in `vitest.config.ts`.

## Playwright / capture pipelines

Do **not** require Playwright in CI for screenshot tools. Unit-test parsing, paths, and component rendering; leave browser capture as a documented manual/`og:setup` step (see `.cursor/rules/og-playwright.mdc`).
2 changes: 1 addition & 1 deletion .github/workflows/unit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@ jobs:

- name: Run unit tests
if: github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'skip tests')
run: npx vitest run
run: npm run test:unit:ci
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ npm run og:check

See [og/README.md](./og/README.md).

### Roadmap Twitter cards

Thread visuals (timeline, roadmap board, hero) — see [roadmap/social/README.md](./roadmap/social/README.md):

```bash
npm run capture:social
```

## Deployment

Production builds (`dist/website`, `dist/docs`, `dist/community-token`, `dist/roadmap`) are **not** in the repo — the server runs `npm run build:deploy` after `git pull`. See `server-config/deployment/fyp_deploy.sh`.
Expand Down
1 change: 1 addition & 0 deletions og/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules
.tmp
.chromium-ready.json
39 changes: 36 additions & 3 deletions og/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ Browsers are stored in the **OS user cache** (e.g. `~/Library/Caches/ms-playwrig
on macOS), not in git. Root `npm install` does **not** install `og/` deps — you
must run `og:setup` explicitly.

`og/` entry scripts redirect Cursor's sandbox `PLAYWRIGHT_BROWSERS_PATH` to your
real user cache automatically — agents should not need setup again after you run
`og:check` once.

### When to re-run setup

- New machine or fresh clone
Expand All @@ -36,6 +40,7 @@ From `website/`:
```bash
npm run generate:og:exploration -- eip-7594
npm run generate:og:topic -- scaling
npm run generate:og:roadmap
npm run generate:og:all
```

Expand All @@ -50,8 +55,36 @@ public/og/manifest.json
The main site's SEO layer reads `manifest.json` and falls back to
`public/og/default.webp` when a specific image has not been generated yet.

## Roadmap social cards (Twitter / threads)

Timeline, roadmap board, and hero cards for `@FeelEthereum` threads — built from
the same Vue components as the live roadmap. See
[`roadmap/social/README.md`](../roadmap/social/README.md).

```bash
npm run capture:social
```

Output: `roadmap/social/out/{hero,timeline,board}.{png,webp}`

## Tests

```bash
npx vitest run roadmap/social/src/__tests__/ og/src/social/__tests__/
```

Unit tests cover card registry, CLI parsing, paths, and Vue frame rendering (no Playwright in CI).

## Cursor / agent note

AI agents should use the npm scripts above with full shell permissions (`all`),
not install Playwright in a sandbox. If Chromium is missing, run `og:setup`
locally — see `.cursor/rules/og-playwright.mdc`.
AI agents must **not** run `og:setup` or `playwright install`. Use the npm scripts below with full shell permissions (`all`). If Chromium is missing, ask the human to run `og:setup` — see `.cursor/rules/og-playwright.mdc`.

Quick agent preflight (no browser launch):

```bash
npm run og:preflight
```

- `status: ready` — proceed with `generate:og:*` or `capture:social`
- `status: needs_agent_permissions` — re-run with `required_permissions: ["all"]`
- `status: needs_human_setup` — ask the user to run `npm run og:check`
2 changes: 2 additions & 0 deletions og/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"scripts": {
"setup": "npm install && npx playwright install chromium",
"check": "node --experimental-strip-types src/check-cli.ts",
"preflight": "node --experimental-strip-types src/preflight-cli.ts",
"capture:social:run": "node --experimental-strip-types src/social-cli.ts",
"generate": "node --experimental-strip-types src/cli.ts"
},
"dependencies": {
Expand Down
26 changes: 26 additions & 0 deletions og/src/__tests__/bootstrap-playwright-env.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, expect, it } from 'vitest'

import {
bootstrapPlaywrightEnv,
defaultPlaywrightCacheDir,
isCursorSandboxBrowsersPath,
} from '../bootstrap-playwright-env.ts'

describe('bootstrap-playwright-env', () => {
it('detects Cursor sandbox browser paths', () => {
expect(
isCursorSandboxBrowsersPath(
'/var/folders/xx/cursor-sandbox-cache/abc/playwright/chromium',
),
).toBe(true)
expect(isCursorSandboxBrowsersPath('/Users/holger/Library/Caches/ms-playwright')).toBe(false)
})

it('redirects sandbox PLAYWRIGHT_BROWSERS_PATH to user cache', () => {
const sandboxPath = '/tmp/cursor-sandbox-cache/abc/playwright'
process.env.PLAYWRIGHT_BROWSERS_PATH = sandboxPath
bootstrapPlaywrightEnv()
expect(process.env.PLAYWRIGHT_BROWSERS_PATH).toBe(defaultPlaywrightCacheDir())
delete process.env.PLAYWRIGHT_BROWSERS_PATH
})
})
72 changes: 72 additions & 0 deletions og/src/__tests__/chromium-status.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
import { afterEach, describe, expect, it } from 'vitest'

import {
AGENT_PERMISSIONS_HINT,
CHROMIUM_STAMP_PATH,
formatChromiumStatus,
launchFailureStatus,
OG_PACKAGE_ROOT,
playwrightPackageRoot,
readChromiumStamp,
} from '../chromium-status-core.ts'

describe('chromium status (CI-safe — no playwright import)', () => {
afterEach(() => {
if (existsSync(CHROMIUM_STAMP_PATH)) rmSync(CHROMIUM_STAMP_PATH)
})

it('playwrightPackageRoot resolves under og/', () => {
expect(playwrightPackageRoot()).toBe(`${OG_PACKAGE_ROOT}/node_modules/playwright`)
})

it('reads and writes chromium stamp', () => {
const stamp = {
checkedAt: '2026-06-30T12:00:00.000Z',
executablePath: '/tmp/chromium',
playwrightVersion: '1.61.1',
}
mkdirSync(OG_PACKAGE_ROOT, { recursive: true })
writeFileSync(CHROMIUM_STAMP_PATH, JSON.stringify(stamp), 'utf8')
expect(readChromiumStamp()).toEqual(stamp)
})
})

describe('formatChromiumStatus', () => {
it('never tells agents to run og:setup when permissions are the issue', () => {
const msg = formatChromiumStatus({
kind: 'needs_agent_permissions',
executablePath: '/Users/holger/.cache/ms-playwright/chromium-1234/chrome',
stamp: {
checkedAt: '2026-06-30T12:00:00.000Z',
executablePath: '/Users/holger/.cache/ms-playwright/chromium-1234/chrome',
playwrightVersion: '1.61.1',
},
})
expect(msg).toContain(AGENT_PERMISSIONS_HINT)
expect(msg).not.toContain('npm run og:setup')
})

it('tells humans to run setup when og deps are missing', () => {
const msg = formatChromiumStatus({ kind: 'og_deps_missing' })
expect(msg).toContain('npm run og:setup')
expect(msg).not.toContain('required_permissions')
})
})

describe('launchFailureStatus', () => {
it('prefers agent permissions hint when stamp proves prior successful check', () => {
const status = launchFailureStatus('spawn EACCES', {
kind: 'ready',
executablePath: '/tmp/chromium',
stamp: {
checkedAt: '2026-06-30T12:00:00.000Z',
executablePath: '/tmp/chromium',
playwrightVersion: '1.61.1',
},
})
expect(status.kind).toBe('needs_agent_permissions')
expect(formatChromiumStatus(status)).toContain('required_permissions')
expect(formatChromiumStatus(status)).not.toContain('npm run og:setup')
})
})
24 changes: 24 additions & 0 deletions og/src/__tests__/generate-roadmap-og.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'

import { OG_HEIGHT, OG_WIDTH, WEBSITE_ROOT } from '../config.ts'
import {
ROADMAP_OG_OUTPUT,
ROADMAP_OG_PUBLIC_DIR,
ROADMAP_OG_RENDER_HTML,
} from '../roadmap-og-paths.ts'

describe('roadmap OG generator paths', () => {
it('render template and output resolve under roadmap/public/og', () => {
expect(ROADMAP_OG_PUBLIC_DIR).toBe(resolve(WEBSITE_ROOT, 'roadmap/public/og'))
expect(ROADMAP_OG_RENDER_HTML).toBe(resolve(ROADMAP_OG_PUBLIC_DIR, 'render.html'))
expect(ROADMAP_OG_OUTPUT).toBe(resolve(ROADMAP_OG_PUBLIC_DIR, 'default.webp'))
})

it('render.html declares standard OG viewport size', () => {
const html = readFileSync(ROADMAP_OG_RENDER_HTML, 'utf8')
expect(html).toContain(`width: ${OG_WIDTH}px`)
expect(html).toContain(`height: ${OG_HEIGHT}px`)
})
})
23 changes: 23 additions & 0 deletions og/src/__tests__/server.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, expect, it } from 'vitest'

import { resolveStaticFile } from '../server.ts'

describe('resolveStaticFile', () => {
const root = '/tmp/fyp-social/dist'

it('maps / to index.html', () => {
expect(resolveStaticFile(root, '/')).toBe(`${root}/index.html`)
})

it('maps empty path to index.html', () => {
expect(resolveStaticFile(root, '')).toBe(`${root}/index.html`)
})

it('maps asset paths without double root', () => {
expect(resolveStaticFile(root, '/assets/index.js')).toBe(`${root}/assets/index.js`)
})

it('rejects path traversal', () => {
expect(() => resolveStaticFile(root, '/../secret')).toThrow(/escapes static root/)
})
})
40 changes: 40 additions & 0 deletions og/src/bootstrap-playwright-env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { existsSync } from 'node:fs'
import { homedir, platform } from 'node:os'
import { join } from 'node:path'

/** Playwright cache dir for the logged-in user (not Cursor's sandbox copy). */
export function defaultPlaywrightCacheDir(home = homedir()): string {
switch (platform()) {
case 'darwin':
return join(home, 'Library/Caches/ms-playwright')
case 'win32':
return join(home, 'AppData', 'Local', 'ms-playwright')
default:
return join(home, '.cache', 'ms-playwright')
}
}

export function isCursorSandboxBrowsersPath(path: string): boolean {
return /cursor-sandbox-cache|cursor-sandbox/i.test(path)
}

/**
* Cursor injects PLAYWRIGHT_BROWSERS_PATH pointing at an empty sandbox cache.
* Redirect to the developer's real cache so og:check / capture use Holger's Chromium.
* Must run before any `playwright` import (side-effect import this module first).
*/
export function bootstrapPlaywrightEnv(): void {
const current = process.env.PLAYWRIGHT_BROWSERS_PATH ?? ''
const userCache = defaultPlaywrightCacheDir()

if (current && isCursorSandboxBrowsersPath(current)) {
process.env.PLAYWRIGHT_BROWSERS_PATH = userCache
return
}

if (!current && existsSync(userCache)) {
process.env.PLAYWRIGHT_BROWSERS_PATH = userCache
}
}

bootstrapPlaywrightEnv()
Loading