feat: initial Lam Agent extension (#1)
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { __resetLocalStorage } from "../__mocks__/@vicinae/api";
|
||||
import { MAX_STORED_CHARS_PER_TURN, MAX_TURNS_PER_SESSION } from "../lib/constants";
|
||||
import {
|
||||
capTurn,
|
||||
capTurns,
|
||||
createSession,
|
||||
createTurn,
|
||||
clearSessions,
|
||||
deleteSession,
|
||||
listSessions,
|
||||
loadSession,
|
||||
saveSession,
|
||||
withTitleFromFirstTurn,
|
||||
} from "../lib/sessions";
|
||||
import type { ChatTurn } from "../lib/types";
|
||||
|
||||
function turn(index: number, response = `answer ${index}`): ChatTurn {
|
||||
return {
|
||||
id: `turn-${index}`,
|
||||
prompt: `question ${index}`,
|
||||
response,
|
||||
thinking: "",
|
||||
model: "test-model",
|
||||
createdAt: 1_000 + index,
|
||||
toolCalls: [],
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
__resetLocalStorage();
|
||||
});
|
||||
|
||||
describe("session store", () => {
|
||||
it("round-trips a session through LocalStorage", async () => {
|
||||
const session = { ...createSession("test-model", 1_000), title: "First chat", turns: [turn(1)] };
|
||||
|
||||
await saveSession(session);
|
||||
|
||||
const summaries = await listSessions();
|
||||
expect(summaries).toHaveLength(1);
|
||||
expect(summaries[0]).toMatchObject({ id: session.id, title: "First chat", turnCount: 1 });
|
||||
|
||||
const loaded = await loadSession(session.id);
|
||||
expect(loaded?.turns[0].response).toBe("answer 1");
|
||||
expect(loaded?.model).toBe("test-model");
|
||||
});
|
||||
|
||||
it("returns undefined for an unknown session id", async () => {
|
||||
expect(await loadSession("does-not-exist")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("orders sessions by most recently updated", async () => {
|
||||
const older = { ...createSession("m", 1_000), title: "older", updatedAt: 1_000 };
|
||||
const newer = { ...createSession("m", 2_000), title: "newer", updatedAt: 5_000 };
|
||||
|
||||
await saveSession(older);
|
||||
await saveSession(newer);
|
||||
|
||||
const summaries = await listSessions();
|
||||
expect(summaries.map((summary) => summary.title)).toEqual(["newer", "older"]);
|
||||
});
|
||||
|
||||
it("updates an existing index entry instead of duplicating it", async () => {
|
||||
const session = createSession("m", 1_000);
|
||||
await saveSession(session);
|
||||
await saveSession({ ...session, title: "renamed", updatedAt: 2_000 });
|
||||
|
||||
const summaries = await listSessions();
|
||||
expect(summaries).toHaveLength(1);
|
||||
expect(summaries[0].title).toBe("renamed");
|
||||
});
|
||||
|
||||
it("deletes a session and its index entry", async () => {
|
||||
const session = createSession("m");
|
||||
await saveSession(session);
|
||||
await deleteSession(session.id);
|
||||
|
||||
expect(await listSessions()).toHaveLength(0);
|
||||
expect(await loadSession(session.id)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("clears everything", async () => {
|
||||
await saveSession(createSession("m", 1));
|
||||
await saveSession(createSession("m", 2));
|
||||
await clearSessions();
|
||||
|
||||
expect(await listSessions()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("survives a corrupt index rather than throwing", async () => {
|
||||
const { LocalStorage } = await import("../__mocks__/@vicinae/api");
|
||||
await LocalStorage.setItem("lam.sessions.index", "{not json");
|
||||
|
||||
await expect(listSessions()).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it("returns undefined for a corrupt session body", async () => {
|
||||
const { LocalStorage } = await import("../__mocks__/@vicinae/api");
|
||||
await LocalStorage.setItem("lam.session.broken", '{"id":"broken"}');
|
||||
|
||||
await expect(loadSession("broken")).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("caps", () => {
|
||||
it("keeps only the most recent turns", () => {
|
||||
const many = Array.from({ length: MAX_TURNS_PER_SESSION + 5 }, (_, index) => turn(index));
|
||||
const capped = capTurns(many);
|
||||
|
||||
expect(capped).toHaveLength(MAX_TURNS_PER_SESSION);
|
||||
// The oldest five should be the ones dropped, not the newest.
|
||||
expect(capped[0].id).toBe("turn-5");
|
||||
expect(capped[capped.length - 1].id).toBe(`turn-${MAX_TURNS_PER_SESSION + 4}`);
|
||||
});
|
||||
|
||||
it("truncates an oversized turn and marks it", () => {
|
||||
const huge = turn(1, "x".repeat(MAX_STORED_CHARS_PER_TURN + 500));
|
||||
const capped = capTurn(huge);
|
||||
|
||||
expect(capped.response.length).toBeLessThan(MAX_STORED_CHARS_PER_TURN + 100);
|
||||
expect(capped.response).toContain("[truncated]");
|
||||
});
|
||||
|
||||
it("applies caps on save so storage cannot grow without bound", async () => {
|
||||
const session = {
|
||||
...createSession("m", 1_000),
|
||||
turns: Array.from({ length: MAX_TURNS_PER_SESSION + 10 }, (_, index) => turn(index)),
|
||||
};
|
||||
|
||||
await saveSession(session);
|
||||
const loaded = await loadSession(session.id);
|
||||
|
||||
expect(loaded?.turns).toHaveLength(MAX_TURNS_PER_SESSION);
|
||||
});
|
||||
});
|
||||
|
||||
describe("withTitleFromFirstTurn", () => {
|
||||
it("names the session from its first prompt", () => {
|
||||
const session = { ...createSession("m", 1_000), turns: [turn(1)] };
|
||||
expect(withTitleFromFirstTurn(session).title).toBe("question 1");
|
||||
});
|
||||
|
||||
it("leaves an already-named session alone", () => {
|
||||
const session = { ...createSession("m", 1_000), title: "Kept", turns: [turn(1)] };
|
||||
expect(withTitleFromFirstTurn(session).title).toBe("Kept");
|
||||
});
|
||||
|
||||
it("leaves an empty session unnamed", () => {
|
||||
expect(withTitleFromFirstTurn(createSession("m", 1_000)).title).toBe("New chat");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createTurn", () => {
|
||||
it("starts with an empty response so the UI can stream into it", () => {
|
||||
const created = createTurn("hello", "test-model", 1_000);
|
||||
expect(created.response).toBe("");
|
||||
expect(created.prompt).toBe("hello");
|
||||
expect(created.toolCalls).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user