feat: initial Lam Agent extension (#1)

This commit was merged in pull request #1.
This commit is contained in:
2026-10-03 20:32:40 +00:00
parent 1d39aabb50
commit 8b507e7953
36 changed files with 7136 additions and 2 deletions
+161
View File
@@ -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([]);
});
});