Files
kandev/apps/web/components/session-commands.test.tsx
T
2026-09-17 19:00:34 +01:00

170 lines
5.5 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import type { TFunction } from "i18next";
import { t as translate } from "@/lib/i18n";
import type { CommandItem } from "@/lib/commands/types";
import {
buildGitCommands,
buildPanelCommands,
buildSessionCommands,
buildTaskCommands,
buildWorkspaceCommands,
} from "./session-commands";
const t = translate as unknown as TFunction;
const ARCHIVE_COMMAND_ID = "task-archive";
/**
* A `t` that echoes the key it was asked for. Any label or group that did NOT
* go through the catalog comes back as plain English instead, which is what the
* "every … resolves through the catalog" tests below detect. A palette entry
* whose copy is inlined renders correct English and passes lint, so an assertion
* on the English itself cannot tell the two apart.
*/
const markerT = ((key: string) => `«${key}»`) as unknown as TFunction;
const unresolved = (commands: CommandItem[]) =>
commands.flatMap((cmd) =>
[
["label", cmd.label],
["group", cmd.group],
["inputPlaceholder", cmd.inputPlaceholder],
]
.filter(([, value]) => typeof value === "string" && !value.startsWith("«"))
.map(([field, value]) => `${cmd.id}.${field} = ${value}`),
);
function build(overrides: Partial<Parameters<typeof buildTaskCommands>[0]> = {}) {
return buildTaskCommands({
activeTaskId: "task-A",
isTaskArchived: false,
t,
openNewAgent: vi.fn(),
openSubtask: vi.fn(),
requestArchive: vi.fn(),
...overrides,
});
}
describe("buildTaskCommands", () => {
it("offers an archive command that matches typing 'archive'", () => {
const archive = build().find((cmd) => cmd.id === ARCHIVE_COMMAND_ID);
expect(archive).toBeDefined();
expect(archive?.label).toBe("Archive task");
expect(archive?.group).toBe("Tasks");
expect(archive?.keywords).toContain("archive");
});
it("groups the archive command with the other task commands", () => {
const commands = build();
const subtask = commands.find((cmd) => cmd.id === "subtask-create");
const archive = commands.find((cmd) => cmd.id === ARCHIVE_COMMAND_ID);
expect(archive?.group).toBe(subtask?.group);
});
it("runs the archive request rather than archiving directly", () => {
const requestArchive = vi.fn();
build({ requestArchive })
.find((cmd) => cmd.id === ARCHIVE_COMMAND_ID)
?.action?.();
expect(requestArchive).toHaveBeenCalledTimes(1);
});
it("closes the palette before opening standalone archive confirmation", () => {
const archive = build().find((cmd) => cmd.id === ARCHIVE_COMMAND_ID);
expect(archive?.keepOpen).not.toBe(true);
expect(archive?.confirmation).toBeUndefined();
});
it("hides the archive command for an archived task", () => {
const commands = build({ isTaskArchived: true });
expect(commands.find((cmd) => cmd.id === ARCHIVE_COMMAND_ID)).toBeUndefined();
expect(commands.find((cmd) => cmd.id === "subtask-create")).toBeDefined();
});
it("registers no task commands without an active task", () => {
expect(build({ activeTaskId: null })).toEqual([]);
});
});
describe("session palette copy", () => {
const gitOptions = {
git: { push: vi.fn(), pull: vi.fn(), rebase: vi.fn(), merge: vi.fn() },
baseBranch: "origin/main",
openCommitDialog: vi.fn(),
openPRDialog: vi.fn(),
runGitWithFeedback: vi.fn(),
};
it("resolves every session, git, workspace and panel entry through the catalog", () => {
const commands = [
...buildSessionCommands(true, vi.fn(), markerT),
...buildGitCommands({
...gitOptions,
git: gitOptions.git as unknown as Parameters<typeof buildGitCommands>[0]["git"],
t: markerT,
}),
...buildWorkspaceCommands("session-A", markerT),
...buildPanelCommands(
{
addBrowser: vi.fn(),
addTerminal: vi.fn(),
addPlan: vi.fn(),
addChanges: vi.fn(),
} as unknown as Parameters<typeof buildPanelCommands>[0],
false,
markerT,
),
...buildTaskCommands({
activeTaskId: "task-A",
isTaskArchived: false,
t: markerT,
openNewAgent: vi.fn(),
openSubtask: vi.fn(),
requestArchive: vi.fn(),
}),
];
expect(unresolved(commands)).toEqual([]);
});
it("labels the git operation toast with the same catalog entry as the palette row", () => {
const runGitWithFeedback = vi.fn();
const push = buildGitCommands({
...gitOptions,
git: gitOptions.git as unknown as Parameters<typeof buildGitCommands>[0]["git"],
runGitWithFeedback,
t: markerT,
}).find((cmd) => cmd.id === "git-push");
push?.action?.();
// The second argument is the operation name `useGitWithFeedback` composes
// its toast titles around; it used to be a hardcoded "Push".
expect(runGitWithFeedback).toHaveBeenCalledWith(expect.any(Function), push?.label);
});
it("keeps every group of a builder identical so the palette does not split it", () => {
const groups = new Set(
buildGitCommands({
...gitOptions,
git: gitOptions.git as unknown as Parameters<typeof buildGitCommands>[0]["git"],
t: markerT,
}).map((cmd) => cmd.group),
);
expect(groups.size).toBe(1);
});
it("omits the task cancel entry while Quick Chat owns the foreground", () => {
const commands = buildSessionCommands(true, vi.fn(), markerT, { suppressCancel: true });
expect(commands.find((command) => command.id === "session-cancel")).toBeUndefined();
});
});