diff --git a/pi-web-plugins/updates/updatesLogic.test.ts b/pi-web-plugins/updates/updatesLogic.test.ts index 1a3066d..b1e5c5d 100644 --- a/pi-web-plugins/updates/updatesLogic.test.ts +++ b/pi-web-plugins/updates/updatesLogic.test.ts @@ -233,9 +233,11 @@ describe("fallbackDockerStatus", () => { const fallback = fallbackDockerStatus({ dockerMode: "dev" }, "generated"); expect(fallback?.generatedAt).toBe("generated"); expect(fallback?.components.web.installation).toEqual({ kind: "docker", dockerMode: "dev" }); - expect(fallback?.commands).toMatchObject({ + expect(fallback?.commands).toEqual({ update: "pi-web-docker --dev update", restart: "pi-web-docker --dev restart", + restartWeb: "pi-web-docker --dev restart-web", + restartSessiond: "pi-web-docker --dev restart-sessiond", status: "pi-web-docker --dev status", }); expect(fallback?.messages[0]?.id).toBe("docker-status-compatibility"); diff --git a/src/client/src/components/selectableRow.test.ts b/src/client/src/components/selectableRow.test.ts index a49255b..48d028e 100644 --- a/src/client/src/components/selectableRow.test.ts +++ b/src/client/src/components/selectableRow.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { activateSelectableRow, activateSelectableRowFromKeyboard, handleSelectableRowKeyboard } from "./selectableRow"; +import { activateSelectableRow, handleSelectableRowKeyboard } from "./selectableRow"; describe("selectable row activation", () => { it("activates rows from non-interactive click targets", () => { @@ -20,8 +20,8 @@ describe("selectable row activation", () => { const enter = keyboardEventWithPath("Enter", matchTarget(() => false)); const space = keyboardEventWithPath(" ", matchTarget(() => false)); - activateSelectableRowFromKeyboard(enter, enterAction); - activateSelectableRowFromKeyboard(space, spaceAction); + expect(handleSelectableRowKeyboard(enter, { activate: enterAction })).toBe(true); + expect(handleSelectableRowKeyboard(space, { activate: spaceAction })).toBe(true); expect(enterAction).toHaveBeenCalledOnce(); expect(spaceAction).toHaveBeenCalledOnce(); @@ -33,7 +33,7 @@ describe("selectable row activation", () => { const action = vi.fn(); const event = keyboardEventWithPath("Enter", matchTarget((selector: string) => selector.includes("button"))); - activateSelectableRowFromKeyboard(event, action); + expect(handleSelectableRowKeyboard(event, { activate: action })).toBe(false); expect(action).not.toHaveBeenCalled(); expect(event.preventDefault).not.toHaveBeenCalled(); diff --git a/src/client/src/components/selectableRow.ts b/src/client/src/components/selectableRow.ts index 1bda886..cbf7a90 100644 --- a/src/client/src/components/selectableRow.ts +++ b/src/client/src/components/selectableRow.ts @@ -11,8 +11,9 @@ const interactiveSelector = [ ].join(","); type ComposedPathEvent = Pick; -type SelectableKeyboardEvent = ComposedPathEvent & Pick; -type SelectableNavigationKeyboardEvent = SelectableKeyboardEvent & Partial>; +type SelectableNavigationKeyboardEvent = ComposedPathEvent + & Pick + & Partial>; export interface SelectableRowKeyboardOptions { activate: () => void; @@ -37,13 +38,6 @@ export function activateSelectableRow(event: ComposedPathEvent, action: () => vo action(); } -export function activateSelectableRowFromKeyboard(event: SelectableKeyboardEvent, action: () => void): void { - if (event.key !== "Enter" && event.key !== " ") return; - if (isFromInteractiveElement(event)) return; - event.preventDefault(); - action(); -} - export function handleSelectableRowKeyboard(event: SelectableNavigationKeyboardEvent, options: SelectableRowKeyboardOptions): boolean { if (isFromInteractiveElement(event)) return false; if (event.key === "Enter" || event.key === " ") { diff --git a/src/docker/piWebDockerDocs.test.ts b/src/docker/piWebDockerDocs.test.ts index 979e15e..682f112 100644 --- a/src/docker/piWebDockerDocs.test.ts +++ b/src/docker/piWebDockerDocs.test.ts @@ -37,10 +37,8 @@ describe("pi-web-docker documentation", () => { readRepoFile("docker/pi-web-docker"), ]); - for (const command of PI_WEB_DOCKER_USER_COMMANDS) { - expect(dockerReadme).toContain(`| \`${command}\` |`); - expect(dockerEntrypoint).toContain(command); - } + expect(readDockerCommandMatrix(dockerReadme)).toEqual([...PI_WEB_DOCKER_USER_COMMANDS]); + expect(readEntrypointCommandCases(dockerEntrypoint)).toEqual(new Set(PI_WEB_DOCKER_USER_COMMANDS)); expect(dockerReadme).toContain("`pi-web-docker --dev status`"); expect(dockerReadme).toContain("`./docker/pi-web-docker --dev start`"); @@ -49,6 +47,26 @@ describe("pi-web-docker documentation", () => { }); }); +function readDockerCommandMatrix(dockerReadme: string): string[] { + const commandMatrixSection = dockerReadme.split("### Command matrix\n")[1]?.split("\n### Installer options")[0] ?? ""; + return Array.from(commandMatrixSection.matchAll(/^\| `([^`]+)` \|/gm), (match) => { + const command = match[1]; + if (command === undefined) throw new Error("Docker command matrix row did not include a command"); + return command; + }); +} + +function readEntrypointCommandCases(dockerEntrypoint: string): Set { + const commandCaseBlock = dockerEntrypoint.slice(dockerEntrypoint.indexOf('case "$command_name" in')); + const commandCases = new Set(); + for (const line of commandCaseBlock.split("\n")) { + const match = /^ {2}([a-z][a-z-]*(?:\|[a-z][a-z-]*)*)(?:\|__run-detached)?\)$/.exec(line); + if (match?.[1] === undefined) continue; + for (const command of match[1].split("|")) commandCases.add(command); + } + return commandCases; +} + async function readRepoFile(relativePath: string): Promise { return await readFile(join(repoRoot, relativePath), "utf8"); } diff --git a/src/server/piWebPluginService.test.ts b/src/server/piWebPluginService.test.ts index 517973c..733cfe0 100644 --- a/src/server/piWebPluginService.test.ts +++ b/src/server/piWebPluginService.test.ts @@ -67,7 +67,10 @@ describe("PiWebPluginService", () => { const service = new PiWebPluginService({ roots: [{ path: join(tempDir, "plugins"), source: "test", scope: "local" }], packageProvider: false }); const manifest = await service.manifest(); - expect(manifest.plugins[0]?.module).toMatch(/^\/pi-web-plugins\/updates\/pi-web-plugin\.js\?v=\d+&piWebDockerMode=dev$/u); + const moduleUrl = new URL(manifest.plugins[0]?.module ?? "", "http://pi-web.test"); + expect(moduleUrl.pathname).toBe("/pi-web-plugins/updates/pi-web-plugin.js"); + expect(moduleUrl.searchParams.get("v")).toMatch(/^\d+$/u); + expect(moduleUrl.searchParams.get("piWebDockerMode")).toBe("dev"); }); it("discovers Pi package plugins through an injected package provider", async () => { diff --git a/src/shared/activity.test.ts b/src/shared/activity.test.ts index 7135a22..badf315 100644 --- a/src/shared/activity.test.ts +++ b/src/shared/activity.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { isSessionActive, sessionActivityLabel, isWorkspaceActivityActive } from "./activity"; +import { isSessionActive, isWorkspaceActivityActive } from "./activity"; import type { SessionStatus, WorkspaceActivity } from "./apiTypes"; const idleStatus: SessionStatus = { @@ -14,17 +14,10 @@ const idleStatus: SessionStatus = { }; describe("activity helpers", () => { - it("detects and labels active session states consistently", () => { + it("detects active session states", () => { expect(isSessionActive(idleStatus)).toBe(false); - expect(sessionActivityLabel(idleStatus)).toBeUndefined(); - expect(isSessionActive({ ...idleStatus, isStreaming: true })).toBe(true); - expect(sessionActivityLabel({ ...idleStatus, isStreaming: true })).toBe("streaming"); - expect(isSessionActive({ ...idleStatus, pendingMessageCount: 2 })).toBe(true); - expect(sessionActivityLabel({ ...idleStatus, pendingMessageCount: 2 })).toBe("2 pending"); - - expect(sessionActivityLabel(idleStatus, { sessionId: "s1", phase: "active", label: "running tool", detail: "read", at: "now" })).toBe("running tool: read"); }); it("detects workspace activity presence without exposing details", () => { diff --git a/src/shared/activity.ts b/src/shared/activity.ts index dd97ae8..657b34b 100644 --- a/src/shared/activity.ts +++ b/src/shared/activity.ts @@ -8,16 +8,6 @@ export function isSessionActive(status?: SessionStatus, activity?: SessionActivi || (status?.pendingMessageCount ?? 0) > 0; } -export function sessionActivityLabel(status?: SessionStatus, activity?: SessionActivity): string | undefined { - if (activity?.phase === "active") return activity.detail !== undefined && activity.detail !== "" ? `${activity.label}: ${activity.detail}` : activity.label; - if (status === undefined) return undefined; - if (status.isCompacting) return "compacting"; - if (status.isBashRunning) return "bash"; - if (status.isStreaming) return "streaming"; - if (status.pendingMessageCount > 0) return `${String(status.pendingMessageCount)} pending`; - return undefined; -} - export function isWorkspaceActivityActive(activity: WorkspaceActivity | undefined): boolean { return activity !== undefined && (activity.hasSessionActivity || activity.hasTerminalActivity); }