Adds optional path_mappings to agent.yaml so host directories are transparently mapped to virtual paths in agent responses and CLAUDE.md. Sanitizer replaces host paths (longest first, both slash styles) before repoRoot/home substitutions. /new-agent accepts a host:virtual path option with Windows-safe splitting and fs.existsSync validation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
128 lines
5.3 KiB
TypeScript
128 lines
5.3 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { sanitizeForDiscord } from "../../src/runtime/sanitize";
|
|
|
|
describe("sanitizeForDiscord", () => {
|
|
// (a) Repo-root path is replaced with <disclaw>
|
|
it("replaces repoRoot with <disclaw>", () => {
|
|
const text = "Reading config from /home/user/project/disclaw.yaml";
|
|
const result = sanitizeForDiscord(text, { repoRoot: "/home/user/project" });
|
|
expect(result).toBe("Reading config from <disclaw>/disclaw.yaml");
|
|
expect(result).not.toContain("/home/user/project");
|
|
});
|
|
|
|
// (b) Home directory is replaced with ~
|
|
it("replaces home directory with ~", () => {
|
|
const text = "Workspace located at /home/user/.disclaw/workspaces/agent-x";
|
|
const result = sanitizeForDiscord(text, { home: "/home/user" });
|
|
expect(result).toBe("Workspace located at ~/.disclaw/workspaces/agent-x");
|
|
expect(result).not.toContain("/home/user");
|
|
});
|
|
|
|
// (c) Discord bot token pattern is redacted
|
|
it("redacts Discord bot tokens", () => {
|
|
const token = "MTA1NjYwNDczNTA3NDU2NjE2.GbXkRa.abcdefghijklmnopqrstuvwxyz123";
|
|
const text = `Bot initialized with token ${token} — ready.`;
|
|
const result = sanitizeForDiscord(text);
|
|
expect(result).not.toContain(token);
|
|
expect(result).toContain("<redacted>");
|
|
});
|
|
|
|
// (d) Anthropic API key is redacted
|
|
it("redacts Anthropic API keys", () => {
|
|
const key = "sk-ant-api03-ABCDEFGHIJ1234567890abcdefghij";
|
|
const text = `Using key ${key} for requests.`;
|
|
const result = sanitizeForDiscord(text);
|
|
expect(result).not.toContain(key);
|
|
expect(result).toContain("<redacted>");
|
|
});
|
|
|
|
// (e) Harmless text is not modified (no false positives)
|
|
it("does not modify harmless text", () => {
|
|
const text = "Hello! The agent processed your request successfully in 120ms.";
|
|
const result = sanitizeForDiscord(text, { home: "/home/user", repoRoot: "/home/user/project" });
|
|
expect(result).toBe(text);
|
|
});
|
|
|
|
// (f) Empty string returns empty string
|
|
it("handles empty string input", () => {
|
|
expect(sanitizeForDiscord("")).toBe("");
|
|
expect(sanitizeForDiscord("", { home: "/home/user", repoRoot: "/home/user/project" })).toBe("");
|
|
});
|
|
|
|
// (g) No opts provided — plain text stays unchanged when no token patterns present
|
|
it("returns text unchanged when no opts and no token patterns", () => {
|
|
const text = "This is a perfectly normal response from the agent.";
|
|
expect(sanitizeForDiscord(text)).toBe(text);
|
|
});
|
|
|
|
// Ordering: repoRoot (sub-path of home) is replaced before home
|
|
it("replaces repoRoot before home so nested paths are handled correctly", () => {
|
|
const text = "/home/user/project/src/index.ts and /home/user/other.ts";
|
|
const result = sanitizeForDiscord(text, {
|
|
repoRoot: "/home/user/project",
|
|
home: "/home/user",
|
|
});
|
|
expect(result).toBe("<disclaw>/src/index.ts and ~/other.ts");
|
|
});
|
|
|
|
// Windows-style backslash paths are sanitized
|
|
it("replaces Windows backslash paths", () => {
|
|
const text = "File at C:\\Users\\dev\\.disclaw\\workspaces\\agent-x\\src\\main.ts";
|
|
const result = sanitizeForDiscord(text, {
|
|
repoRoot: "C:\\Users\\dev\\.disclaw\\workspaces\\agent-x",
|
|
home: "C:\\Users\\dev",
|
|
});
|
|
expect(result).toBe("File at <disclaw>\\src\\main.ts");
|
|
});
|
|
|
|
// Mixed slash styles (common on Windows when tools output forward slashes)
|
|
it("replaces forward-slash variants of Windows paths", () => {
|
|
const text = "File at C:/Users/dev/.disclaw/workspaces/agent-x/src/main.ts";
|
|
const result = sanitizeForDiscord(text, {
|
|
repoRoot: "C:\\Users\\dev\\.disclaw\\workspaces\\agent-x",
|
|
home: "C:\\Users\\dev",
|
|
});
|
|
expect(result).toBe("File at <disclaw>/src/main.ts");
|
|
});
|
|
|
|
describe("pathMappings", () => {
|
|
it("replaces host path with virtual path", () => {
|
|
const text = "Working on /home/user/myproject/src/index.ts";
|
|
const result = sanitizeForDiscord(text, {
|
|
pathMappings: [{ host: "/home/user/myproject", virtual: "~/myproject" }],
|
|
});
|
|
expect(result).toBe("Working on ~/myproject/src/index.ts");
|
|
expect(result).not.toContain("/home/user/myproject");
|
|
});
|
|
|
|
it("handles both slash styles (forward and backslash)", () => {
|
|
const textFwd = "File at C:/Code/projekt/main.ts";
|
|
const textBwd = "File at C:\\Code\\projekt\\main.ts";
|
|
const opts = {
|
|
pathMappings: [{ host: "C:\\Code\\projekt", virtual: "~/projekt" }],
|
|
};
|
|
expect(sanitizeForDiscord(textFwd, opts)).toBe("File at ~/projekt/main.ts");
|
|
expect(sanitizeForDiscord(textBwd, opts)).toBe("File at ~/projekt\\main.ts");
|
|
});
|
|
|
|
it("applies longest match first", () => {
|
|
const text = "/home/user/projects/specific/file.ts and /home/user/projects/other.ts";
|
|
const result = sanitizeForDiscord(text, {
|
|
pathMappings: [
|
|
{ host: "/home/user/projects", virtual: "~/projects" },
|
|
{ host: "/home/user/projects/specific", virtual: "~/specific" },
|
|
],
|
|
});
|
|
// The more specific (longer) mapping must win for the first path
|
|
expect(result).toBe("~/specific/file.ts and ~/projects/other.ts");
|
|
});
|
|
|
|
it("does not affect text without host paths", () => {
|
|
const text = "Hello! Everything is running smoothly.";
|
|
const result = sanitizeForDiscord(text, {
|
|
pathMappings: [{ host: "/home/user/myproject", virtual: "~/myproject" }],
|
|
});
|
|
expect(result).toBe(text);
|
|
});
|
|
});
|
|
});
|