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
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user