import { describe, expect, it } from "vite-plus/test";import type { NavigationState } from "@react-navigation/native"; import type { RecentThreadShortcut } from "../../persistence/imperative";import { activeThreadRef, buildShortcutActions, MAX_RECENT_THREAD_SHORTCUTS, NEW_TASK_SHORTCUT_ID, shortcutHref, withRecentThreadShortcut,} from "./appShortcuts"; function navState(route: { name: string; params?: unknown }): NavigationState { return { index: 0, routes: [route] } as unknown as NavigationState;} function thread(suffix: string, title = `Thread ${suffix}`): RecentThreadShortcut { return { environmentId: `env-${suffix}`, threadId: `thread-${suffix}`, title };} describe("withRecentThreadShortcut", () => { it("prepends a newly opened thread", () => { const next = withRecentThreadShortcut([thread("a")], thread("b")); expect(next.map((entry) => entry.threadId)).toEqual(["thread-b", "thread-a"]); }); it("moves a reopened thread to the front without duplicating it", () => { const next = withRecentThreadShortcut([thread("a"), thread("b")], thread("b")); expect(next.map((entry) => entry.threadId)).toEqual(["thread-b", "thread-a"]); }); it("caps the list at the shortcut budget", () => { const current = [thread("a"), thread("b"), thread("c")]; const next = withRecentThreadShortcut(current, thread("d")); expect(next).toHaveLength(MAX_RECENT_THREAD_SHORTCUTS); expect(next[0]?.threadId).toBe("thread-d"); expect(next.map((entry) => entry.threadId)).not.toContain("thread-c"); }); it("returns the same array when the thread already leads with the same title", () => { const current = [thread("a"), thread("b")]; expect(withRecentThreadShortcut(current, thread("a"))).toBe(current); }); it("keeps the known title when a reopen records an empty one", () => { const current = [thread("a", "Fix the build")]; const next = withRecentThreadShortcut(current, thread("a", "")); expect(next).toBe(current); }); it("updates the title once the shell provides one", () => { const current = [thread("a", "")]; const next = withRecentThreadShortcut(current, thread("a", "Fix the build")); expect(next[0]?.title).toBe("Fix the build"); expect(next).toHaveLength(1); });}); describe("buildShortcutActions", () => { it("leads with the static new-task action", () => { const actions = buildShortcutActions([thread("a")]); expect(actions[0]?.id).toBe(NEW_TASK_SHORTCUT_ID); expect(actions[0]?.params?.href).toBe("/new"); expect(actions).toHaveLength(2); }); it("deep-links threads with encoded route params", () => { const actions = buildShortcutActions([ { environmentId: "env 1", threadId: "thread/2", title: "Spaced out" }, ]); expect(actions[1]?.params?.href).toBe("/threads/env%201/thread%2F2"); expect(actions[1]?.title).toBe("Spaced out"); }); it("falls back to a generic label for missing titles", () => { const actions = buildShortcutActions([thread("a", " ")]); expect(actions[1]?.title).toBe("Thread"); });}); describe("shortcutHref", () => { it("accepts exactly the destinations shortcuts can produce", () => { expect(shortcutHref({ id: "x", title: "x", params: { href: "/new" } })).toBe("/new"); expect(shortcutHref({ id: "x", title: "x", params: { href: "/threads/env-1/thread-2" } })).toBe( "/threads/env-1/thread-2", ); expect( shortcutHref({ id: "x", title: "x", params: { href: "/threads/env%201/thread%2F2" } }), ).toBe("/threads/env%201/thread%2F2"); }); it("rejects everything else", () => { for (const href of [ "https://evil.example", "//evil.example", "/settings", "/threads/only-one-segment", "/threads/a/b/c", "/threads//x", "/threads/a/b?x=1", "/threads/a/b#frag", "/new/extra", ]) { expect(shortcutHref({ id: "x", title: "x", params: { href } })).toBe(null); } expect(shortcutHref({ id: "x", title: "x", params: { href: 3 } })).toBe(null); expect(shortcutHref({ id: "x", title: "x" })).toBe(null); });}); describe("activeThreadRef", () => { it("resolves the active Thread route's params", () => { const ref = activeThreadRef( navState({ name: "Thread", params: { environmentId: "env-1", threadId: "thread-2" } }), ); expect(ref).toEqual({ environmentId: "env-1", threadId: "thread-2" }); }); it("takes the first value of array params", () => { const ref = activeThreadRef( navState({ name: "Thread", params: { environmentId: ["env-1"], threadId: ["thread-2"] } }), ); expect(ref).toEqual({ environmentId: "env-1", threadId: "thread-2" }); }); it("returns null for non-thread routes", () => { expect(activeThreadRef(navState({ name: "Home" }))).toBe(null); }); it("returns null instead of throwing for malformed params", () => { const malformed: unknown[] = [ undefined, {}, { environmentId: "env-1" }, { environmentId: "", threadId: "thread-2" }, { environmentId: " ", threadId: "thread-2" }, { environmentId: "env-1", threadId: " " }, { environmentId: 42, threadId: "thread-2" }, { environmentId: { nested: true }, threadId: "thread-2" }, { environmentId: [], threadId: [] }, ]; for (const params of malformed) { expect(activeThreadRef(navState({ name: "Thread", params }))).toBe(null); } });}); describe("launcher shortcut ids", () => { it("cannot collide across different env/thread pairs", () => { const a = buildShortcutActions([{ environmentId: "a-b", threadId: "c", title: "x" }]); const b = buildShortcutActions([{ environmentId: "a", threadId: "b-c", title: "x" }]); expect(a[1]?.id).not.toBe(b[1]?.id); });});
import { describe, expect, it } from "vite-plus/test";
import type { NavigationState } from "@react-navigation/native";
import type { RecentThreadShortcut } from "../../persistence/imperative";
import {
activeThreadRef,
buildShortcutActions,
MAX_RECENT_THREAD_SHORTCUTS,
NEW_TASK_SHORTCUT_ID,
shortcutHref,
withRecentThreadShortcut,
} from "./appShortcuts";
function navState(route: { name: string; params?: unknown }): NavigationState {
return { index: 0, routes: [route] } as unknown as NavigationState;
}
function thread(suffix: string, title = `Thread ${suffix}`): RecentThreadShortcut {
return { environmentId: `env-${suffix}`, threadId: `thread-${suffix}`, title };
}
describe("withRecentThreadShortcut", () => {
it("prepends a newly opened thread", () => {
const next = withRecentThreadShortcut([thread("a")], thread("b"));
expect(next.map((entry) => entry.threadId)).toEqual(["thread-b", "thread-a"]);
});
it("moves a reopened thread to the front without duplicating it", () => {
const next = withRecentThreadShortcut([thread("a"), thread("b")], thread("b"));
expect(next.map((entry) => entry.threadId)).toEqual(["thread-b", "thread-a"]);
});
it("caps the list at the shortcut budget", () => {
const current = [thread("a"), thread("b"), thread("c")];
const next = withRecentThreadShortcut(current, thread("d"));
expect(next).toHaveLength(MAX_RECENT_THREAD_SHORTCUTS);
expect(next[0]?.threadId).toBe("thread-d");
expect(next.map((entry) => entry.threadId)).not.toContain("thread-c");
});
it("returns the same array when the thread already leads with the same title", () => {
const current = [thread("a"), thread("b")];
expect(withRecentThreadShortcut(current, thread("a"))).toBe(current);
});
it("keeps the known title when a reopen records an empty one", () => {
const current = [thread("a", "Fix the build")];
const next = withRecentThreadShortcut(current, thread("a", ""));
expect(next).toBe(current);
});
it("updates the title once the shell provides one", () => {
const current = [thread("a", "")];
const next = withRecentThreadShortcut(current, thread("a", "Fix the build"));
expect(next[0]?.title).toBe("Fix the build");
expect(next).toHaveLength(1);
});
});
describe("buildShortcutActions", () => {
it("leads with the static new-task action", () => {
const actions = buildShortcutActions([thread("a")]);
expect(actions[0]?.id).toBe(NEW_TASK_SHORTCUT_ID);
expect(actions[0]?.params?.href).toBe("/new");
expect(actions).toHaveLength(2);
});
it("deep-links threads with encoded route params", () => {
const actions = buildShortcutActions([
{ environmentId: "env 1", threadId: "thread/2", title: "Spaced out" },
]);
expect(actions[1]?.params?.href).toBe("/threads/env%201/thread%2F2");
expect(actions[1]?.title).toBe("Spaced out");
});
it("falls back to a generic label for missing titles", () => {
const actions = buildShortcutActions([thread("a", " ")]);
expect(actions[1]?.title).toBe("Thread");
});
});
describe("shortcutHref", () => {
it("accepts exactly the destinations shortcuts can produce", () => {
expect(shortcutHref({ id: "x", title: "x", params: { href: "/new" } })).toBe("/new");
expect(shortcutHref({ id: "x", title: "x", params: { href: "/threads/env-1/thread-2" } })).toBe(
"/threads/env-1/thread-2",
);
expect(
shortcutHref({ id: "x", title: "x", params: { href: "/threads/env%201/thread%2F2" } }),
).toBe("/threads/env%201/thread%2F2");
});
it("rejects everything else", () => {
for (const href of [
"https://evil.example",
"//evil.example",
"/settings",
"/threads/only-one-segment",
"/threads/a/b/c",
"/threads//x",
"/threads/a/b?x=1",
"/threads/a/b#frag",
"/new/extra",
]) {
expect(shortcutHref({ id: "x", title: "x", params: { href } })).toBe(null);
}
expect(shortcutHref({ id: "x", title: "x", params: { href: 3 } })).toBe(null);
expect(shortcutHref({ id: "x", title: "x" })).toBe(null);
});
});
describe("activeThreadRef", () => {
it("resolves the active Thread route's params", () => {
const ref = activeThreadRef(
navState({ name: "Thread", params: { environmentId: "env-1", threadId: "thread-2" } }),
);
expect(ref).toEqual({ environmentId: "env-1", threadId: "thread-2" });
});
it("takes the first value of array params", () => {
const ref = activeThreadRef(
navState({ name: "Thread", params: { environmentId: ["env-1"], threadId: ["thread-2"] } }),
);
expect(ref).toEqual({ environmentId: "env-1", threadId: "thread-2" });
});
it("returns null for non-thread routes", () => {
expect(activeThreadRef(navState({ name: "Home" }))).toBe(null);
});
it("returns null instead of throwing for malformed params", () => {
const malformed: unknown[] = [
undefined,
{},
{ environmentId: "env-1" },
{ environmentId: "", threadId: "thread-2" },
{ environmentId: " ", threadId: "thread-2" },
{ environmentId: "env-1", threadId: " " },
{ environmentId: 42, threadId: "thread-2" },
{ environmentId: { nested: true }, threadId: "thread-2" },
{ environmentId: [], threadId: [] },
];
for (const params of malformed) {
expect(activeThreadRef(navState({ name: "Thread", params }))).toBe(null);
}
});
});
describe("launcher shortcut ids", () => {
it("cannot collide across different env/thread pairs", () => {
const a = buildShortcutActions([{ environmentId: "a-b", threadId: "c", title: "x" }]);
const b = buildShortcutActions([{ environmentId: "a", threadId: "b-c", title: "x" }]);
expect(a[1]?.id).not.toBe(b[1]?.id);
});
});