0eaaa54e25
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
85 lines
2.7 KiB
TypeScript
85 lines
2.7 KiB
TypeScript
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 { assertValidId, isValidId, loadOrInitAgentState } from "../src/state/index.js";
|
|
|
|
describe("isValidId / assertValidId", () => {
|
|
const valid = ["default_project", "default_agent", "my-agent", "agent-1", "ABC_123"];
|
|
// Only alphanumerics, `_`, and `-` are allowed; dots, spaces/tabs, path separators, and any
|
|
// other character are all rejected.
|
|
const invalid = [
|
|
"",
|
|
" ",
|
|
"a b",
|
|
"a\tb",
|
|
" lead",
|
|
"trail ",
|
|
"a/b",
|
|
"a\\b",
|
|
".",
|
|
"..",
|
|
"...",
|
|
"a.b",
|
|
"v1.0",
|
|
".hidden",
|
|
"中文ok",
|
|
"emoji😀",
|
|
];
|
|
|
|
it.each(valid)("accepts a valid single-segment directory name: %j", (id) => {
|
|
expect(isValidId(id)).toBe(true);
|
|
expect(() => assertValidId("project_id", id)).not.toThrow();
|
|
expect(() => assertValidId("agent_id", id)).not.toThrow();
|
|
});
|
|
|
|
it.each(invalid)("rejects an invalid id: %j", (id) => {
|
|
expect(isValidId(id)).toBe(false);
|
|
expect(() => assertValidId("agent_id", id)).toThrow();
|
|
});
|
|
|
|
it("the error message names the kind and the invalid id", () => {
|
|
expect(() => assertValidId("agent_id", "a/b")).toThrow(/agent_id/);
|
|
expect(() => assertValidId("agent_id", "a/b")).toThrow(/a\/b/);
|
|
expect(() => assertValidId("project_id", "..")).toThrow(/project_id/);
|
|
});
|
|
});
|
|
|
|
describe("loadOrInitAgentState id validation", () => {
|
|
let tmpRoot: string;
|
|
let prevHome: string | undefined;
|
|
|
|
beforeEach(async () => {
|
|
prevHome = process.env.PENGUIN_HOME;
|
|
tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "penguin-validate-id-"));
|
|
process.env.PENGUIN_HOME = tmpRoot;
|
|
});
|
|
|
|
afterEach(async () => {
|
|
if (prevHome === undefined) {
|
|
delete process.env.PENGUIN_HOME;
|
|
} else {
|
|
process.env.PENGUIN_HOME = prevHome;
|
|
}
|
|
await fs.rm(tmpRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
it("throws when agentId contains a path separator", async () => {
|
|
await expect(loadOrInitAgentState({ agentId: "a/b" })).rejects.toThrow(/agent_id/);
|
|
});
|
|
|
|
it("throws when projectId is ..", async () => {
|
|
await expect(loadOrInitAgentState({ projectId: ".." })).rejects.toThrow(/project_id/);
|
|
});
|
|
|
|
it("throws on the Windows trailing-space variant '.. ' as agentId (blocks a path-traversal bypass)", async () => {
|
|
await expect(loadOrInitAgentState({ agentId: ".. " })).rejects.toThrow(/agent_id/);
|
|
});
|
|
|
|
it("the default ids are valid and initialization succeeds", async () => {
|
|
const state = await loadOrInitAgentState();
|
|
expect(state.projectId).toBe("default_project");
|
|
expect(state.agentId).toBe("default_agent");
|
|
});
|
|
});
|