Files
penguin-harness/packages/server/test/session-delete-scratchpad.test.ts
T
Yaowei Zheng 45bfae6e94 Initialize repository with harness code and assets
Initial import of all source code, config, and README assets: the
packages workspace (cli, core, server, web, docs, landing, skills),
build scripts, tooling config, and CI workflows.

Includes the data-layout revision made on this branch: the local data
root defaults to ~/.penguin/data (PENGUIN_HOME still overrides; the
installer keeps its binaries in ~/.penguin), and every Agent lives
under <project>/agents/<agent>/ — path helpers, the three
agent-enumeration scans, the system prompt, built-in Skills, tests
and docs all follow the new layout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ihk8iQuo3kv2aPjAYEPuR
2026-07-19 14:06:53 +08:00

100 lines
3.9 KiB
TypeScript

/**
* Integration tests for Session deletion cleanup: DELETE /api/sessions/:id
* removes the Session's scratchpad directory (model-generated temp files and
* input images saved to disk for models without image support) in addition
* to its Trace and index row.
*/
import fs from "node:fs/promises";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { scratchpadDir } from "@prismshadow/penguin-core";
import type { ProjectCreateResponse, SessionCreateResponse } from "../src/api/types.js";
import { apiClient, createTestApp, provisionUser } from "./helpers.js";
import type { TestApp } from "./helpers.js";
async function exists(p: string): Promise<boolean> {
try {
await fs.access(p);
return true;
} catch {
return false;
}
}
describe("会话删除清理 scratchpad", () => {
let t: TestApp;
let owner: ReturnType<typeof apiClient>;
let projectId: string;
beforeEach(async () => {
t = await createTestApp();
const a = await provisionUser(t.app, "owner_s");
owner = apiClient(t.app, a.cookie);
const created = (await (
await owner.post("/api/projects", {
projectId: "owner_s-scratchpad",
name: "scratchpad 项目",
})
).json()) as ProjectCreateResponse;
projectId = created.project.projectId;
await owner.put(`/api/projects/${projectId}/models`, {
defaultModel: { provider: "anthropic", modelId: "claude-sonnet-4-6" },
models: [{ provider: "anthropic", modelId: "claude-sonnet-4-6" }],
});
});
afterEach(async () => {
await t.cleanup();
});
it("DELETE 会话后其 scratchpad 目录一并删除", async () => {
const created = await owner.post(
`/api/projects/${projectId}/agents/default_agent/sessions`,
{},
);
expect(created.status).toBe(201);
const { session } = (await created.json()) as SessionCreateResponse;
// Simulate temp files written during the session (input images / model-generated files).
const dir = path.join(scratchpadDir(t.root, projectId, "default_agent"), session.sessionId);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(path.join(dir, "upload-1.png"), "fake");
expect(await exists(dir)).toBe(true);
const del = await owner.delete(`/api/sessions/${session.sessionId}`);
expect(del.status).toBe(204);
expect(await exists(dir)).toBe(false);
// Deleting a session with no scratchpad also succeeds (rm force is idempotent).
const created2 = await owner.post(
`/api/projects/${projectId}/agents/default_agent/sessions`,
{},
);
const { session: s2 } = (await created2.json()) as SessionCreateResponse;
expect((await owner.delete(`/api/sessions/${s2.sessionId}`)).status).toBe(204);
});
it("GET /sessions/:id/scratchpad/:file 按会话读取文件;缺失或非法文件名 404", async () => {
const created = await owner.post(
`/api/projects/${projectId}/agents/default_agent/sessions`,
{},
);
const { session } = (await created.json()) as SessionCreateResponse;
const dir = path.join(scratchpadDir(t.root, projectId, "default_agent"), session.sessionId);
await fs.mkdir(dir, { recursive: true });
const png = Buffer.from("89504e470d0a1a0a", "hex"); // just needs the PNG magic bytes
await fs.writeFile(path.join(dir, "upload-1.png"), png);
const res = await owner.get(`/api/sessions/${session.sessionId}/scratchpad/upload-1.png`);
expect(res.status).toBe(200);
expect(res.headers.get("content-type")).toBe("image/png");
expect(Buffer.from(await res.arrayBuffer())).toEqual(png);
// Missing files and filenames with path separators/traversal both 404 (no existence leak).
expect((await owner.get(`/api/sessions/${session.sessionId}/scratchpad/nope.png`)).status).toBe(
404,
);
expect(
(await owner.get(`/api/sessions/${session.sessionId}/scratchpad/..%2Fsecret.png`)).status,
).toBe(404);
});
});