816a823b75
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
106 lines
4.3 KiB
TypeScript
106 lines
4.3 KiB
TypeScript
/**
|
|
* Agent lifecycle: new session id format + no more `.penguin` symlink in the Workspace.
|
|
*
|
|
* - sessionId looks like `session-YYYY-MM-DD-HH-mm-ss-<8-digit hex>` (local time, zero-padded fields).
|
|
* - createSession no longer creates any `.penguin` symlink inside the Workspace, nor touches existing
|
|
* Workspace files; the model reaches Agent State and other absolute paths directly by combining the
|
|
* App Data Dir / Agent ID placeholders from the system prompt.
|
|
*/
|
|
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import { createAgent } from "../src/index.js";
|
|
import { formatSessionId } from "../src/internal/session-support.js";
|
|
import { projectDir } from "../src/state/paths.js";
|
|
import { stubProviderKeys } from "./provider-keys.js";
|
|
|
|
let tmpRoot: string;
|
|
let prevHome: string | undefined;
|
|
let restoreKeys: () => void;
|
|
|
|
beforeEach(async () => {
|
|
prevHome = process.env.PENGUIN_HOME;
|
|
tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "penguin-harness-lifecycle-"));
|
|
process.env.PENGUIN_HOME = tmpRoot;
|
|
restoreKeys = stubProviderKeys();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
if (prevHome === undefined) delete process.env.PENGUIN_HOME;
|
|
else process.env.PENGUIN_HOME = prevHome;
|
|
restoreKeys();
|
|
await fs.rm(tmpRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
const SESSION_ID_RE = /^session-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}-[0-9a-f]{8}$/;
|
|
|
|
describe("formatSessionId", () => {
|
|
it("matches the session-YYYY-MM-DD-HH-mm-ss-<8hex> format", () => {
|
|
expect(formatSessionId()).toMatch(SESSION_ID_RE);
|
|
});
|
|
|
|
it("uses local time fields with zero padding", () => {
|
|
// 2026-06-19 15:28:08 local time -> session-2026-06-19-15-28-08-<hex>.
|
|
const d = new Date(2026, 5, 19, 15, 28, 8);
|
|
expect(formatSessionId(d)).toMatch(/^session-2026-06-19-15-28-08-[0-9a-f]{8}$/);
|
|
});
|
|
|
|
it("generates distinct ids on repeated calls", () => {
|
|
expect(formatSessionId()).not.toBe(formatSessionId());
|
|
});
|
|
});
|
|
|
|
describe("Agent.createSession session id + no .penguin symlink", () => {
|
|
it("assigns a sessionId in the new format", async () => {
|
|
const agent = await createAgent();
|
|
const ws = path.join(tmpRoot, "ws");
|
|
await fs.mkdir(ws, { recursive: true });
|
|
const session = await agent.createSession({ workspaceDir: ws });
|
|
expect(session.sessionId).toMatch(SESSION_ID_RE);
|
|
});
|
|
|
|
it("does not create a .penguin entry in the workspace", async () => {
|
|
const agent = await createAgent();
|
|
const ws = path.join(tmpRoot, "ws");
|
|
await fs.mkdir(ws, { recursive: true });
|
|
await agent.createSession({ workspaceDir: ws });
|
|
await expect(fs.lstat(path.join(ws, ".penguin"))).rejects.toThrow();
|
|
// agent_state also no longer creates traces/notes symlinks.
|
|
await expect(fs.lstat(path.join(agent.state.stateDir, "traces"))).rejects.toThrow();
|
|
await expect(fs.lstat(path.join(agent.state.stateDir, "notes"))).rejects.toThrow();
|
|
});
|
|
|
|
it("leaves a user's pre-existing .penguin file untouched", async () => {
|
|
const agent = await createAgent();
|
|
const ws = path.join(tmpRoot, "ws");
|
|
await fs.mkdir(ws, { recursive: true });
|
|
const linkPath = path.join(ws, ".penguin");
|
|
await fs.writeFile(linkPath, "user-data", "utf8");
|
|
await agent.createSession({ workspaceDir: ws });
|
|
expect(await fs.readFile(linkPath, "utf8")).toBe("user-data");
|
|
});
|
|
|
|
it("is idempotent: repeated createSession registers no exit listeners", async () => {
|
|
const agent = await createAgent();
|
|
const ws = path.join(tmpRoot, "ws");
|
|
await fs.mkdir(ws, { recursive: true });
|
|
const before = process.listenerCount("exit");
|
|
for (let i = 0; i < 12; i++) {
|
|
await agent.createSession({ workspaceDir: ws });
|
|
}
|
|
expect(process.listenerCount("exit") - before).toBe(0);
|
|
});
|
|
|
|
it("injects App Data Dir and Agent ID into the assembled system prompt", async () => {
|
|
const agent = await createAgent();
|
|
const ws = path.join(tmpRoot, "ws");
|
|
await fs.mkdir(ws, { recursive: true });
|
|
const session = await agent.createSession({ workspaceDir: ws });
|
|
const prompt = (session.metaMessage.payload as { system_prompt: string }).system_prompt;
|
|
expect(prompt).toContain(`Agent ID: ${agent.state.agentId}`);
|
|
expect(prompt).toContain(`App Data Dir: ${projectDir(tmpRoot, agent.state.projectId)}`);
|
|
expect(prompt).not.toContain(".penguin");
|
|
});
|
|
});
|