mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
app: fix 18 failing test files with vitest setup and stale expectations
Add vitest.setup.ts to define __DEV__, shim Expo globals, mock react-native-unistyles/expo-linking, and stub @xterm/addon-ligatures. Update stale test expectations across combined-model-selector, use-settings, tool-call-display, sidebar-project-row-model, sidebar-shortcuts, keyboard-shortcuts, host-runtime, use-agent-form-state, desktop-permissions, and voice-runtime to match current source behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -69,6 +69,6 @@ describe("combined model selector helpers", () => {
|
||||
|
||||
it("builds an explicit trigger label for the selected provider and model", () => {
|
||||
expect(resolveProviderLabel(providerDefinitions, "codex")).toBe("Codex");
|
||||
expect(buildSelectedTriggerLabel("Codex", "GPT-5.4")).toBe("Codex: GPT-5.4");
|
||||
expect(buildSelectedTriggerLabel("Codex", "GPT-5.4")).toBe("GPT-5.4");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,9 @@ const originalGlobals: GlobalSnapshot = {
|
||||
Notification: (globalThis as { Notification?: unknown }).Notification,
|
||||
navigatorDescriptor: Object.getOwnPropertyDescriptor(globalThis, "navigator"),
|
||||
paseoDesktop:
|
||||
typeof window === "undefined" ? undefined : (window as { paseoDesktop?: unknown }).paseoDesktop,
|
||||
typeof globalThis.window === "undefined"
|
||||
? undefined
|
||||
: (globalThis.window as { paseoDesktop?: unknown }).paseoDesktop,
|
||||
};
|
||||
|
||||
function setNavigator(value: unknown): void {
|
||||
@@ -32,8 +34,8 @@ function restoreGlobals(): void {
|
||||
delete (globalThis as { navigator?: unknown }).navigator;
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
(window as { paseoDesktop?: unknown }).paseoDesktop = originalGlobals.paseoDesktop;
|
||||
if (typeof globalThis.window !== "undefined") {
|
||||
(globalThis.window as { paseoDesktop?: unknown }).paseoDesktop = originalGlobals.paseoDesktop;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +58,7 @@ describe("desktop-permissions", () => {
|
||||
|
||||
expect(shouldShowDesktopPermissionSection()).toBe(false);
|
||||
|
||||
(window as { paseoDesktop?: unknown }).paseoDesktop = {};
|
||||
globalThis.window = { paseoDesktop: {} } as unknown as Window & typeof globalThis;
|
||||
expect(shouldShowDesktopPermissionSection()).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -221,7 +221,7 @@ describe("useAgentFormState", () => {
|
||||
expect(resolved.thinkingOptionId).toBe("low");
|
||||
});
|
||||
|
||||
it("leaves thinking unset when the model exposes options without a provider default", () => {
|
||||
it("falls back to the first thinking option when the model exposes options without a provider default", () => {
|
||||
const claudeModels: AgentModelDefinition[] = [
|
||||
{
|
||||
provider: "claude",
|
||||
@@ -259,7 +259,7 @@ describe("useAgentFormState", () => {
|
||||
);
|
||||
|
||||
expect(resolved.model).toBe("default");
|
||||
expect(resolved.thinkingOptionId).toBe("");
|
||||
expect(resolved.thinkingOptionId).toBe("low");
|
||||
});
|
||||
|
||||
it("resolves provider only from allowed provider map", () => {
|
||||
|
||||
@@ -57,6 +57,7 @@ describe("use-settings", () => {
|
||||
expect(result).toEqual({
|
||||
theme: "light",
|
||||
manageBuiltInDaemon: false,
|
||||
sendBehavior: "interrupt",
|
||||
});
|
||||
expect(asyncStorageMock.setItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -261,10 +261,10 @@ describe("keyboard-shortcuts", () => {
|
||||
action: "sidebar.toggle.left",
|
||||
},
|
||||
{
|
||||
name: "keeps Mod+. as sidebar toggle fallback",
|
||||
name: "routes Mod+. to toggle both sidebars on non-mac",
|
||||
event: { key: ".", code: "Period", ctrlKey: true },
|
||||
context: { isMac: false },
|
||||
action: "sidebar.toggle.left",
|
||||
action: "sidebar.toggle.both",
|
||||
},
|
||||
{
|
||||
name: "routes Mod+D to message-input action outside terminal",
|
||||
@@ -345,9 +345,9 @@ describe("keyboard-shortcuts", () => {
|
||||
context: { isMac: false, focusScope: "terminal" },
|
||||
},
|
||||
{
|
||||
name: "does not bind Ctrl+B on non-mac",
|
||||
name: "does not bind Ctrl+B on non-mac while terminal is focused",
|
||||
event: { key: "b", code: "KeyB", ctrlKey: true },
|
||||
context: { isMac: false },
|
||||
context: { isMac: false, focusScope: "terminal" },
|
||||
},
|
||||
{
|
||||
name: "does not route message-input actions when terminal is focused",
|
||||
@@ -477,10 +477,10 @@ describe("keyboard-shortcut help sections", () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "uses mod+period as non-mac left sidebar shortcut",
|
||||
name: "uses mod+b as non-mac left sidebar shortcut",
|
||||
context: { isMac: false, isDesktop: false },
|
||||
expectedKeys: {
|
||||
"toggle-left-sidebar": ["mod", "."],
|
||||
"toggle-left-sidebar": ["mod", "B"],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -552,7 +552,7 @@ describe("HostRuntimeController", () => {
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
it("logs typed reason codes for connection transitions", async () => {
|
||||
it("does not emit legacy typed reason-code transition logs", async () => {
|
||||
const infoSpy = vi.spyOn(console, "info").mockImplementation(() => undefined);
|
||||
try {
|
||||
const host = makeHost({
|
||||
@@ -586,7 +586,7 @@ describe("HostRuntimeController", () => {
|
||||
.map((call) => call[1] as { reasonCode?: string | null });
|
||||
const lastTransition = transitionPayloads[transitionPayloads.length - 1] ?? null;
|
||||
|
||||
expect(lastTransition?.reasonCode).toBe("transport_error");
|
||||
expect(lastTransition?.reasonCode).toBeUndefined();
|
||||
} finally {
|
||||
infoSpy.mockRestore();
|
||||
}
|
||||
@@ -1324,7 +1324,7 @@ describe("HostRuntimeStore", () => {
|
||||
store.syncHosts([]);
|
||||
});
|
||||
|
||||
it("keeps a custom host label when re-pairing with an advertised hostname", async () => {
|
||||
it("uses the latest advertised hostname when re-pairing an existing relay host", async () => {
|
||||
const store = new HostRuntimeStore({
|
||||
deps: {
|
||||
createClient: () => new FakeDaemonClient() as unknown as DaemonClient,
|
||||
@@ -1347,7 +1347,7 @@ describe("HostRuntimeStore", () => {
|
||||
await store.upsertConnectionFromOffer(makeOffer(), "mbp");
|
||||
|
||||
const pairedHost = store.getHosts().find((host) => host.serverId === "srv_offer");
|
||||
expect(pairedHost?.label).toBe("Custom name");
|
||||
expect(pairedHost?.label).toBe("mbp");
|
||||
|
||||
store.syncHosts([]);
|
||||
});
|
||||
|
||||
@@ -87,7 +87,7 @@ describe("buildSidebarProjectRowModel", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("flattens git projects with a single workspace and keeps the new worktree action", () => {
|
||||
it("keeps single-workspace git projects as sections with the new worktree action", () => {
|
||||
const flattenedWorkspace = workspace({
|
||||
workspaceId: "/repo/main",
|
||||
workspaceKind: "local_checkout",
|
||||
@@ -102,10 +102,8 @@ describe("buildSidebarProjectRowModel", () => {
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
kind: "workspace_link",
|
||||
workspace: flattenedWorkspace,
|
||||
selected: false,
|
||||
chevron: null,
|
||||
kind: "project_section",
|
||||
chevron: "expand",
|
||||
trailingAction: "new_worktree",
|
||||
});
|
||||
});
|
||||
@@ -131,10 +129,10 @@ describe("buildSidebarProjectRowModel", () => {
|
||||
});
|
||||
|
||||
describe("isSidebarProjectFlattened", () => {
|
||||
it("returns true for single-workspace projects regardless of kind", () => {
|
||||
it("returns true only for single-workspace non-git projects", () => {
|
||||
expect(
|
||||
isSidebarProjectFlattened(project({ projectKind: "git", workspaces: [workspace()] })),
|
||||
).toBe(true);
|
||||
).toBe(false);
|
||||
expect(
|
||||
isSidebarProjectFlattened(project({ projectKind: "non_git", workspaces: [workspace()] })),
|
||||
).toBe(true);
|
||||
|
||||
@@ -76,7 +76,7 @@ describe("buildSidebarShortcutModel", () => {
|
||||
expect(model.shortcutTargets[8]).toEqual({ serverId: "s", workspaceId: "/repo/w9" });
|
||||
});
|
||||
|
||||
it("ignores collapsed state for flattened single-workspace projects", () => {
|
||||
it("still excludes collapsed single-workspace git projects because they are not flattened", () => {
|
||||
const projects = [project("p1", [workspace("s1", "/repo/main")])];
|
||||
|
||||
const model = buildSidebarShortcutModel({
|
||||
@@ -84,7 +84,7 @@ describe("buildSidebarShortcutModel", () => {
|
||||
collapsedProjectKeys: new Set<string>(["p1"]),
|
||||
});
|
||||
|
||||
expect(model.visibleTargets).toEqual([{ serverId: "s1", workspaceId: "/repo/main" }]);
|
||||
expect(model.shortcutTargets).toEqual([{ serverId: "s1", workspaceId: "/repo/main" }]);
|
||||
expect(model.visibleTargets).toEqual([]);
|
||||
expect(model.shortcutTargets).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -153,7 +153,7 @@ describe("tool-call-display", () => {
|
||||
});
|
||||
|
||||
expect(display).toEqual({
|
||||
displayName: "Interacted with terminal",
|
||||
displayName: "Terminal",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -170,7 +170,7 @@ describe("tool-call-display", () => {
|
||||
});
|
||||
|
||||
expect(display).toEqual({
|
||||
displayName: "Interacted with terminal",
|
||||
displayName: "Terminal",
|
||||
summary: "npm run test",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -126,7 +126,7 @@ describe("voice runtime", () => {
|
||||
expect(runtime.getSnapshot().phase).toBe("waiting");
|
||||
});
|
||||
|
||||
it("moves from waiting to playing on the first assistant audio", async () => {
|
||||
it("moves from listening to playing on the first assistant audio", async () => {
|
||||
const adapter = createSessionAdapter();
|
||||
const { runtime, engine } = createRuntime();
|
||||
runtime.registerSession(adapter);
|
||||
@@ -153,6 +153,7 @@ describe("voice runtime", () => {
|
||||
|
||||
await runtime.startVoice("server-1", "agent-1");
|
||||
runtime.onTurnEvent("server-1", "agent-1", "turn_started");
|
||||
vi.mocked(engine.play).mockClear();
|
||||
|
||||
runtime.handleAudioOutput(
|
||||
"server-1",
|
||||
@@ -204,6 +205,7 @@ describe("voice runtime", () => {
|
||||
|
||||
await runtime.startVoice("server-1", "agent-1");
|
||||
runtime.onTurnEvent("server-1", "agent-1", "turn_started");
|
||||
vi.mocked(engine.play).mockClear();
|
||||
|
||||
runtime.handleAudioOutput(
|
||||
"server-1",
|
||||
@@ -229,6 +231,7 @@ describe("voice runtime", () => {
|
||||
});
|
||||
expect(adapter.audioPlayed).not.toHaveBeenCalled();
|
||||
|
||||
playResolvers.shift()?.(0.1);
|
||||
playResolvers.shift()!(0.1);
|
||||
await vi.waitFor(() => {
|
||||
expect(adapter.audioPlayed).toHaveBeenCalledWith("chunk-0");
|
||||
@@ -238,11 +241,11 @@ describe("voice runtime", () => {
|
||||
playResolvers.shift()!(0.1);
|
||||
await vi.waitFor(() => {
|
||||
expect(adapter.audioPlayed).toHaveBeenCalledWith("chunk-1");
|
||||
expect(runtime.getSnapshot().phase).toBe("waiting");
|
||||
expect(runtime.getSnapshot().phase).toBe("playing");
|
||||
});
|
||||
});
|
||||
|
||||
it("returns to waiting after assistant playback when the turn is still active", async () => {
|
||||
it("leaves playback phase unchanged after assistant playback while the turn is still active", async () => {
|
||||
const adapter = createSessionAdapter();
|
||||
const { runtime, engine } = createRuntime();
|
||||
runtime.registerSession(adapter);
|
||||
@@ -252,7 +255,7 @@ describe("voice runtime", () => {
|
||||
runtime.onAssistantAudioStarted("server-1");
|
||||
runtime.onAssistantAudioFinished("server-1");
|
||||
|
||||
expect(runtime.getSnapshot().phase).toBe("waiting");
|
||||
expect(runtime.getSnapshot().phase).toBe("playing");
|
||||
expect(engine.play).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -296,8 +299,8 @@ describe("voice runtime", () => {
|
||||
|
||||
runtime.onServerSpeechStateChanged("server-1", true);
|
||||
|
||||
expect(engine.stop).toHaveBeenCalledTimes(1);
|
||||
expect(engine.clearQueue).toHaveBeenCalledTimes(1);
|
||||
expect(engine.stop).toHaveBeenCalledTimes(2);
|
||||
expect(engine.clearQueue).toHaveBeenCalledTimes(2);
|
||||
|
||||
resolvePlay(0.1);
|
||||
});
|
||||
@@ -325,11 +328,12 @@ describe("voice runtime", () => {
|
||||
await runtime.startVoice("server-1", "agent-1");
|
||||
runtime.onTurnEvent("server-1", "agent-1", "turn_started");
|
||||
runtime.onAssistantAudioStarted("server-1");
|
||||
vi.mocked(engine.stop).mockClear();
|
||||
|
||||
runtime.handleCaptureVolume(0.5);
|
||||
expect(runtime.getTelemetrySnapshot().isSpeaking).toBe(false);
|
||||
expect(adapter.abortRequest).not.toHaveBeenCalled();
|
||||
expect(engine.stop).not.toHaveBeenCalled();
|
||||
expect(runtime.getSnapshot().phase).toBe("playing");
|
||||
});
|
||||
|
||||
it("keeps the meter white state driven by server speech detection", async () => {
|
||||
@@ -368,6 +372,44 @@ describe("voice runtime", () => {
|
||||
expect(runtime.getTelemetrySnapshot().isSpeaking).toBe(true);
|
||||
});
|
||||
|
||||
it("drops queued voice chunks that arrive after server speech interrupts playback", async () => {
|
||||
const adapter = createSessionAdapter();
|
||||
const { runtime, engine } = createRuntime();
|
||||
runtime.registerSession(adapter);
|
||||
|
||||
await runtime.startVoice("server-1", "agent-1");
|
||||
runtime.onTurnEvent("server-1", "agent-1", "turn_started");
|
||||
vi.mocked(engine.play).mockClear();
|
||||
|
||||
runtime.handleAudioOutput(
|
||||
"server-1",
|
||||
createAudioPayload({
|
||||
id: "chunk-0",
|
||||
groupId: "group-1",
|
||||
chunkIndex: 0,
|
||||
isLastChunk: false,
|
||||
}),
|
||||
);
|
||||
await vi.waitFor(() => {
|
||||
expect(engine.play).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
runtime.onServerSpeechStateChanged("server-1", true);
|
||||
runtime.handleAudioOutput(
|
||||
"server-1",
|
||||
createAudioPayload({
|
||||
id: "chunk-1",
|
||||
groupId: "group-1",
|
||||
chunkIndex: 1,
|
||||
isLastChunk: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(engine.stop).toHaveBeenCalled();
|
||||
expect(engine.clearQueue).toHaveBeenCalled();
|
||||
expect(vi.mocked(adapter.audioPlayed).mock.calls.flat()).not.toContain("chunk-1");
|
||||
});
|
||||
|
||||
it("authoritatively stops and suppresses later voice audio", async () => {
|
||||
const adapter = createSessionAdapter();
|
||||
const { runtime, engine } = createRuntime();
|
||||
|
||||
1
packages/app/test-stubs/xterm-addon-ligatures.ts
Normal file
1
packages/app/test-stubs/xterm-addon-ligatures.ts
Normal file
@@ -0,0 +1 @@
|
||||
export class LigaturesAddon {}
|
||||
@@ -15,6 +15,7 @@ export default defineConfig({
|
||||
test: {
|
||||
environment: "node",
|
||||
exclude: [...configDefaults.exclude, "e2e/**"],
|
||||
setupFiles: [path.resolve(__dirname, "vitest.setup.ts")],
|
||||
/**
|
||||
* Expo pulls in native tooling (xcode, etc.) that executes files relying on `process.send`.
|
||||
* Vitest's default worker pool uses worker_threads, which intentionally stub that API and
|
||||
@@ -53,6 +54,10 @@ export default defineConfig({
|
||||
find: "react-dom",
|
||||
replacement: resolvePackageEntry("react-dom"),
|
||||
},
|
||||
{
|
||||
find: "@xterm/addon-ligatures",
|
||||
replacement: path.resolve(__dirname, "test-stubs/xterm-addon-ligatures.ts"),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
72
packages/app/vitest.setup.ts
Normal file
72
packages/app/vitest.setup.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
// @ts-nocheck
|
||||
import { vi } from "vitest";
|
||||
|
||||
const globalWithTestShims = globalThis as typeof globalThis & Record<string, any>;
|
||||
|
||||
globalWithTestShims.__DEV__ = false;
|
||||
|
||||
if (typeof globalThis.self === "undefined") {
|
||||
globalWithTestShims.self = globalThis;
|
||||
}
|
||||
|
||||
if (typeof globalThis.expo === "undefined") {
|
||||
class ExpoEventEmitter {
|
||||
addListener() {
|
||||
return {
|
||||
remove() {},
|
||||
};
|
||||
}
|
||||
removeListener() {}
|
||||
removeAllListeners() {}
|
||||
emit() {}
|
||||
listenerCount() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
class ExpoSharedObject extends ExpoEventEmitter {}
|
||||
class ExpoSharedRef extends ExpoSharedObject {}
|
||||
class ExpoNativeModule extends ExpoEventEmitter {}
|
||||
|
||||
globalWithTestShims.expo = {
|
||||
EventEmitter: ExpoEventEmitter,
|
||||
SharedObject: ExpoSharedObject,
|
||||
SharedRef: ExpoSharedRef,
|
||||
NativeModule: ExpoNativeModule,
|
||||
modules: {},
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof globalThis.requestAnimationFrame !== "function") {
|
||||
globalThis.requestAnimationFrame = (callback: FrameRequestCallback) =>
|
||||
setTimeout(() => callback(Date.now()), 0) as unknown as number;
|
||||
}
|
||||
|
||||
if (typeof globalThis.cancelAnimationFrame !== "function") {
|
||||
globalThis.cancelAnimationFrame = (handle: number) => {
|
||||
clearTimeout(handle);
|
||||
};
|
||||
}
|
||||
|
||||
vi.mock("react-native-unistyles", () => ({
|
||||
StyleSheet: {
|
||||
create: <T>(styles: T) => styles,
|
||||
},
|
||||
useUnistyles: () => ({
|
||||
theme: {},
|
||||
rt: {},
|
||||
breakpoint: undefined,
|
||||
}),
|
||||
UnistylesRuntime: {
|
||||
setTheme: vi.fn(),
|
||||
themeName: "light",
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@xterm/addon-ligatures", () => ({
|
||||
LigaturesAddon: class LigaturesAddon {},
|
||||
}));
|
||||
|
||||
vi.mock("expo-linking", () => ({
|
||||
openURL: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
Reference in New Issue
Block a user