Files
paseo/packages/desktop/e2e/skills-selection.spec.ts
T
Mohamed BoudraandGitHub 5a30affd7f Skip unrelated required checks without losing their names (#2542)
* fix(ci): skip unrelated required checks cleanly

Matrix jobs had to start no-op runners to preserve interpolated required-check names. Static named jobs let GitHub report genuine skipped conclusions while keeping those names stable.

* fix(ci): gate changes to install patching script

* fix(ci): route cross-package test contracts narrowly

* fix(ci): keep routed contracts merge-blocking

* fix(server): exclude nested test utilities from builds

* fix(ci): cover app-owned native and browser domains

* refactor(ci): align tests with runtime domains

Own browser specs, desktop specs, and Electron-only app code by stable directories so PR routing does not depend on filename conventions. Keep desktop integration coverage inside the required desktop check.

* fix(ci): keep routing and desktop suites isolated

The routing regression must run before dependency installation, and desktop Vitest must not collect Playwright-owned specs.

* fix(ci): isolate desktop e2e runtime

* perf(test): isolate Playwright workers

* perf(test): parallelize Playwright within specs

* fix(test): keep Playwright file scheduling

* fix(test): share dynamic Metro fixture

* fix(test): preserve own-host restart fixtures

* fix(ci): preserve routed contracts after rebase

* Stabilize Playwright wheel-scroll assertions
2026-08-02 00:56:05 +08:00

334 lines
12 KiB
TypeScript

import { test as base } from "../../app/e2e/support/fixtures";
import { expect } from "@playwright/test";
import { gotoAppShell } from "../../app/e2e/support/helpers/app";
import { installDesktopRuntime } from "./support/runtime";
import { getServerId } from "../../app/e2e/support/helpers/server-id";
import {
cancelSkillSelection,
chooseAllSkills,
chooseCustomSkills,
createSkillsSandbox,
expectAllSkillsSelected,
expectNoRemovalWarning,
expectRemovalWarning,
expectSavedSkillSelection,
expectSaveErrorKeepsSheetOpen,
expectSelectedSkills,
expectSkillChoices,
expectSkillFilePresent,
expectSkillPresentIn,
expectSkillSelectionOpen,
expectSkillsInstalled,
openSkillsIntegrations,
openSkillSelection,
saveSkillSelection,
toggleSkill,
serveRealSkillsCommands,
type SkillsSandbox,
type SkillsSandboxOptions,
} from "./support/skills-selection";
interface SkillsSandboxSetup extends SkillsSandboxOptions {
/** What the desktop confirm dialog answers when the removal warning fires. */
confirmRemoval?: boolean;
}
type StartSkills = (setup?: SkillsSandboxSetup) => Promise<SkillsSandbox>;
// Every skills command runs against the real desktop handlers over a temp bundle
// and temp user data, so the assertions cover persistence and convergence, not a
// browser stand-in for them.
const test = base.extend<{ startSkills: StartSkills }>({
startSkills: async ({ page }, provide) => {
const sandboxes: SkillsSandbox[] = [];
await provide(async (setup = {}) => {
const { confirmRemoval, ...options } = setup;
const sandbox = await createSkillsSandbox(options);
await installDesktopRuntime(page, {
serverId: getServerId(),
confirmShouldAccept: confirmRemoval ?? false,
});
await serveRealSkillsCommands(page, sandbox);
sandboxes.push(sandbox);
return sandbox;
});
for (const sandbox of sandboxes) {
await sandbox.cleanup();
}
},
});
test.describe("Choosing installed skills", () => {
test("installs only the skills selected in Settings", async ({ page, startSkills }) => {
const skills = await startSkills();
await gotoAppShell(page);
await openSkillsIntegrations(page);
await openSkillSelection(page);
await chooseCustomSkills(page, ["paseo", "paseo-loop"]);
await saveSkillSelection(page);
await expectSkillsInstalled(skills, ["paseo", "paseo-loop"]);
await expectSelectedSkills(page, ["paseo", "paseo-loop"]);
});
test("all skills includes every available skill", async ({ page, startSkills }) => {
const skills = await startSkills();
await gotoAppShell(page);
await openSkillsIntegrations(page);
await openSkillSelection(page);
await chooseCustomSkills(page, ["paseo"]);
await saveSkillSelection(page);
await openSkillSelection(page);
await chooseAllSkills(page);
await saveSkillSelection(page);
await expectSkillsInstalled(skills, skills.bundledSkills);
await expectAllSkillsSelected(page);
});
test("cancelling leaves the installed skills untouched", async ({ page, startSkills }) => {
const skills = await startSkills();
await gotoAppShell(page);
await openSkillsIntegrations(page);
await openSkillSelection(page);
await chooseCustomSkills(page, ["paseo"]);
await saveSkillSelection(page);
await openSkillSelection(page);
await toggleSkill(page, "paseo-loop");
await cancelSkillSelection(page);
await expectSkillsInstalled(skills, ["paseo"]);
await expectSelectedSkills(page, ["paseo"]);
});
test("a failed save keeps the sheet open with an error", async ({ page, startSkills }) => {
const skills = await startSkills({ blockAgentsDir: true });
await gotoAppShell(page);
await openSkillsIntegrations(page);
await openSkillSelection(page);
await chooseCustomSkills(page, ["paseo"]);
await saveSkillSelection(page);
await expectSaveErrorKeepsSheetOpen(page);
await expectSkillsInstalled(skills, []);
});
test("an empty selection does not offer an Install action that cannot install anything", async ({
page,
startSkills,
}) => {
await startSkills({ installed: { mode: "all" }, confirmRemoval: true });
await gotoAppShell(page);
await openSkillsIntegrations(page);
await openSkillSelection(page);
await chooseCustomSkills(page, []);
await saveSkillSelection(page);
// The remaining Install action belongs to the command-line integration.
await expect(page.getByRole("button", { name: "Install", exact: true })).toHaveCount(1);
await expect(page.getByRole("button", { name: "Choose skills", exact: true })).toBeVisible();
});
test("a selection containing only retired skills does not offer a no-op Install", async ({
page,
startSkills,
}) => {
await startSkills({ installed: { mode: "custom", skills: ["retired-skill"] } });
await gotoAppShell(page);
await openSkillsIntegrations(page);
// The remaining Install action belongs to the command-line integration.
await expect(page.getByRole("button", { name: "Install", exact: true })).toHaveCount(1);
await expect(page.getByRole("button", { name: "Choose skills", exact: true })).toBeVisible();
});
test("an empty bundled catalog does not offer a no-op Install", async ({ page, startSkills }) => {
await startSkills({ bundledSkills: [] });
await gotoAppShell(page);
await openSkillsIntegrations(page);
// The remaining Install action belongs to the command-line integration.
await expect(page.getByRole("button", { name: "Install", exact: true })).toHaveCount(1);
await expect(page.getByRole("button", { name: "Choose skills", exact: true })).toBeVisible();
});
test("cannot dismiss the sheet while Save is pending", async ({ page, startSkills }) => {
const skills = await startSkills({ holdSave: true });
await gotoAppShell(page);
await openSkillsIntegrations(page);
await openSkillSelection(page);
await chooseCustomSkills(page, ["paseo"]);
await saveSkillSelection(page);
await skills.waitForHeldSave();
await page.keyboard.press("Escape");
await expectSkillSelectionOpen(page);
skills.releaseHeldSave();
await expect(page.getByRole("switch", { name: "All skills", exact: true })).toHaveCount(0);
});
});
test.describe("Removing an installed skill", () => {
test("delete-only drift stays in Choose skills instead of Update", async ({
page,
startSkills,
}) => {
await startSkills({
installed: { mode: "custom", skills: ["paseo"] },
placeOnDisk: {
skill: "paseo-loop",
target: "claude",
files: { "SKILL.md": "# paseo-loop\n", "notes/mine.md": "my notes" },
},
});
await gotoAppShell(page);
await openSkillsIntegrations(page);
await expect(page.getByText("Update available", { exact: true })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Update", exact: true })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Choose skills", exact: true })).toBeVisible();
});
test("warns before deleting the skill folders", async ({ page, startSkills }) => {
const skills = await startSkills({ installed: { mode: "all" }, confirmRemoval: false });
await gotoAppShell(page);
await openSkillsIntegrations(page);
await openSkillSelection(page);
await chooseCustomSkills(page, ["paseo", "paseo-advisor"]);
await saveSkillSelection(page);
await expectRemovalWarning(page, ["paseo-loop"]);
await expectSkillSelectionOpen(page);
await expectSkillsInstalled(skills, skills.bundledSkills);
await expectSavedSkillSelection(skills, { mode: "all" });
});
test("confirming the warning removes the skill", async ({ page, startSkills }) => {
const skills = await startSkills({ installed: { mode: "all" }, confirmRemoval: true });
await gotoAppShell(page);
await openSkillsIntegrations(page);
await openSkillSelection(page);
await chooseCustomSkills(page, ["paseo", "paseo-advisor"]);
await saveSkillSelection(page);
await expectRemovalWarning(page, ["paseo-loop"]);
await expectSkillsInstalled(skills, ["paseo", "paseo-advisor"]);
await expectSelectedSkills(page, ["paseo", "paseo-advisor"]);
});
test("warns for a skill installed in only one agent directory", async ({ page, startSkills }) => {
const skills = await startSkills({
installed: { mode: "all" },
keepOnlyIn: { skill: "paseo-loop", target: "claude" },
confirmRemoval: false,
});
await gotoAppShell(page);
await openSkillsIntegrations(page);
await openSkillSelection(page);
await chooseCustomSkills(page, ["paseo", "paseo-advisor"]);
await saveSkillSelection(page);
await expectRemovalWarning(page, ["paseo-loop"]);
await expectSkillSelectionOpen(page);
await expectSkillPresentIn(skills, "claude", "paseo-loop");
await expectSavedSkillSelection(skills, { mode: "all" });
});
test("warns for a skill on disk that the saved selection never mentioned", async ({
page,
startSkills,
}) => {
// The committed preference excludes paseo-loop, but the directory is back —
// a manual reinstall or an external sync. Saving any edit deletes it.
const skills = await startSkills({
installed: { mode: "custom", skills: ["paseo"] },
placeOnDisk: {
skill: "paseo-loop",
target: "claude",
files: { "SKILL.md": "# paseo-loop\n", "notes/mine.md": "my notes" },
},
confirmRemoval: false,
});
await gotoAppShell(page);
await openSkillsIntegrations(page);
await openSkillSelection(page);
await toggleSkill(page, "paseo-advisor");
await saveSkillSelection(page);
await expectRemovalWarning(page, ["paseo-loop"]);
await expectSkillSelectionOpen(page);
await expectSkillFilePresent(skills, "claude", "paseo-loop", "notes/mine.md");
await expectSavedSkillSelection(skills, { mode: "custom", skills: ["paseo"] });
});
test("warns about a directory that appears after the sheet was opened", async ({
page,
startSkills,
}) => {
const skills = await startSkills({
installed: { mode: "custom", skills: ["paseo"] },
confirmRemoval: false,
});
await gotoAppShell(page);
await openSkillsIntegrations(page);
await openSkillSelection(page);
// Nothing to delete when the sheet opened; the host looks again at save time.
await skills.placeSkillOnDisk("paseo-loop", "claude");
await toggleSkill(page, "paseo-advisor");
await saveSkillSelection(page);
await expectRemovalWarning(page, ["paseo-loop"]);
await expectSkillSelectionOpen(page);
await expectSkillPresentIn(skills, "claude", "paseo-loop");
await expectSavedSkillSelection(skills, { mode: "custom", skills: ["paseo"] });
});
test("keeps draft choices when fresher status arrives while the sheet is open", async ({
page,
startSkills,
}) => {
const skills = await startSkills({ installed: { mode: "all" }, confirmRemoval: false });
await gotoAppShell(page);
await openSkillsIntegrations(page);
await openSkillSelection(page);
await chooseCustomSkills(page, ["paseo", "paseo-advisor"]);
// Another window commits a different selection. Saving picks that up, so the
// sheet is handed a selection it did not start from while an edit is open.
await skills.commitSelectionExternally({ mode: "custom", skills: ["paseo", "paseo-loop"] }, [
"paseo-advisor",
]);
await saveSkillSelection(page);
await expectRemovalWarning(page, ["paseo-loop"]);
await expectSkillChoices(page, ["paseo", "paseo-advisor"]);
});
test("adding a skill saves without a warning", async ({ page, startSkills }) => {
const skills = await startSkills({
installed: { mode: "custom", skills: ["paseo"] },
confirmRemoval: false,
});
await gotoAppShell(page);
await openSkillsIntegrations(page);
await openSkillSelection(page);
await toggleSkill(page, "paseo-loop");
await saveSkillSelection(page);
await expectSkillsInstalled(skills, ["paseo", "paseo-loop"]);
await expectNoRemovalWarning(page);
});
});