Archived
test: audit existing suite
This commit is contained in:
@@ -80,15 +80,27 @@ describe("ViewportPositionRepairer", () => {
|
||||
const timer = firstMapEntry(scheduler.timers);
|
||||
expect(timer[1].delayMs).toBe(VIEWPORT_POSITION_REPAIR_DELAY_MS);
|
||||
|
||||
scheduler.documentElement.scrollTop = 56;
|
||||
scheduler.body.scrollTop = 78;
|
||||
scheduler.runAnimationFrame(firstFrame);
|
||||
expect(scheduler.scrollCalls).toHaveLength(2);
|
||||
expect(scheduler.scrollCalls).toEqual([[0, 0], [0, 0]]);
|
||||
expect(scheduler.documentElement.scrollTop).toBe(0);
|
||||
expect(scheduler.body.scrollTop).toBe(0);
|
||||
const secondFrame = firstMapKey(scheduler.animationFrames);
|
||||
|
||||
scheduler.documentElement.scrollTop = 90;
|
||||
scheduler.body.scrollTop = 123;
|
||||
scheduler.runAnimationFrame(secondFrame);
|
||||
expect(scheduler.scrollCalls).toHaveLength(3);
|
||||
expect(scheduler.scrollCalls).toEqual([[0, 0], [0, 0], [0, 0]]);
|
||||
expect(scheduler.documentElement.scrollTop).toBe(0);
|
||||
expect(scheduler.body.scrollTop).toBe(0);
|
||||
|
||||
scheduler.documentElement.scrollTop = 34;
|
||||
scheduler.body.scrollTop = 12;
|
||||
scheduler.runTimer(timer[0]);
|
||||
expect(scheduler.scrollCalls).toHaveLength(4);
|
||||
expect(scheduler.scrollCalls).toEqual([[0, 0], [0, 0], [0, 0], [0, 0]]);
|
||||
expect(scheduler.documentElement.scrollTop).toBe(0);
|
||||
expect(scheduler.body.scrollTop).toBe(0);
|
||||
});
|
||||
|
||||
it("replaces pending scheduled repairs", () => {
|
||||
@@ -102,6 +114,10 @@ describe("ViewportPositionRepairer", () => {
|
||||
|
||||
expect(scheduler.canceledAnimationFrames).toEqual([firstFrame]);
|
||||
expect(scheduler.clearedTimers).toEqual([firstTimer]);
|
||||
expect(scheduler.animationFrames.has(firstFrame)).toBe(false);
|
||||
expect(scheduler.timers.has(firstTimer)).toBe(false);
|
||||
expect(scheduler.animationFrames.size).toBe(1);
|
||||
expect(scheduler.timers.size).toBe(1);
|
||||
});
|
||||
|
||||
it("clears pending work when repair is no longer needed", () => {
|
||||
@@ -115,5 +131,7 @@ describe("ViewportPositionRepairer", () => {
|
||||
|
||||
expect(scheduler.canceledAnimationFrames).toEqual([firstFrame]);
|
||||
expect(scheduler.clearedTimers).toEqual([firstTimer]);
|
||||
expect(scheduler.animationFrames.size).toBe(0);
|
||||
expect(scheduler.timers.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,9 +57,12 @@ describe("cached new sessions", () => {
|
||||
rememberCachedNewSession(baseSession, "local", storage);
|
||||
rememberCachedNewSession({ ...baseSession, id: "other", cwd: "/other" }, "local", storage);
|
||||
|
||||
expect(mergeCachedNewSessions("/repo", [], "local", storage).map((session) => session.id)).toEqual(["session-1"]);
|
||||
expect(mergeCachedNewSessions("/repo", [baseSession], "local", storage).map((session) => session.id)).toEqual(["session-1"]);
|
||||
expect(isCachedNewSessionInfo(mergeCachedNewSessions("/repo", [baseSession], "local", storage)[0])).toBe(false);
|
||||
const cachedOnly = mergeCachedNewSessions("/repo", [], "local", storage);
|
||||
const mergedWithServerSession = mergeCachedNewSessions("/repo", [baseSession], "local", storage);
|
||||
|
||||
expect(cachedOnly.map((session) => session.id)).toEqual(["session-1"]);
|
||||
expect(mergedWithServerSession.map((session) => session.id)).toEqual(["session-1"]);
|
||||
expect(isCachedNewSessionInfo(mergedWithServerSession[0])).toBe(false);
|
||||
expect(loadCachedNewSessions(storage).map((session) => session.id)).toEqual(["other"]);
|
||||
});
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ describe("ChatScrollController", () => {
|
||||
expect(JSON.parse(storage.getItem(key) ?? "{}")).toEqual({ mode: "bottom" });
|
||||
});
|
||||
|
||||
it("captures the session id when scheduling a delayed save", () => {
|
||||
it("cancels the previous delayed save and passes the latest session id", () => {
|
||||
const scheduler = new ManualScheduler();
|
||||
const controller = new ChatScrollController(new MemoryScrollStorage(), scheduler);
|
||||
const saved: string[] = [];
|
||||
|
||||
@@ -8,7 +8,7 @@ describe("selectable row activation", () => {
|
||||
expect(action).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("preserves contributed links and other interactive elements", () => {
|
||||
it("preserves contributed links inside rows", () => {
|
||||
const action = vi.fn();
|
||||
activateSelectableRow(eventWithPath(matchTarget((selector: string) => selector.includes("a[href]"))), action);
|
||||
expect(action).not.toHaveBeenCalled();
|
||||
@@ -57,6 +57,8 @@ describe("selectable row activation", () => {
|
||||
expect(handleSelectableRowKeyboard(event, { activate: vi.fn(), cancel })).toBe(true);
|
||||
|
||||
expect(cancel).toHaveBeenCalledOnce();
|
||||
expect(event.preventDefault).toHaveBeenCalledOnce();
|
||||
expect(event.stopPropagation).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -123,25 +123,6 @@ describe("settings-general-panel save payloads", () => {
|
||||
expect(getPanelProperty(panel, "machineLocalError")).toBe("");
|
||||
});
|
||||
|
||||
it("clears upload defaults with a selected-machine-safe patch", async () => {
|
||||
const panel = new SettingsGeneralPanel();
|
||||
const onSaveMachineConfig = vi.fn();
|
||||
panel.onSaveMachineConfig = onSaveMachineConfig;
|
||||
setPanelProperty(panel, "machineDraft", {
|
||||
allowedPathsText: "",
|
||||
uploadDefaultFolder: "",
|
||||
} satisfies MachineAccessConfigDraft);
|
||||
|
||||
await callPanelPromise(panel, "saveMachineAccessConfig", new Event("submit", { cancelable: true }));
|
||||
|
||||
expect(onSaveMachineConfig.mock.calls).toEqual([[
|
||||
{
|
||||
pathAccess: { allowedPaths: [] },
|
||||
uploads: {},
|
||||
},
|
||||
]]);
|
||||
});
|
||||
|
||||
it("keeps invalid upload folders local and does not save selected-machine config", async () => {
|
||||
const panel = new SettingsGeneralPanel();
|
||||
const onSaveMachineConfig = vi.fn();
|
||||
|
||||
@@ -59,7 +59,7 @@ describe("settings-packages-panel layout", () => {
|
||||
expect(rendered).not.toContain("Pi package list unavailable");
|
||||
});
|
||||
|
||||
it("orders package load errors before the trusted-code warning while preserving loaded data", () => {
|
||||
it("orders package errors before the trusted-code warning while preserving loaded data", () => {
|
||||
const panel = new SettingsPackagesPanel();
|
||||
panel.targetMachine = remoteTarget;
|
||||
panel.packagesResponse = { packages: [packageInfo("npm:@acme/tools")] };
|
||||
|
||||
@@ -28,7 +28,7 @@ describe("settings-panel-frame", () => {
|
||||
expect(rendered.indexOf('class="notice-stack"')).toBeLessThan(rendered.indexOf('class="content"'));
|
||||
});
|
||||
|
||||
it("maps notice types to consistent default tones and roles", () => {
|
||||
it("maps representative notice types to consistent default tones and roles", () => {
|
||||
const notices: readonly SettingsNotice[] = [
|
||||
{ type: "availability", content: "Configuration unavailable." },
|
||||
{ type: "success", content: "Saved." },
|
||||
@@ -41,7 +41,12 @@ describe("settings-panel-frame", () => {
|
||||
const values = collectTemplateValues(frame.render());
|
||||
|
||||
expect(notices.map(settingsNoticeTone)).toEqual(["error", "success", "warning", "info"]);
|
||||
expect(values).toEqual(expect.arrayContaining(["notice error", "alert", "notice success", "status", "notice warning", "note", "notice info"]));
|
||||
expect(values.filter(isNoticeClassOrRole)).toEqual([
|
||||
"notice error", "alert",
|
||||
"notice success", "status",
|
||||
"notice warning", "note",
|
||||
"notice info", "note",
|
||||
]);
|
||||
});
|
||||
|
||||
it("wires the default header action through the frame", () => {
|
||||
@@ -145,3 +150,8 @@ function isStringArray(value: unknown): value is string[] {
|
||||
function isActionHandler(value: unknown): value is () => void {
|
||||
return typeof value === "function";
|
||||
}
|
||||
|
||||
function isNoticeClassOrRole(value: unknown): value is string {
|
||||
return typeof value === "string"
|
||||
&& (value.startsWith("notice ") || value === "alert" || value === "status" || value === "note");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
configFromDraft,
|
||||
draftFromConfig,
|
||||
gatewayServerConfigFromDraft,
|
||||
gatewayServerDraftFromConfig,
|
||||
machineAccessConfigPatchFromDraft,
|
||||
@@ -28,28 +26,42 @@ describe("settings config drafts", () => {
|
||||
allowedPathsText: "/tmp\n~/SDKs",
|
||||
uploadDefaultFolder: "manual/uploads",
|
||||
});
|
||||
expect(gatewayServerDraftFromConfig({ allowedHosts: true }).allowedHostsMode).toBe("all");
|
||||
});
|
||||
|
||||
it("builds gateway server saves without changing selected-machine-safe config values", () => {
|
||||
it("builds gateway server saves without dropping preserved config values", () => {
|
||||
expect(gatewayServerConfigFromDraft({
|
||||
host: " gateway.local ",
|
||||
port: "9000",
|
||||
allowedHostsMode: "all",
|
||||
allowedHostsText: "ignored.local",
|
||||
}, {
|
||||
shortcuts: { "core:view.chat": "mod+1" },
|
||||
plugins: { info: { enabled: false } },
|
||||
pathAccess: { allowedPaths: ["/old"] },
|
||||
uploads: { defaultFolder: "old/uploads" },
|
||||
maxUploadBytes: 1234,
|
||||
spawnSessions: true,
|
||||
subsessions: false,
|
||||
})).toEqual({
|
||||
host: "gateway.local",
|
||||
port: 9000,
|
||||
allowedHosts: true,
|
||||
shortcuts: { "core:view.chat": "mod+1" },
|
||||
plugins: { info: { enabled: false } },
|
||||
pathAccess: { allowedPaths: ["/old"] },
|
||||
uploads: { defaultFolder: "old/uploads" },
|
||||
maxUploadBytes: 1234,
|
||||
spawnSessions: true,
|
||||
subsessions: false,
|
||||
});
|
||||
|
||||
expect(gatewayServerConfigFromDraft({
|
||||
host: "",
|
||||
port: "",
|
||||
allowedHostsMode: "list",
|
||||
allowedHostsText: "example.local, 192.168.1.20\n",
|
||||
})).toEqual({ allowedHosts: ["example.local", "192.168.1.20"] });
|
||||
});
|
||||
|
||||
it("builds selected-machine access/upload patches only from selected-machine-safe fields", () => {
|
||||
@@ -77,64 +89,10 @@ describe("settings config drafts", () => {
|
||||
expect(() => machineAccessConfigPatchFromDraft({ allowedPathsText: "", uploadDefaultFolder: "../secret" })).toThrow("Upload default folder must not contain path traversal.");
|
||||
});
|
||||
|
||||
it("converts PI WEB config values to editable general settings drafts", () => {
|
||||
expect(draftFromConfig({ host: "0.0.0.0", port: 8504, allowedHosts: ["example.local", "192.168.1.20"], pathAccess: { allowedPaths: ["/tmp", "~/SDKs"] } })).toEqual({
|
||||
host: "0.0.0.0",
|
||||
port: "8504",
|
||||
allowedHostsMode: "list",
|
||||
allowedHostsText: "example.local\n192.168.1.20",
|
||||
allowedPathsText: "/tmp\n~/SDKs",
|
||||
});
|
||||
expect(draftFromConfig({ allowedHosts: true }).allowedHostsMode).toBe("all");
|
||||
});
|
||||
|
||||
it("converts drafts back to config while preserving non-general preferences", () => {
|
||||
expect(configFromDraft({
|
||||
host: " 127.0.0.1 ",
|
||||
port: "9000",
|
||||
allowedHostsMode: "list",
|
||||
allowedHostsText: "example.local, 192.168.1.20\n",
|
||||
allowedPathsText: "/tmp\n~/SDKs\n",
|
||||
}, { shortcuts: { "core:view.chat": "mod+1", "core:session.stop": null }, plugins: { info: { enabled: false } }, pathAccess: { allowedPaths: ["/old"] }, uploads: { defaultFolder: "manual/uploads" }, maxUploadBytes: 1234 })).toEqual({
|
||||
host: "127.0.0.1",
|
||||
port: 9000,
|
||||
allowedHosts: ["example.local", "192.168.1.20"],
|
||||
shortcuts: { "core:view.chat": "mod+1", "core:session.stop": null },
|
||||
plugins: { info: { enabled: false } },
|
||||
pathAccess: { allowedPaths: ["/tmp", "~/SDKs"] },
|
||||
uploads: { defaultFolder: "manual/uploads" },
|
||||
maxUploadBytes: 1234,
|
||||
});
|
||||
});
|
||||
|
||||
it("removes global path access when the allowed paths field is cleared", () => {
|
||||
expect(configFromDraft({
|
||||
host: "",
|
||||
port: "",
|
||||
allowedHostsMode: "list",
|
||||
allowedHostsText: "",
|
||||
allowedPathsText: "",
|
||||
}, { pathAccess: { allowedPaths: ["/old"] } })).not.toHaveProperty("pathAccess");
|
||||
});
|
||||
|
||||
it("rejects relative external paths before saving", () => {
|
||||
expect(() => configFromDraft({
|
||||
host: "",
|
||||
port: "",
|
||||
allowedHostsMode: "list",
|
||||
allowedHostsText: "",
|
||||
it("rejects relative external paths before saving selected-machine access", () => {
|
||||
expect(() => machineAccessConfigPatchFromDraft({
|
||||
allowedPathsText: "relative/path",
|
||||
uploadDefaultFolder: "",
|
||||
})).toThrow("Allowed external paths must be absolute paths or start with ~");
|
||||
});
|
||||
|
||||
it("preserves the spawnSessions flag when saving general settings", () => {
|
||||
const result = configFromDraft({
|
||||
host: "",
|
||||
port: "",
|
||||
allowedHostsMode: "list",
|
||||
allowedHostsText: "",
|
||||
allowedPathsText: "",
|
||||
}, { spawnSessions: true });
|
||||
expect(result.spawnSessions).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,10 +12,6 @@ export interface MachineAccessConfigDraft {
|
||||
uploadDefaultFolder: string;
|
||||
}
|
||||
|
||||
export interface ConfigDraft extends GatewayServerConfigDraft {
|
||||
allowedPathsText: string;
|
||||
}
|
||||
|
||||
export function emptyGatewayServerConfigDraft(): GatewayServerConfigDraft {
|
||||
return { host: "", port: "", allowedHostsMode: "list", allowedHostsText: "" };
|
||||
}
|
||||
@@ -40,10 +36,6 @@ export function machineAccessDraftFromConfig(config: PiWebConfigValues): Machine
|
||||
};
|
||||
}
|
||||
|
||||
export function draftFromConfig(config: PiWebConfigValues): ConfigDraft {
|
||||
return { ...gatewayServerDraftFromConfig(config), allowedPathsText: machineAccessDraftFromConfig(config).allowedPathsText };
|
||||
}
|
||||
|
||||
export function gatewayServerConfigFromDraft(draft: GatewayServerConfigDraft, baseConfig: PiWebConfigValues = {}): PiWebConfigValues {
|
||||
const config = preservedGatewayConfigRemainder(baseConfig);
|
||||
const host = draft.host.trim();
|
||||
@@ -67,14 +59,6 @@ export function machineAccessConfigPatchFromDraft(draft: MachineAccessConfigDraf
|
||||
};
|
||||
}
|
||||
|
||||
export function configFromDraft(draft: ConfigDraft, baseConfig: PiWebConfigValues = {}): PiWebConfigValues {
|
||||
const config = gatewayServerConfigFromDraft(draft, baseConfig);
|
||||
const allowedPaths = parseAllowedPathsText(draft.allowedPathsText);
|
||||
if (allowedPaths.length > 0) config.pathAccess = { allowedPaths };
|
||||
else delete config.pathAccess;
|
||||
return config;
|
||||
}
|
||||
|
||||
function preservedGatewayConfigRemainder(baseConfig: PiWebConfigValues): PiWebConfigValues {
|
||||
return {
|
||||
...(baseConfig.shortcuts === undefined ? {} : { shortcuts: baseConfig.shortcuts }),
|
||||
|
||||
@@ -5,9 +5,7 @@ import { mergeSelectedMachineSessiondConfig, spawnSessionsConfigPatch, subsessio
|
||||
describe("session daemon settings config helpers", () => {
|
||||
it("builds daemon-only save patches for the sessiond toggles", () => {
|
||||
expect(spawnSessionsConfigPatch(false)).toEqual({ spawnSessions: false });
|
||||
expect(Object.keys(spawnSessionsConfigPatch(false))).toEqual(["spawnSessions"]);
|
||||
expect(subsessionsConfigPatch(true)).toEqual({ subsessions: true });
|
||||
expect(Object.keys(subsessionsConfigPatch(true))).toEqual(["subsessions"]);
|
||||
});
|
||||
|
||||
it("merges local selected-machine daemon config into gateway config without dropping gateway-only values", () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { api as defaultApi, type MessagePage, type PromptAttachment, type Sessio
|
||||
import type { SessionUiEvent } from "../sessionSocket";
|
||||
import { isCachedNewSessionInfo, loadCachedNewSessions, markCachedNewSessionInfo, rememberCachedNewSession } from "../cachedNewSessions";
|
||||
import { initialAppState, type AppState } from "../appState";
|
||||
import { ChatTranscriptStore } from "../chatTranscriptStore";
|
||||
import { machineSessionKey } from "../machineKeys";
|
||||
import { PI_WEB_CAPABILITIES } from "../../../shared/capabilities";
|
||||
import { loadDraft, saveDraft } from "../promptDraftStorage";
|
||||
@@ -1414,8 +1415,10 @@ describe("SessionController", () => {
|
||||
});
|
||||
|
||||
it("reloads the selected session from disk, discards the cached transcript, and re-fetches history", async () => {
|
||||
Object.defineProperty(globalThis, "localStorage", { value: new MemoryStorage(), configurable: true });
|
||||
const persistedSession = { ...oldSession, persisted: true };
|
||||
const cacheKey = sessionKey(oldSession.id);
|
||||
const freshPage: MessagePage = { messages: [{ role: "assistant", content: "fresh from disk" }], start: 1, total: 2 };
|
||||
const cachedPages = new Map<string, MessagePage>([[cacheKey, { messages: [{ role: "user", content: "stale cached transcript" }], start: 0, total: 2 }]]);
|
||||
const reloadCalls: string[] = [];
|
||||
const messageCalls: string[] = [];
|
||||
let state: AppState = {
|
||||
@@ -1433,7 +1436,7 @@ describe("SessionController", () => {
|
||||
},
|
||||
messages: (session) => {
|
||||
messageCalls.push(sessionLookupId(session));
|
||||
return Promise.resolve(emptyPage);
|
||||
return Promise.resolve(freshPage);
|
||||
},
|
||||
status: (session) => Promise.resolve(status(sessionLookupId(session))),
|
||||
};
|
||||
@@ -1442,13 +1445,24 @@ describe("SessionController", () => {
|
||||
(patch) => { state = { ...state, ...patch }; },
|
||||
() => undefined,
|
||||
new InMemorySessionSelectionMemory(),
|
||||
{ api, socket: new FakeSocket() },
|
||||
{
|
||||
api,
|
||||
socket: new FakeSocket(),
|
||||
transcripts: new ChatTranscriptStore({
|
||||
read: (sessionId) => cachedPages.get(sessionId),
|
||||
write: (sessionId, page) => { cachedPages.set(sessionId, page); },
|
||||
remove: (sessionId) => { cachedPages.delete(sessionId); },
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
await controller.reloadSession(persistedSession);
|
||||
|
||||
expect(reloadCalls).toEqual([oldSession.id]);
|
||||
expect(messageCalls).toContain(oldSession.id);
|
||||
expect(messageCalls).toEqual([oldSession.id]);
|
||||
expect(cachedPages.get(cacheKey)).toEqual(freshPage);
|
||||
expect(state.messages).toEqual([{ role: "assistant", parts: [{ type: "text", text: "fresh from disk" }] }]);
|
||||
expect(state.messagePageStart).toBe(1);
|
||||
expect(state.error).toBe("");
|
||||
});
|
||||
|
||||
|
||||
@@ -28,18 +28,12 @@ describe("selectPreferredSession", () => {
|
||||
expect(selectPreferredSession(sessions)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("can remember an archived selected session", () => {
|
||||
it("returns a remembered archived session before falling back to active sessions", () => {
|
||||
const sessions = [{ ...testSession("s1"), archived: true }, testSession("s2")];
|
||||
|
||||
expect(selectPreferredSession(sessions, { latestSessionId: "s1" })?.id).toBe("s1");
|
||||
});
|
||||
|
||||
it("can remember an archived selected session when only archived sessions remain", () => {
|
||||
const sessions = [{ ...testSession("s1"), archived: true }];
|
||||
|
||||
expect(selectPreferredSession(sessions, { latestSessionId: "s1" })?.id).toBe("s1");
|
||||
});
|
||||
|
||||
it("falls back to the first active session when the remembered session no longer exists", () => {
|
||||
const sessions = [{ ...testSession("s1"), archived: true }, testSession("s2")];
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ describe("selectPreferredWorkspace", () => {
|
||||
expect(selectPreferredWorkspace(workspaces, { latestWorkspaceId: "old" })?.id).toBe("main");
|
||||
});
|
||||
|
||||
it("preserves explicit invalid target behavior", () => {
|
||||
it("does not fall back to remembered workspace when the explicit target is invalid", () => {
|
||||
const workspaces = [testWorkspace("main"), testWorkspace("feature")];
|
||||
|
||||
expect(selectPreferredWorkspace(workspaces, { targetWorkspaceId: "old", latestWorkspaceId: "feature" })).toBeUndefined();
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { inputModeForDraft, inputModesEqual, isShellInput } from "./inputModes";
|
||||
|
||||
describe("inputModeForDraft", () => {
|
||||
it("detects shell input and context-excluded shell input after leading whitespace", () => {
|
||||
describe("input mode helpers", () => {
|
||||
it("detects shell mode and context-excluded shell mode after leading whitespace", () => {
|
||||
expect(inputModeForDraft(" ! npm test")).toEqual({ kind: "shell", excludeFromContext: false });
|
||||
expect(inputModeForDraft("\n!! secret command")).toEqual({ kind: "shell", excludeFromContext: true });
|
||||
expect(isShellInput(" ! pwd")).toBe(true);
|
||||
});
|
||||
|
||||
it("detects slash commands only for the current token", () => {
|
||||
it("detects slash-command mode from the current token", () => {
|
||||
expect(inputModeForDraft("/compact")).toEqual({ kind: "command" });
|
||||
expect(inputModeForDraft("please /compact")).toEqual({ kind: "command" });
|
||||
expect(inputModeForDraft("please mention/path")).toEqual({ kind: "normal" });
|
||||
});
|
||||
|
||||
it("treats modes as equal only when kind and shell context-exclusion match", () => {
|
||||
it("compares modes by kind and shell context-exclusion", () => {
|
||||
expect(inputModesEqual({ kind: "normal" }, { kind: "normal" })).toBe(true);
|
||||
expect(inputModesEqual({ kind: "normal" }, { kind: "command" })).toBe(false);
|
||||
expect(inputModesEqual({ kind: "shell", excludeFromContext: false }, { kind: "shell", excludeFromContext: false })).toBe(true);
|
||||
expect(inputModesEqual({ kind: "shell", excludeFromContext: false }, { kind: "shell", excludeFromContext: true })).toBe(false);
|
||||
});
|
||||
|
||||
it("detects file completion contexts", () => {
|
||||
it("collapses file completion triggers to file mode", () => {
|
||||
expect(inputModeForDraft("open @src/main.ts")).toEqual({ kind: "file" });
|
||||
expect(inputModeForDraft("open @ ")).toEqual({ kind: "file" });
|
||||
expect(inputModeForDraft("open @ A FILE")).toEqual({ kind: "file" });
|
||||
|
||||
@@ -64,7 +64,7 @@ describe("PluginRegistry", () => {
|
||||
expect(registry.getWorkspacePanels().map((panel) => panel.id)).toEqual(["core:workspace.files", "core:workspace.git", "core:workspace.terminal"]);
|
||||
});
|
||||
|
||||
it("provides html and svg helpers to plugin activation", () => {
|
||||
it("provides html and svg helpers to plugin activation and callbacks", () => {
|
||||
const registry = new PluginRegistry();
|
||||
registry.register({
|
||||
id: "example",
|
||||
@@ -86,7 +86,10 @@ describe("PluginRegistry", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(registry.getWorkspacePanels()[0]?.icon).toBeDefined();
|
||||
const panel = registry.getWorkspacePanels()[0];
|
||||
|
||||
expect(panel?.icon).toBeDefined();
|
||||
expect(panel?.render(createWorkspacePanelContext("local"))).toBeDefined();
|
||||
});
|
||||
|
||||
it("exposes the prompt helper to workspace panel callbacks", () => {
|
||||
|
||||
@@ -8,9 +8,10 @@ afterEach(() => {
|
||||
Object.defineProperty(globalThis, "window", { value: originalWindow, configurable: true });
|
||||
});
|
||||
|
||||
function installWindow(href: string): { pushed: string[] } {
|
||||
function installWindow(href: string): { pushed: string[]; replaced: string[] } {
|
||||
const url = new URL(href);
|
||||
const pushed: string[] = [];
|
||||
const replaced: string[] = [];
|
||||
const fakeWindow = {
|
||||
location: {
|
||||
href: url.href,
|
||||
@@ -23,12 +24,12 @@ function installWindow(href: string): { pushed: string[] } {
|
||||
pushed.push(String(next));
|
||||
}),
|
||||
replaceState: vi.fn((_state: object, _title: string, next: URL | string) => {
|
||||
pushed.push(String(next));
|
||||
replaced.push(String(next));
|
||||
}),
|
||||
},
|
||||
};
|
||||
Object.defineProperty(globalThis, "window", { value: fakeWindow, configurable: true });
|
||||
return { pushed };
|
||||
return { pushed, replaced };
|
||||
}
|
||||
|
||||
describe("route helpers", () => {
|
||||
@@ -51,8 +52,8 @@ describe("route helpers", () => {
|
||||
expect(readRoute()).toMatchObject({ tool: undefined, view: undefined });
|
||||
});
|
||||
|
||||
it("writes compact URLs and preserves path/hash", () => {
|
||||
const { pushed } = installWindow("http://localhost/app?old=1#section");
|
||||
it("writes compact URLs with push history and preserves path/hash", () => {
|
||||
const { pushed, replaced } = installWindow("http://localhost/app?old=1#section");
|
||||
const route: AppRoute = {
|
||||
machineId: "remote",
|
||||
projectId: "project/id",
|
||||
@@ -65,13 +66,15 @@ describe("route helpers", () => {
|
||||
writeRoute(route);
|
||||
|
||||
expect(pushed).toEqual(["http://localhost/app?old=1&machine=remote&project=project%2Fid&workspace=workspace+id&tool=core%3Aworkspace.files&view=chat#section"]);
|
||||
expect(replaced).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not push history when the route is unchanged", () => {
|
||||
const { pushed } = installWindow("http://localhost/app?project=p1&tool=core%3Aworkspace.git");
|
||||
it("does not write history when the route is unchanged", () => {
|
||||
const { pushed, replaced } = installWindow("http://localhost/app?project=p1&tool=core%3Aworkspace.git");
|
||||
|
||||
writeRoute({ machineId: undefined, projectId: "p1", workspaceId: undefined, sessionId: undefined, tool: "core:workspace.git", view: undefined });
|
||||
|
||||
expect(pushed).toEqual([]);
|
||||
expect(replaced).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -93,17 +93,21 @@ describe("resolveThemePreference", () => {
|
||||
expect(resolution.activeTheme?.id).toBe("themes:classic");
|
||||
});
|
||||
|
||||
it("does not overwrite a missing selected theme preference in the resolution result", () => {
|
||||
const missingThemeId: QualifiedContributionId = "plugin:missing";
|
||||
it("falls back to Classic without mutating a missing selected theme preference", () => {
|
||||
const preference = {
|
||||
themeId: "plugin:missing",
|
||||
auto: true,
|
||||
} satisfies { themeId: QualifiedContributionId; auto: boolean };
|
||||
const resolution = resolveThemePreference({
|
||||
themes,
|
||||
themePairs,
|
||||
preference: { themeId: missingThemeId, auto: true },
|
||||
preference,
|
||||
prefersLight: false,
|
||||
});
|
||||
|
||||
expect(resolution.selectedTheme?.id).toBe("themes:classic");
|
||||
expect(missingThemeId).toBe("plugin:missing");
|
||||
expect(resolution.activeTheme?.id).toBe("themes:classic");
|
||||
expect(preference).toEqual({ themeId: "plugin:missing", auto: true });
|
||||
});
|
||||
|
||||
it("can look up a pair from either member theme", () => {
|
||||
|
||||
@@ -15,9 +15,11 @@ function activity(cwd: string, patch: Partial<WorkspaceActivity> = {}): Workspac
|
||||
}
|
||||
|
||||
describe("workspace activity aggregation", () => {
|
||||
it("matches activity to workspace paths", () => {
|
||||
const ws = workspace("p1", "/repo");
|
||||
expect(workspaceActivityFor(ws, { "/repo": activity("/repo") })?.hasSessionActivity).toBe(true);
|
||||
it("matches activity to workspace paths rather than ids", () => {
|
||||
const ws = { ...workspace("p1", "/repo"), id: "workspace-1" };
|
||||
const matched = activity("/repo");
|
||||
|
||||
expect(workspaceActivityFor(ws, { "/repo": matched, "workspace-1": activity("workspace-1") })).toEqual(matched);
|
||||
});
|
||||
|
||||
it("uses a terminal indicator only when there is no session activity", () => {
|
||||
|
||||
@@ -47,10 +47,18 @@ describe("workspace deletion state", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("reports pending workspace deletions for disabling repeated actions", () => {
|
||||
const state = { workspaceDeletionRuns: { w1: run("new", "w1", "2026-05-25T00:00:01.000Z", "running") } };
|
||||
it("reports only queued or running workspace deletions as pending", () => {
|
||||
const state = {
|
||||
workspaceDeletionRuns: {
|
||||
w1: run("running", "w1", "2026-05-25T00:00:01.000Z", "running"),
|
||||
w2: run("queued", "w2", "2026-05-25T00:00:02.000Z", "queued"),
|
||||
w3: run("succeeded", "w3", "2026-05-25T00:00:03.000Z", "succeeded"),
|
||||
w4: run("failed", "w4", "2026-05-25T00:00:04.000Z", "failed"),
|
||||
},
|
||||
};
|
||||
|
||||
expect(isWorkspaceDeletionPending(state, workspace)).toBe(true);
|
||||
expect(pendingWorkspaceDeletionIds(state.workspaceDeletionRuns)).toEqual(["w1"]);
|
||||
expect(isWorkspaceDeletionPending(state, { ...workspace, id: "w3" })).toBe(false);
|
||||
expect(pendingWorkspaceDeletionIds(state.workspaceDeletionRuns)).toEqual(["w1", "w2"]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user