refactor(agent): tighten harness environment and resources
This commit is contained in:
148
packages/agent/test/harness/session.test.ts
Normal file
148
packages/agent/test/harness/session.test.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Session } from "../../src/harness/session/session.js";
|
||||
import { JsonlSessionStorage } from "../../src/harness/session/storage/jsonl.js";
|
||||
import { InMemorySessionStorage } from "../../src/harness/session/storage/memory.js";
|
||||
import type { SessionStorage } from "../../src/harness/types.js";
|
||||
import { createAssistantMessage, createTempDir, createUserMessage, getLatestTempDir } from "./session-test-utils.js";
|
||||
|
||||
async function runSessionSuite(
|
||||
name: string,
|
||||
createStorage: () => SessionStorage | Promise<SessionStorage>,
|
||||
inspect?: () => void,
|
||||
) {
|
||||
describe(name, () => {
|
||||
it("appends messages and builds context in order", async () => {
|
||||
const session = new Session(await createStorage());
|
||||
await session.appendMessage(createUserMessage("one"));
|
||||
await session.appendMessage(createAssistantMessage("two"));
|
||||
const context = await session.buildContext();
|
||||
expect(context.messages.map((message) => message.role)).toEqual(["user", "assistant"]);
|
||||
});
|
||||
|
||||
it("tracks model and thinking level changes", async () => {
|
||||
const session = new Session(await createStorage());
|
||||
await session.appendMessage(createUserMessage("one"));
|
||||
await session.appendModelChange("openai", "gpt-4.1");
|
||||
await session.appendThinkingLevelChange("high");
|
||||
const context = await session.buildContext();
|
||||
expect(context.thinkingLevel).toBe("high");
|
||||
expect(context.model).toEqual({ provider: "openai", modelId: "gpt-4.1" });
|
||||
});
|
||||
|
||||
it("supports branching by moving the leaf and appending a new branch", async () => {
|
||||
const session = new Session(await createStorage());
|
||||
const user1 = await session.appendMessage(createUserMessage("one"));
|
||||
const assistant1 = await session.appendMessage(createAssistantMessage("two"));
|
||||
await session.appendMessage(createUserMessage("three"));
|
||||
await session.moveTo(user1);
|
||||
await session.appendMessage(createAssistantMessage("branched"));
|
||||
const branch = await session.getBranch();
|
||||
expect(branch.map((entry) => entry.id)).toContain(user1);
|
||||
expect(branch.map((entry) => entry.id)).not.toContain(assistant1);
|
||||
const context = await session.buildContext();
|
||||
expect(context.messages.map((message) => message.role)).toEqual(["user", "assistant"]);
|
||||
});
|
||||
|
||||
it("supports moving the leaf to root", async () => {
|
||||
const session = new Session(await createStorage());
|
||||
await session.appendMessage(createUserMessage("one"));
|
||||
await session.moveTo(null);
|
||||
expect(await session.getLeafId()).toBeNull();
|
||||
expect((await session.buildContext()).messages).toEqual([]);
|
||||
});
|
||||
|
||||
it("reconstructs compaction summaries in context", async () => {
|
||||
const session = new Session(await createStorage());
|
||||
await session.appendMessage(createUserMessage("one"));
|
||||
await session.appendMessage(createAssistantMessage("two"));
|
||||
const user2 = await session.appendMessage(createUserMessage("three"));
|
||||
await session.appendMessage(createAssistantMessage("four"));
|
||||
await session.appendCompaction("summary", user2, 1234);
|
||||
await session.appendMessage(createUserMessage("five"));
|
||||
const context = await session.buildContext();
|
||||
expect(context.messages[0]?.role).toBe("compactionSummary");
|
||||
expect(context.messages).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("supports moving with branch summary entries in context", async () => {
|
||||
const session = new Session(await createStorage());
|
||||
const user1 = await session.appendMessage(createUserMessage("one"));
|
||||
const summaryId = await session.moveTo(user1, { summary: "summary text" });
|
||||
expect(summaryId).toBeTruthy();
|
||||
const summaryEntry = await session.getEntry(summaryId!);
|
||||
expect(summaryEntry).toMatchObject({ type: "branch_summary", parentId: user1, fromId: user1 });
|
||||
const context = await session.buildContext();
|
||||
expect(context.messages[1]?.role).toBe("branchSummary");
|
||||
});
|
||||
|
||||
it("supports custom message entries in context", async () => {
|
||||
const session = new Session(await createStorage());
|
||||
await session.appendMessage(createUserMessage("one"));
|
||||
await session.appendCustomMessageEntry("custom", "hello", true, { ok: true });
|
||||
const context = await session.buildContext();
|
||||
expect(context.messages[1]?.role).toBe("custom");
|
||||
});
|
||||
|
||||
it("supports labels and session info entries without affecting context", async () => {
|
||||
const session = new Session(await createStorage());
|
||||
const user1 = await session.appendMessage(createUserMessage("one"));
|
||||
await session.appendLabel(user1, "checkpoint");
|
||||
await session.appendSessionName("name");
|
||||
const entries = await session.getEntries();
|
||||
expect(entries.some((entry) => entry.type === "label")).toBe(true);
|
||||
expect(entries.some((entry) => entry.type === "session_info")).toBe(true);
|
||||
expect(await session.getLabel(user1)).toBe("checkpoint");
|
||||
expect(await session.getSessionName()).toBe("name");
|
||||
expect((await session.buildContext()).messages).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("rejects labels for missing entries", async () => {
|
||||
const session = new Session(await createStorage());
|
||||
await expect(session.appendLabel("missing", "checkpoint")).rejects.toThrow("Entry missing not found");
|
||||
});
|
||||
|
||||
it("persists leaf changes and appended entries via storage", async () => {
|
||||
const storage = await createStorage();
|
||||
const session = new Session(storage);
|
||||
const user1 = await session.appendMessage(createUserMessage("one"));
|
||||
await session.appendMessage(createAssistantMessage("two"));
|
||||
await session.appendLabel(user1, "checkpoint");
|
||||
await session.appendSessionName("name");
|
||||
await session.moveTo(user1);
|
||||
await session.appendMessage(createAssistantMessage("branched"));
|
||||
const session2 = new Session(storage);
|
||||
const context = await session2.buildContext();
|
||||
expect(context.messages.map((message) => message.role)).toEqual(["user", "assistant"]);
|
||||
expect(await session2.getLabel(user1)).toBe("checkpoint");
|
||||
expect(await session2.getSessionName()).toBe("name");
|
||||
inspect?.();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
runSessionSuite("Session with in-memory storage", () => new InMemorySessionStorage());
|
||||
|
||||
runSessionSuite(
|
||||
"Session with JSONL storage",
|
||||
async () => {
|
||||
const dir = createTempDir();
|
||||
return await JsonlSessionStorage.create(join(dir, "session.jsonl"), { cwd: dir, sessionId: "session-1" });
|
||||
},
|
||||
() => {
|
||||
const dir = getLatestTempDir();
|
||||
const filePath = join(dir, "session.jsonl");
|
||||
const lines = readFileSync(filePath, "utf8").trim().split("\n");
|
||||
expect(lines.length).toBeGreaterThan(1);
|
||||
const header = JSON.parse(lines[0]!);
|
||||
expect(header.type).toBe("session");
|
||||
expect(header.version).toBe(3);
|
||||
for (const line of lines.slice(1)) {
|
||||
const entry = JSON.parse(line);
|
||||
expect(entry.type).not.toBe("entry");
|
||||
expect(entry.type).not.toBe("leaf");
|
||||
expect(typeof entry.id).toBe("string");
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user