Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
de470aa
test: move the cli end-to-end suite into e2e/cli
lorem-dev Sep 8, 2026
46d6160
docs: fix stale e2e paths left by the cli suite move
lorem-dev Sep 8, 2026
4dc5b1c
test: boot the renderer under a scripted backend
lorem-dev Sep 8, 2026
21fe82d
fix: address boot-smoke-test review findings
lorem-dev Sep 8, 2026
3a7593e
test: give the desktop suite a scenario-driven fixture
lorem-dev Sep 8, 2026
b235175
fix(e2e): zero animations earlier and prove the harness wiring
lorem-dev Sep 8, 2026
6637fa4
test: cover adding a repository and a failing clone
lorem-dev Sep 8, 2026
bb83fce
fix(desktop): stop treating a duplicate add as success
lorem-dev Sep 9, 2026
983cdd3
test: cover browsing, installing, and skill dependencies
lorem-dev Sep 9, 2026
e1ca532
fix(desktop): keep skills-page layout inert, tighten flows 3/11
lorem-dev Sep 9, 2026
df26bbb
test: cover tracking a project and a missing folder
lorem-dev Sep 9, 2026
34b303b
test: cover the settings page and an offered update
lorem-dev Sep 9, 2026
d0c4dc2
fix(e2e): drive config validity from the scenario
lorem-dev Sep 9, 2026
d56512c
test: cover installing, updating, and a refused mcp update
lorem-dev Sep 9, 2026
af5ba4e
test: rebuild mcp update regression test on the real path
lorem-dev Sep 9, 2026
09b10d3
test: prove the unmocked-command guard can actually fail
lorem-dev Sep 9, 2026
3be4def
style: run prettier over drifted renderer and e2e files
lorem-dev Sep 9, 2026
68eebc8
ci: run the desktop ui suite on every pull request
lorem-dev Sep 9, 2026
818a84a
fix(ci): scope the cli e2e job to the cli half only
lorem-dev Sep 9, 2026
cfd88f9
docs(e2e): repoint stale test:e2e refs at the cli half
lorem-dev Sep 9, 2026
8d139eb
fix(desktop): unstick repo-add cancel and notif log cutoff
lorem-dev Sep 9, 2026
4cf1504
fix(desktop): kind-only toast testid, unique skill tree node ids
lorem-dev Sep 9, 2026
ce7e5d1
test(e2e): strengthen mcp/projects assertions, fix a false claim
lorem-dev Sep 9, 2026
ea15fe6
docs: name test:e2e:cli as the submodule's actual trigger
lorem-dev Sep 9, 2026
d9331a3
docs(e2e): drop pointers to git-ignored design artifacts
lorem-dev Sep 9, 2026
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
22 changes: 12 additions & 10 deletions .agents/skills/check-fixture-repo/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ its own README explains what each fixture drives.

## Isolation

The suite never touches the developer's machine state. `e2e/src/cli.ts` is the
The suite never touches the developer's machine state. `e2e/cli/src/cli.ts` is the
only way a spec can invoke the CLI, and it always sets **both**:

- `XDG_CONFIG_HOME`, which relocates `state.json` and `config.yaml`
Expand All @@ -42,13 +42,13 @@ the machine -- which is why the harness owns them rather than each spec.
### 1. Run the suite

```bash
pnpm test:e2e
pnpm test:e2e:cli
```

That is the whole check. The script behind it
(`scripts/e2e-prepare.mjs`) initializes the fixture submodule, force-pulls it to
the tip of its branch, and builds `target/debug/skillkeeper`; Jest then runs the
specs in `e2e/tests/`.
specs in `e2e/cli/tests/`.

Set `SKILLKEEPER_E2E_PIN_FIXTURE=1` to run against the fixture commit this
repository pins instead of pulling. CI does that for reproducibility; locally
Expand All @@ -61,11 +61,13 @@ to look:

| spec | covers | a failure means |
|---|---|---|
| `e2e/tests/fixture.spec.ts` | the submodule is checked out, ASCII-only, and still has the manifests and file modes the rest of the suite assumes | the **fixture** drifted |
| `e2e/tests/skills.spec.ts` | resolution schemes, `.skid.yml` identity, nested body paths, selective `+x`, guidance precedence, hook merge and consent, the delimited-text region, and both silent-failure modes of the resolver | the **product** changed |
| `e2e/tests/mcp.spec.ts` | preset discovery including the group-scoped file, parameter substitution, both ledger files, the `.gitignore` guard for the secrets file, rules rendering, instance-name allocation, the Codex stdio-only skip, and removal | the **product** changed |
| `e2e/tests/repair.spec.ts` | `verify` -> `repair` -> `verify`, directory pruning, the bounds that keep repair inside the repaired skill, and uninstall reversing hooks and guidance | the **product** changed |
| `e2e/tests/requires.spec.ts` | skill dependencies: every `repo lint` code the `requires` group triggers, the single-document `--json` form, both target-misuse exits, the transitive install closure, and the uninstall breakage report | the **product** changed |
| `e2e/cli/tests/fixture.spec.ts` | the submodule is checked out, ASCII-only, and still has the manifests and file modes the rest of the suite assumes | the **fixture** drifted |
| `e2e/cli/tests/skills.spec.ts` | resolution schemes, `.skid.yml` identity, nested body paths, selective `+x`, guidance precedence, hook merge and consent, the delimited-text region, and both silent-failure modes of the resolver | the **product** changed |
| `e2e/cli/tests/mcp.spec.ts` | preset discovery including the group-scoped file, parameter substitution, both ledger files, the `.gitignore` guard for the secrets file, rules rendering, instance-name allocation, the Codex stdio-only skip, and removal | the **product** changed |
| `e2e/cli/tests/mcp-oauth.spec.ts` | the oauth preset's exact per-agent native shape, the copilot skip, that no agent's config ever carries a client secret, and `repo lint` on the deliberately invalid oauth preset | the **product** changed |
| `e2e/cli/tests/mcp-parameters.spec.ts` | link rendering, description truncation, option-value validation on install, and every mcp lint warning a description or parameter can trigger | the **product** changed |
| `e2e/cli/tests/repair.spec.ts` | `verify` -> `repair` -> `verify`, directory pruning, the bounds that keep repair inside the repaired skill, and uninstall reversing hooks and guidance | the **product** changed |
| `e2e/cli/tests/requires.spec.ts` | skill dependencies: every `repo lint` code the `requires` group triggers, the single-document `--json` form, both target-misuse exits, the transitive install closure, and the uninstall breakage report | the **product** changed |

If `fixture.spec.ts` fails, fix or re-pin the fixture. If it passes and another
spec fails, the CLI's behaviour moved and the fixture is telling you so.
Expand All @@ -90,7 +92,7 @@ git -C examples/test-repo status --porcelain # expect clean
```

Every spec runs the CLI with throwaway `HOME` and `XDG_CONFIG_HOME`
(`e2e/src/cli.ts`), so a dirty tree here means a test wrote somewhere it should
(`e2e/cli/src/cli.ts`), so a dirty tree here means a test wrote somewhere it should
not have -- a blocker, and a harness defect rather than a product one.

Note that a force-pull may legitimately leave the submodule pointer moved; that
Expand All @@ -103,7 +105,7 @@ commit the bump on its own.
Jest already reports per-test results, so summarize rather than restate:

```
pnpm test:e2e: PASS / FAIL (N passed, N failed of 51)
pnpm test:e2e:cli: PASS / FAIL (N passed, N failed of 67)
failing spec(s): <file> -> <test name>
attributed to: fixture drift / product change / harness defect
working tree clean after: yes / no
Expand Down
69 changes: 68 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ jobs:
- name: Type-check
run: pnpm typecheck

- name: Check formatting
run: pnpm format:check

- name: Test with coverage (90% gate)
# vitest run --coverage enforces the thresholds defined in vitest.config.ts.
# The build fails if lines or branches fall below 90%.
Expand Down Expand Up @@ -262,10 +265,74 @@ jobs:

# The suite drives only the CLI crate, which does not link the webview, so
# the Tauri system libraries the `rust` job installs are not needed here.
# Explicitly the CLI half, not `pnpm test:e2e`: this job has no
# Playwright browser install, so the desktop half would fail with
# "browserType.launch: Executable doesn't exist" here. The desktop half
# runs in its own `desktop-e2e` job below, which does install Chromium.
- name: Run the end-to-end suite
# Pin the fixture to the commit this repository records: a CI run must be
# reproducible and fail for reasons in the diff, not because the fixture
# moved. Locally the same script force-pulls instead.
env:
SKILLKEEPER_E2E_PIN_FIXTURE: '1'
run: pnpm test:e2e
run: pnpm test:e2e:cli

desktop-e2e:
name: Desktop UI (Playwright)
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
lfs: false
submodules: false

- name: Enable pnpm via corepack
run: corepack enable

- name: Set up Node.js with pnpm cache
uses: actions/setup-node@v5
with:
node-version: '24'
cache: pnpm

- name: Install dependencies (frozen lockfile)
run: pnpm install --frozen-lockfile

- name: Generate localization catalogs
run: pnpm run i18n

# Keyed on the whole lockfile, not just the Playwright package version:
# a browser build is ~150MB, so caching it is worth doing, but the key
# deliberately errs coarse. Keying on a parsed-out Playwright version
# instead would risk a bump the parsing misses reusing a stale,
# mismatched browser binary; hashing the full lockfile instead means an
# occasional needless re-download on an unrelated dependency bump,
# which is the safe direction to be wrong in.
- name: Cache the Playwright browser
id: playwright-cache
uses: actions/cache@v6
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}

- name: Install Chromium
run: pnpm exec playwright install --with-deps chromium

# No separate "build the renderer" step here: playwright.config.ts
# chains `frontend:build` into the webServer command so every run
# boots a fresh bundle, and a build failure surfaces as the webServer
# failing to come up. A second, earlier build step would only repeat
# that work.
- name: Run the desktop UI suite
run: pnpm test:e2e:desktop

- name: Upload the report on failure
if: failure()
uses: actions/upload-artifact@v7
with:
name: playwright-report
path: playwright-report/
retention-days: 7
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ Thumbs.db
# Storybook
apps/desktop/storybook-static/

# Playwright: `outputDir`'s default walks up from the config file's directory
# (e2e/desktop/) to the nearest package.json and appends test-results/ to
# THAT directory -- today that lands at the repository root (e2e/desktop/ has
# no package.json of its own), but that is one `e2e/desktop/package.json` away
# from moving to e2e/desktop/test-results/ (see e2e/cli/package.json for a
# sibling suite that already has one). No leading slash, so these match
# wherever it actually lands rather than assuming which.
test-results/
playwright-report/
blob-report/

# Documentation site (mkdocs + uv): uv caches the tools globally, but keep any
# local Python virtualenv and the rendered static site out of git.
.venv/
Expand Down
12 changes: 9 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,16 +88,22 @@ All of these must pass before a pull request is ready.
### End-to-end suite

```bash
pnpm test:e2e # Jest, drives the built CLI against examples/test-repo
pnpm test:e2e:cli # Jest, drives the built CLI against examples/test-repo
```

Separate from the gate above because it needs the fixture submodule and a
`cargo build`. It covers what the in-memory fakes cannot: the real binary against
a real working tree. Run it after touching resolution, install, hooks, guidance,
or MCP. Specs live in `e2e/tests/`, the harness in `e2e/src/cli.ts`; the runner is
or MCP. Specs live in `e2e/cli/tests/`, the harness in `e2e/cli/src/cli.ts`; the runner is
Jest (not Vitest) and the suite is scoped to CommonJS -- see
[docs/development/development.md](./docs/development/development.md#end-to-end-tests).

There is a second, independent suite, `pnpm test:e2e:desktop` (Playwright,
drives the renderer against a scripted backend; no filesystem, git, or
network). `pnpm test:e2e` runs both in sequence. See the same section of
[docs/development/development.md](./docs/development/development.md#end-to-end-tests)
for its layout.

**Build the CLI with `cargo build -p skillkeeper-cli`, never a bare
`cargo build`.** Two crates in this workspace produce a binary named
`skillkeeper`: the CLI declares it as `[[bin]]`, and the desktop app crate
Expand All @@ -108,7 +114,7 @@ Jest (not Vitest) and the suite is scoped to CommonJS -- see
Run the desktop app from that path expecting the CLI and you get a GUI process
waiting in the window event loop: no output, no error, indistinguishable from a
hang. Building the CLI package puts the right binary back, cache or not, so
`pnpm test:e2e` is safe on this (it builds the CLI package and then asserts the
`pnpm test:e2e:cli` is safe on this (it builds the CLI package and then asserts the
binary answers `--version`). A hand-run `./target/debug/skillkeeper` right after
working on the desktop app is where this bites.

Expand Down
16 changes: 15 additions & 1 deletion apps/desktop/src/renderer/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ export function App() {
<div
className={cx('sk-app', `sk-app--${platform}`, onboardingActive && 'sk-app--onboarding')}
data-anim={animationMode}
data-testid="app-shell"
>
<WindowChrome />
<ConfigBanner />
Expand All @@ -302,7 +303,13 @@ export function App() {
drag/traffic-light zone, so it renders a draggable panel there. */}
<Sidebar dragRegion={platform === 'mac'}>
{NAV_ITEMS.map(({ id, key }) => (
<SidebarItem key={id} icon={<Icon name={id} />} active={activeView === id} onClick={() => goTo(id)}>
<SidebarItem
key={id}
icon={<Icon name={id} />}
active={activeView === id}
onClick={() => goTo(id)}
data-testid={`nav-${id}`}
>
{t(key)}
</SidebarItem>
))}
Expand All @@ -314,6 +321,7 @@ export function App() {
icon={<Icon name="skills" />}
className={cx('sk-sidebar-item--group', skillsOpen && 'sk-sidebar-item--group--open')}
onClick={() => setSkillsOpen((open) => !open)}
data-testid="nav-group-skills"
>
{t('nav.skills')}
<Icon name="chevron-right" size={14} className="sk-nav-group__chevron" />
Expand All @@ -332,13 +340,15 @@ export function App() {
className="sk-sidebar-item--sub"
active={activeView === 'skills-components'}
onClick={() => goTo('skills-components')}
data-testid="nav-skills-components"
>
{t('skills.componentsTitle')}
</SidebarItem>
<SidebarItem
className="sk-sidebar-item--sub"
active={activeView === 'skills-management'}
onClick={() => goTo('skills-management')}
data-testid="nav-skills-management"
>
{t('skills.managementTitle')}
</SidebarItem>
Expand All @@ -356,6 +366,7 @@ export function App() {
icon={<Icon name="mcp" />}
className={cx('sk-sidebar-item--group', mcpOpen && 'sk-sidebar-item--group--open')}
onClick={() => setMcpOpen((open) => !open)}
data-testid="nav-group-mcp"
>
{t('nav.mcp')}
<Icon name="chevron-right" size={14} className="sk-nav-group__chevron" />
Expand All @@ -374,13 +385,15 @@ export function App() {
className="sk-sidebar-item--sub"
active={activeView === 'mcp-components'}
onClick={() => goTo('mcp-components')}
data-testid="nav-mcp-components"
>
{t('mcp.componentsTitle')}
</SidebarItem>
<SidebarItem
className="sk-sidebar-item--sub"
active={activeView === 'mcp-management'}
onClick={() => goTo('mcp-management')}
data-testid="nav-mcp-management"
>
{t('mcp.managementTitle')}
</SidebarItem>
Expand All @@ -392,6 +405,7 @@ export function App() {
icon={<Icon name="settings" />}
active={activeView === 'settings'}
onClick={() => goTo('settings')}
data-testid="nav-settings"
>
{t('nav.settings')}
</SidebarItem>
Expand Down
16 changes: 12 additions & 4 deletions apps/desktop/src/renderer/entities/project/ui/ProjectCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export function ProjectCard({
}: ProjectCardProps) {
const washHue = hueFromName(project.name);
return (
<Card className="sk-project-card">
<Card className="sk-project-card" data-testid="project-card">
{/* Decorative left wash: a blurred, scaled copy of the project icon when
there is one, else a soft colour field keyed to the project name. It
fades to transparent toward the centre. The name-keyed gradient is
Expand Down Expand Up @@ -135,7 +135,9 @@ export function ProjectCard({
<ProjectIcon iconUrl={iconUrl} name={project.name} size={18} className="sk-project-card__leading-icon" />
<div className="sk-project-card__main">
<span className="sk-project-card__name-row">
<span className="sk-project-card__name">{truncateEnd(project.name, NAME_MAX)}</span>
<span className="sk-project-card__name" data-project-id={project.id}>
{truncateEnd(project.name, NAME_MAX)}
</span>
<AnimatePresence initial={false}>
{missing === true && (
<motion.span
Expand All @@ -146,7 +148,11 @@ export function ProjectCard({
transition={transitionFast}
>
<Tooltip content={missingLabel}>
<span className="sk-project-card__missing-dot" aria-label={missingLabel} />
<span
className="sk-project-card__missing-dot"
aria-label={missingLabel}
data-testid="project-card-folder-missing"
/>
</Tooltip>
</motion.span>
)}
Expand Down Expand Up @@ -191,7 +197,9 @@ export function ProjectCard({
)}
{agentsLabel !== undefined && (
<Tooltip content={agentsHint ?? ''}>
<Badge tone="neutral">{agentsLabel}</Badge>
<Badge tone="neutral" data-testid="project-card-agents">
{agentsLabel}
</Badge>
</Tooltip>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,9 @@ export function RepositoryCard({
<Card className="sk-repo-card">
<div className="sk-repo-card__main">
<span className="sk-repo-card__name-row">
<span className="sk-repo-card__name">{repository.name}</span>
<span className="sk-repo-card__name" data-repo-name={repository.name}>
{repository.name}
</span>
<AnimatePresence mode="wait" initial={false}>
{indicatorKey !== null && (
<motion.span
Expand Down Expand Up @@ -164,6 +166,7 @@ export function RepositoryCard({
<button
type="button"
className="sk-repo-card__branch"
data-testid="repo-row-branch"
onClick={onBranchClick}
aria-label={branchCopyLabel}
>
Expand Down
Loading
Loading