mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
feat: vendor permission system source
This commit is contained in:
@@ -0,0 +1,612 @@
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { FilePolicyLoader } from "#src/policy-loader";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function makeTempDir(): string {
|
||||
return mkdtempSync(join(tmpdir(), "policy-loader-test-"));
|
||||
}
|
||||
|
||||
function makeLoader(
|
||||
baseDir: string,
|
||||
options: {
|
||||
globalConfig?: Record<string, unknown>;
|
||||
mcpServerNames?: readonly string[];
|
||||
} = {},
|
||||
) {
|
||||
const agentsDir = join(baseDir, "agents");
|
||||
mkdirSync(agentsDir, { recursive: true });
|
||||
|
||||
const globalConfigPath = join(baseDir, "config.json");
|
||||
writeFileSync(
|
||||
globalConfigPath,
|
||||
JSON.stringify(options.globalConfig ?? {}, null, 2),
|
||||
);
|
||||
|
||||
return new FilePolicyLoader({
|
||||
globalConfigPath,
|
||||
agentsDir,
|
||||
mcpServerNames: options.mcpServerNames
|
||||
? [...options.mcpServerNames]
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// loadGlobalConfig
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("FilePolicyLoader.loadGlobalConfig", () => {
|
||||
it("returns ScopeConfig with permission from a valid config file", () => {
|
||||
const baseDir = makeTempDir();
|
||||
try {
|
||||
const loader = makeLoader(baseDir, {
|
||||
globalConfig: { permission: { "*": "allow", read: "ask" } },
|
||||
});
|
||||
const config = loader.loadGlobalConfig();
|
||||
expect(config.permission).toEqual({ "*": "allow", read: "ask" });
|
||||
} finally {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("returns empty ScopeConfig when config file is missing", () => {
|
||||
const loader = new FilePolicyLoader({
|
||||
globalConfigPath: "/nonexistent/config.json",
|
||||
agentsDir: "/nonexistent/agents",
|
||||
});
|
||||
const config = loader.loadGlobalConfig();
|
||||
expect(config.permission).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns empty ScopeConfig when config file has no permission key", () => {
|
||||
const baseDir = makeTempDir();
|
||||
try {
|
||||
const loader = makeLoader(baseDir, {
|
||||
globalConfig: { debugLog: true },
|
||||
});
|
||||
const config = loader.loadGlobalConfig();
|
||||
expect(config.permission).toBeUndefined();
|
||||
} finally {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// loadProjectConfig
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("FilePolicyLoader.loadProjectConfig", () => {
|
||||
it("returns empty ScopeConfig when no project path is configured", () => {
|
||||
const loader = new FilePolicyLoader({
|
||||
globalConfigPath: "/nonexistent/config.json",
|
||||
agentsDir: "/nonexistent/agents",
|
||||
});
|
||||
const config = loader.loadProjectConfig();
|
||||
expect(config).toEqual({});
|
||||
});
|
||||
|
||||
it("returns ScopeConfig from a project config file", () => {
|
||||
const baseDir = makeTempDir();
|
||||
try {
|
||||
const projectConfigPath = join(baseDir, "project-config.json");
|
||||
writeFileSync(
|
||||
projectConfigPath,
|
||||
JSON.stringify({ permission: { bash: "allow" } }),
|
||||
);
|
||||
const loader = new FilePolicyLoader({
|
||||
globalConfigPath: "/nonexistent/config.json",
|
||||
agentsDir: "/nonexistent/agents",
|
||||
projectGlobalConfigPath: projectConfigPath,
|
||||
});
|
||||
const config = loader.loadProjectConfig();
|
||||
expect(config.permission).toEqual({ bash: "allow" });
|
||||
expect(config.invalid).toBeUndefined();
|
||||
} finally {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("marks the scope invalid when the project config file is rejected", () => {
|
||||
const baseDir = makeTempDir();
|
||||
try {
|
||||
const projectConfigPath = join(baseDir, "project-config.json");
|
||||
// Unrecognized key — schema rejection, the issue's "typo" scenario.
|
||||
writeFileSync(projectConfigPath, JSON.stringify({ debugLo: true }));
|
||||
const loader = new FilePolicyLoader({
|
||||
globalConfigPath: "/nonexistent/config.json",
|
||||
agentsDir: "/nonexistent/agents",
|
||||
projectGlobalConfigPath: projectConfigPath,
|
||||
});
|
||||
const config = loader.loadProjectConfig();
|
||||
expect(config.invalid).toBe(true);
|
||||
} finally {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("leaves invalid unset when no project file is present", () => {
|
||||
const loader = new FilePolicyLoader({
|
||||
globalConfigPath: "/nonexistent/config.json",
|
||||
agentsDir: "/nonexistent/agents",
|
||||
projectGlobalConfigPath: "/nonexistent/project-config.json",
|
||||
});
|
||||
const config = loader.loadProjectConfig();
|
||||
expect(config.invalid).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// loadAgentConfig / loadProjectAgentConfig
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("FilePolicyLoader.loadAgentConfig", () => {
|
||||
it("returns empty ScopeConfig when agentName is undefined", () => {
|
||||
const baseDir = makeTempDir();
|
||||
try {
|
||||
const loader = makeLoader(baseDir);
|
||||
expect(loader.loadAgentConfig()).toEqual({});
|
||||
} finally {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("returns ScopeConfig from agent frontmatter", () => {
|
||||
const baseDir = makeTempDir();
|
||||
try {
|
||||
const agentsDir = join(baseDir, "agents");
|
||||
mkdirSync(agentsDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(agentsDir, "coder.md"),
|
||||
`---\npermission:\n bash: allow\n---\n# Coder agent\n`,
|
||||
);
|
||||
const loader = new FilePolicyLoader({
|
||||
globalConfigPath: join(baseDir, "config.json"),
|
||||
agentsDir,
|
||||
});
|
||||
writeFileSync(join(baseDir, "config.json"), "{}");
|
||||
const config = loader.loadAgentConfig("coder");
|
||||
expect(config.permission).toEqual({ bash: "allow" });
|
||||
} finally {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("returns empty ScopeConfig when agent file does not exist", () => {
|
||||
const baseDir = makeTempDir();
|
||||
try {
|
||||
const loader = makeLoader(baseDir);
|
||||
const config = loader.loadAgentConfig("nonexistent");
|
||||
expect(config).toEqual({});
|
||||
expect(config.invalid).toBeUndefined();
|
||||
} finally {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("marks the scope invalid when an existing agent file cannot be read", () => {
|
||||
const baseDir = makeTempDir();
|
||||
try {
|
||||
const agentsDir = join(baseDir, "agents");
|
||||
mkdirSync(agentsDir, { recursive: true });
|
||||
// A directory at the agent's file path exists (stat succeeds) but
|
||||
// readFileSync throws EISDIR — a present-but-unreadable file.
|
||||
mkdirSync(join(agentsDir, "coder.md"));
|
||||
const loader = new FilePolicyLoader({
|
||||
globalConfigPath: join(baseDir, "config.json"),
|
||||
agentsDir,
|
||||
});
|
||||
writeFileSync(join(baseDir, "config.json"), "{}");
|
||||
const config = loader.loadAgentConfig("coder");
|
||||
expect(config.invalid).toBe(true);
|
||||
} finally {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("FilePolicyLoader.loadProjectAgentConfig", () => {
|
||||
it("returns empty ScopeConfig when no projectAgentsDir is configured", () => {
|
||||
const baseDir = makeTempDir();
|
||||
try {
|
||||
const loader = makeLoader(baseDir);
|
||||
expect(loader.loadProjectAgentConfig("coder")).toEqual({});
|
||||
} finally {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getConfigIssues
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("FilePolicyLoader.getConfigIssues", () => {
|
||||
it("returns empty array before any loads", () => {
|
||||
const loader = new FilePolicyLoader({
|
||||
globalConfigPath: "/nonexistent/config.json",
|
||||
agentsDir: "/nonexistent/agents",
|
||||
});
|
||||
expect(loader.getConfigIssues()).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns empty array for valid config", () => {
|
||||
const baseDir = makeTempDir();
|
||||
try {
|
||||
const loader = makeLoader(baseDir, {
|
||||
globalConfig: { permission: { "*": "ask" } },
|
||||
});
|
||||
loader.loadGlobalConfig();
|
||||
expect(loader.getConfigIssues()).toEqual([]);
|
||||
} finally {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getResolvedPolicyPaths
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("FilePolicyLoader.getResolvedPolicyPaths", () => {
|
||||
it("returns correct paths and existence when files exist", () => {
|
||||
const baseDir = makeTempDir();
|
||||
try {
|
||||
const globalConfigPath = join(baseDir, "config.json");
|
||||
const agentsDir = join(baseDir, "agents");
|
||||
writeFileSync(globalConfigPath, "{}");
|
||||
mkdirSync(agentsDir, { recursive: true });
|
||||
|
||||
const loader = new FilePolicyLoader({ globalConfigPath, agentsDir });
|
||||
const paths = loader.getResolvedPolicyPaths();
|
||||
|
||||
expect(paths.globalConfigPath).toBe(globalConfigPath);
|
||||
expect(paths.globalConfigExists).toBe(true);
|
||||
expect(paths.agentsDir).toBe(agentsDir);
|
||||
expect(paths.agentsDirExists).toBe(true);
|
||||
expect(paths.projectConfigPath).toBeNull();
|
||||
expect(paths.projectConfigExists).toBe(false);
|
||||
expect(paths.projectAgentsDir).toBeNull();
|
||||
expect(paths.projectAgentsDirExists).toBe(false);
|
||||
} finally {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getCacheStamp
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("FilePolicyLoader.getCacheStamp", () => {
|
||||
it("returns a string stamp", () => {
|
||||
const loader = new FilePolicyLoader({
|
||||
globalConfigPath: "/nonexistent/config.json",
|
||||
agentsDir: "/nonexistent/agents",
|
||||
});
|
||||
const stamp = loader.getCacheStamp();
|
||||
expect(typeof stamp).toBe("string");
|
||||
expect(stamp.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("changes when the global config file changes", () => {
|
||||
const baseDir = makeTempDir();
|
||||
try {
|
||||
const globalConfigPath = join(baseDir, "config.json");
|
||||
writeFileSync(globalConfigPath, "{}");
|
||||
const agentsDir = join(baseDir, "agents");
|
||||
mkdirSync(agentsDir, { recursive: true });
|
||||
|
||||
const loader = new FilePolicyLoader({ globalConfigPath, agentsDir });
|
||||
const stamp1 = loader.getCacheStamp();
|
||||
|
||||
// Wait a tick so mtime changes
|
||||
const now = Date.now();
|
||||
while (Date.now() - now < 50) {
|
||||
// busy-wait for mtime resolution
|
||||
}
|
||||
writeFileSync(globalConfigPath, '{"permission": {}}');
|
||||
const stamp2 = loader.getCacheStamp();
|
||||
|
||||
expect(stamp1).not.toBe(stamp2);
|
||||
} finally {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("includes agent stamp when agentName is provided and agent file exists", () => {
|
||||
const baseDir = makeTempDir();
|
||||
try {
|
||||
const agentsDir = join(baseDir, "agents");
|
||||
mkdirSync(agentsDir, { recursive: true });
|
||||
writeFileSync(join(baseDir, "config.json"), "{}");
|
||||
writeFileSync(
|
||||
join(agentsDir, "coder.md"),
|
||||
"---\npermission:\n read: allow\n---\n",
|
||||
);
|
||||
|
||||
const loader = new FilePolicyLoader({
|
||||
globalConfigPath: join(baseDir, "config.json"),
|
||||
agentsDir,
|
||||
});
|
||||
|
||||
const stampWithout = loader.getCacheStamp();
|
||||
const stampWith = loader.getCacheStamp("coder");
|
||||
// Agent file exists, so the stamp differs from the no-agent case.
|
||||
expect(stampWithout).not.toBe(stampWith);
|
||||
} finally {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mtime cache invalidation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("FilePolicyLoader mtime caching", () => {
|
||||
it("returns cached value on second call with unchanged file", () => {
|
||||
const baseDir = makeTempDir();
|
||||
try {
|
||||
const loader = makeLoader(baseDir, {
|
||||
globalConfig: { permission: { "*": "allow" } },
|
||||
});
|
||||
const first = loader.loadGlobalConfig();
|
||||
const second = loader.loadGlobalConfig();
|
||||
// Same reference — cache hit
|
||||
expect(second).toBe(first);
|
||||
} finally {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("reloads when file mtime changes", () => {
|
||||
const baseDir = makeTempDir();
|
||||
try {
|
||||
const globalConfigPath = join(baseDir, "config.json");
|
||||
const agentsDir = join(baseDir, "agents");
|
||||
mkdirSync(agentsDir, { recursive: true });
|
||||
writeFileSync(
|
||||
globalConfigPath,
|
||||
JSON.stringify({ permission: { "*": "allow" } }),
|
||||
);
|
||||
|
||||
const loader = new FilePolicyLoader({ globalConfigPath, agentsDir });
|
||||
const first = loader.loadGlobalConfig();
|
||||
expect(first.permission?.["*"]).toBe("allow");
|
||||
|
||||
// busy-wait for mtime resolution
|
||||
const now = Date.now();
|
||||
while (Date.now() - now < 50) {
|
||||
/* spin */
|
||||
}
|
||||
|
||||
writeFileSync(
|
||||
globalConfigPath,
|
||||
JSON.stringify({ permission: { "*": "deny" } }),
|
||||
);
|
||||
const second = loader.loadGlobalConfig();
|
||||
expect(second.permission?.["*"]).toBe("deny");
|
||||
expect(second).not.toBe(first);
|
||||
} finally {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Agent frontmatter loading
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("FilePolicyLoader agent frontmatter", () => {
|
||||
it("loads permission from agent frontmatter with pattern map", () => {
|
||||
const baseDir = makeTempDir();
|
||||
try {
|
||||
const agentsDir = join(baseDir, "agents");
|
||||
mkdirSync(agentsDir, { recursive: true });
|
||||
writeFileSync(join(baseDir, "config.json"), "{}");
|
||||
writeFileSync(
|
||||
join(agentsDir, "coder.md"),
|
||||
[
|
||||
"---",
|
||||
"permission:",
|
||||
" bash:",
|
||||
' "git *": allow',
|
||||
' "rm *": deny',
|
||||
"---",
|
||||
"# Coder",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
const loader = new FilePolicyLoader({
|
||||
globalConfigPath: join(baseDir, "config.json"),
|
||||
agentsDir,
|
||||
});
|
||||
const config = loader.loadAgentConfig("coder");
|
||||
expect(config.permission).toBeDefined();
|
||||
expect(config.permission?.bash).toEqual({
|
||||
"git *": "allow",
|
||||
"rm *": "deny",
|
||||
});
|
||||
} finally {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("returns empty config for agent file without frontmatter", () => {
|
||||
const baseDir = makeTempDir();
|
||||
try {
|
||||
const agentsDir = join(baseDir, "agents");
|
||||
mkdirSync(agentsDir, { recursive: true });
|
||||
writeFileSync(join(baseDir, "config.json"), "{}");
|
||||
writeFileSync(join(agentsDir, "plain.md"), "# No frontmatter\n");
|
||||
|
||||
const loader = new FilePolicyLoader({
|
||||
globalConfigPath: join(baseDir, "config.json"),
|
||||
agentsDir,
|
||||
});
|
||||
expect(loader.loadAgentConfig("plain")).toEqual({});
|
||||
} finally {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("project agent config loads from projectAgentsDir", () => {
|
||||
const baseDir = makeTempDir();
|
||||
try {
|
||||
const agentsDir = join(baseDir, "agents");
|
||||
const projectAgentsDir = join(baseDir, "project-agents");
|
||||
mkdirSync(agentsDir, { recursive: true });
|
||||
mkdirSync(projectAgentsDir, { recursive: true });
|
||||
writeFileSync(join(baseDir, "config.json"), "{}");
|
||||
writeFileSync(
|
||||
join(projectAgentsDir, "coder.md"),
|
||||
"---\npermission:\n write: allow\n---\n# Coder\n",
|
||||
);
|
||||
|
||||
const loader = new FilePolicyLoader({
|
||||
globalConfigPath: join(baseDir, "config.json"),
|
||||
agentsDir,
|
||||
projectAgentsDir,
|
||||
});
|
||||
const config = loader.loadProjectAgentConfig("coder");
|
||||
expect(config.permission).toEqual({ write: "allow" });
|
||||
} finally {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MCP server name reading
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("FilePolicyLoader.getConfiguredMcpServerNames", () => {
|
||||
it("returns override names when provided", () => {
|
||||
const loader = new FilePolicyLoader({
|
||||
globalConfigPath: "/nonexistent/config.json",
|
||||
agentsDir: "/nonexistent/agents",
|
||||
mcpServerNames: ["exa", "research"],
|
||||
});
|
||||
expect(loader.getConfiguredMcpServerNames()).toEqual(
|
||||
expect.arrayContaining(["exa", "research"]),
|
||||
);
|
||||
});
|
||||
|
||||
it("deduplicates and trims override names", () => {
|
||||
const loader = new FilePolicyLoader({
|
||||
globalConfigPath: "/nonexistent/config.json",
|
||||
agentsDir: "/nonexistent/agents",
|
||||
mcpServerNames: [" exa ", "exa", "research"],
|
||||
});
|
||||
const names = loader.getConfiguredMcpServerNames();
|
||||
expect(names.filter((n) => n === "exa")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("reads server names from mcp.json on disk", () => {
|
||||
const baseDir = makeTempDir();
|
||||
try {
|
||||
const mcpConfigPath = join(baseDir, "mcp.json");
|
||||
writeFileSync(
|
||||
mcpConfigPath,
|
||||
JSON.stringify({ mcpServers: { exa: {}, research: {} } }),
|
||||
);
|
||||
|
||||
const loader = new FilePolicyLoader({
|
||||
globalConfigPath: "/nonexistent/config.json",
|
||||
agentsDir: "/nonexistent/agents",
|
||||
globalMcpConfigPath: mcpConfigPath,
|
||||
});
|
||||
const names = loader.getConfiguredMcpServerNames();
|
||||
expect(names).toEqual(expect.arrayContaining(["exa", "research"]));
|
||||
} finally {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("returns empty array when mcp.json is missing", () => {
|
||||
const loader = new FilePolicyLoader({
|
||||
globalConfigPath: "/nonexistent/config.json",
|
||||
agentsDir: "/nonexistent/agents",
|
||||
globalMcpConfigPath: "/nonexistent/mcp.json",
|
||||
});
|
||||
expect(loader.getConfiguredMcpServerNames()).toEqual([]);
|
||||
});
|
||||
|
||||
it("caches MCP server names across calls", () => {
|
||||
const baseDir = makeTempDir();
|
||||
try {
|
||||
const mcpConfigPath = join(baseDir, "mcp.json");
|
||||
writeFileSync(mcpConfigPath, JSON.stringify({ mcpServers: { exa: {} } }));
|
||||
|
||||
const loader = new FilePolicyLoader({
|
||||
globalConfigPath: "/nonexistent/config.json",
|
||||
agentsDir: "/nonexistent/agents",
|
||||
globalMcpConfigPath: mcpConfigPath,
|
||||
});
|
||||
const first = loader.getConfiguredMcpServerNames();
|
||||
const second = loader.getConfiguredMcpServerNames();
|
||||
// Same reference — cache hit
|
||||
expect(second).toBe(first);
|
||||
} finally {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config issue accumulation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("FilePolicyLoader config issue accumulation", () => {
|
||||
it("accumulates issues from malformed config files", () => {
|
||||
const baseDir = makeTempDir();
|
||||
try {
|
||||
// Write invalid JSON to trigger a parse error issue
|
||||
const globalConfigPath = join(baseDir, "config.json");
|
||||
const agentsDir = join(baseDir, "agents");
|
||||
mkdirSync(agentsDir, { recursive: true });
|
||||
writeFileSync(globalConfigPath, "{ INVALID JSON");
|
||||
|
||||
const loader = new FilePolicyLoader({ globalConfigPath, agentsDir });
|
||||
loader.loadGlobalConfig();
|
||||
const issues = loader.getConfigIssues();
|
||||
expect(issues.length).toBeGreaterThanOrEqual(1);
|
||||
expect(issues[0]).toContain("Failed to read config");
|
||||
} finally {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("does not duplicate issues on repeated loads", () => {
|
||||
const baseDir = makeTempDir();
|
||||
try {
|
||||
const globalConfigPath = join(baseDir, "config.json");
|
||||
const agentsDir = join(baseDir, "agents");
|
||||
mkdirSync(agentsDir, { recursive: true });
|
||||
writeFileSync(globalConfigPath, "{ INVALID JSON");
|
||||
|
||||
const loader = new FilePolicyLoader({ globalConfigPath, agentsDir });
|
||||
loader.loadGlobalConfig();
|
||||
const issuesBefore = loader.getConfigIssues();
|
||||
|
||||
// Bust cache by waiting for mtime change
|
||||
const now = Date.now();
|
||||
while (Date.now() - now < 50) {
|
||||
/* spin */
|
||||
}
|
||||
writeFileSync(globalConfigPath, "{ INVALID JSON");
|
||||
loader.loadGlobalConfig();
|
||||
const issuesAfter = loader.getConfigIssues();
|
||||
expect(issuesAfter.length).toBe(issuesBefore.length);
|
||||
} finally {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user