/** * POST /api/projects/:projectId/agents/:agentId/config/reset — overwrites * system_config.yaml with the current code defaults, keeping only the identity fields * (name / description / version); the config-side analogue of a skill update. Same * member-level authorization as PUT config; a non-member gets 404 without a write. */ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { defaultSystemConfig } from "@prismshadow/penguin-core"; import type { AgentConfigResponse, ProjectCreateResponse } from "../src/api/types.js"; import { apiClient, createTestApp, provisionUser } from "./helpers.js"; import type { TestApp } from "./helpers.js"; describe("POST agent config reset", () => { let t: TestApp; let alice: ReturnType; let projectId: string; const configUrl = () => `/api/projects/${projectId}/agents/default_agent/config`; beforeEach(async () => { t = await createTestApp(); const a = await provisionUser(t.app, "alice"); alice = apiClient(t.app, a.cookie); // Project ids are username-prefixed (`alice-…`), same as the other route tests. const created = (await ( await alice.post("/api/projects", { projectId: "alice-reset", name: "Reset project" }) ).json()) as ProjectCreateResponse; projectId = created.project.projectId; }); afterEach(async () => { await t.cleanup(); }); it("restores the defaults while preserving name / description / version", async () => { // Customize everything the reset is supposed to overwrite, plus the kept fields. const put = await alice.put(configUrl(), { config: { name: "Custom Name", description: "Custom description", systemPrompt: "CUSTOM PROMPT", maxTurns: 3, model: { maxTokens: 1234 }, compaction: { mode: "discard" }, toolsBuiltin: [], mcpServers: [{ name: "custom-mcp", config: {} }], }, }); expect(put.status).toBe(200); const res = await alice.post(`${configUrl()}/reset`); expect(res.status).toBe(200); const body = (await res.json()) as AgentConfigResponse; const defaults = defaultSystemConfig(); // Identity fields survive. expect(body.config.name).toBe("Custom Name"); expect(body.config.description).toBe("Custom description"); expect(body.config.version).toBe(1); // Everything else is back to the current defaults. expect(body.config.systemPrompt).toBe(defaults.system_prompt); expect(body.config.systemPrompt).toContain("App Data Dir: {{PROJECT_DIR}}"); expect(body.config.maxTurns).toBe(defaults.max_turns); expect(body.config.model?.maxTokens).toBe(defaults.model?.max_tokens); expect(body.config.compaction?.mode).toBe("summarize"); expect(body.config.toolsBuiltin.map((tool) => tool.name)).toEqual( (defaults.tools?.builtin ?? []).map((tool) => tool.name), ); expect(body.config.mcpServers).toEqual([]); // A later GET sees the same reset state (it was persisted, not just echoed). const got = (await (await alice.get(configUrl())).json()) as AgentConfigResponse; expect(got.config.systemPrompt).toBe(defaults.system_prompt); expect(got.config.name).toBe("Custom Name"); }); it("404 for a nonexistent agent (no initialization side effect)", async () => { const res = await alice.post(`/api/projects/${projectId}/agents/ghost/config/reset`); expect(res.status).toBe(404); }); it("404 for a non-member; the config is not modified", async () => { const before = (await (await alice.get(configUrl())).json()) as AgentConfigResponse; const m = await provisionUser(t.app, "mallory"); const mallory = apiClient(t.app, m.cookie); const res = await mallory.post(`${configUrl()}/reset`); expect(res.status).toBe(404); const after = (await (await alice.get(configUrl())).json()) as AgentConfigResponse; expect(after.systemConfigYaml).toBe(before.systemConfigYaml); }); });