feat(core,tooling): Windows support — shell selection, install.ps1, win-x64 release package, Windows CI (#79)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
PLATFORM_PLACEHOLDER,
|
||||
PROJECT_DIR_PLACEHOLDER,
|
||||
SESSION_ID_PLACEHOLDER,
|
||||
SHELL_PLACEHOLDER,
|
||||
addModel,
|
||||
setVisionModel,
|
||||
agentsMdPath,
|
||||
@@ -64,7 +65,10 @@ afterEach(async () => {
|
||||
} else {
|
||||
process.env.PENGUIN_HOME = prevHome;
|
||||
}
|
||||
await fs.rm(tmpRoot, { recursive: true, force: true });
|
||||
// Retries: when a test times out, vitest runs this cleanup while the test's un-cancelled
|
||||
// init may still be writing files, so an immediate recursive rm can hit ENOTEMPTY on
|
||||
// Windows (fs.rm retries ENOTEMPTY/EBUSY/EPERM); a no-op when removal succeeds first try.
|
||||
await fs.rm(tmpRoot, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 });
|
||||
});
|
||||
|
||||
async function exists(p: string): Promise<boolean> {
|
||||
@@ -83,87 +87,96 @@ describe("paths / resolveRoot", () => {
|
||||
});
|
||||
|
||||
describe("loadOrInitAgentState", () => {
|
||||
it("initializes an empty agent directory with the full state layout", async () => {
|
||||
const state = await loadOrInitAgentState();
|
||||
expect(state.root).toBe(tmpRoot);
|
||||
expect(state.projectId).toBe(DEFAULT_PROJECT_ID);
|
||||
expect(state.agentId).toBe(DEFAULT_AGENT_ID);
|
||||
// Timeout: initialization writes the full layout — 15 library skills plus the example
|
||||
// benchmark, dozens of small files — and this first init test also pays the cold-I/O cost
|
||||
// (first-touch reads of the skills package, Defender scans) on Windows runners, where a
|
||||
// slow-disk moment has pushed it past the 5s default. Purely a failure deadline: passing
|
||||
// runs stay as fast as before on every platform.
|
||||
it(
|
||||
"initializes an empty agent directory with the full state layout",
|
||||
{ timeout: 20_000 },
|
||||
async () => {
|
||||
const state = await loadOrInitAgentState();
|
||||
expect(state.root).toBe(tmpRoot);
|
||||
expect(state.projectId).toBe(DEFAULT_PROJECT_ID);
|
||||
expect(state.agentId).toBe(DEFAULT_AGENT_ID);
|
||||
|
||||
const root = tmpRoot;
|
||||
expect(await exists(systemConfigPath(root, DEFAULT_PROJECT_ID, DEFAULT_AGENT_ID))).toBe(true);
|
||||
expect(await exists(agentsMdPath(root, DEFAULT_PROJECT_ID, DEFAULT_AGENT_ID))).toBe(true);
|
||||
expect(await exists(toolsDir(root, DEFAULT_PROJECT_ID, DEFAULT_AGENT_ID))).toBe(true);
|
||||
expect(await exists(memoryDir(root, DEFAULT_PROJECT_ID, DEFAULT_AGENT_ID))).toBe(true);
|
||||
expect(await exists(skillsDir(root, DEFAULT_PROJECT_ID, DEFAULT_AGENT_ID))).toBe(true);
|
||||
// The scratchpad/ directory alongside agent_state (model temp files get a subdirectory per Session id).
|
||||
expect(await exists(scratchpadDir(root, DEFAULT_PROJECT_ID, DEFAULT_AGENT_ID))).toBe(true);
|
||||
const root = tmpRoot;
|
||||
expect(await exists(systemConfigPath(root, DEFAULT_PROJECT_ID, DEFAULT_AGENT_ID))).toBe(true);
|
||||
expect(await exists(agentsMdPath(root, DEFAULT_PROJECT_ID, DEFAULT_AGENT_ID))).toBe(true);
|
||||
expect(await exists(toolsDir(root, DEFAULT_PROJECT_ID, DEFAULT_AGENT_ID))).toBe(true);
|
||||
expect(await exists(memoryDir(root, DEFAULT_PROJECT_ID, DEFAULT_AGENT_ID))).toBe(true);
|
||||
expect(await exists(skillsDir(root, DEFAULT_PROJECT_ID, DEFAULT_AGENT_ID))).toBe(true);
|
||||
// The scratchpad/ directory alongside agent_state (model temp files get a subdirectory per Session id).
|
||||
expect(await exists(scratchpadDir(root, DEFAULT_PROJECT_ID, DEFAULT_AGENT_ID))).toBe(true);
|
||||
|
||||
expect(state.stateDir).toBe(agentStateDir(root, DEFAULT_PROJECT_ID, DEFAULT_AGENT_ID));
|
||||
expect(state.stateDir).toBe(agentStateDir(root, DEFAULT_PROJECT_ID, DEFAULT_AGENT_ID));
|
||||
|
||||
// The default system Prompt states the Agent's identity, without repeating tool details
|
||||
// already in the tool schema (Suggested workflows only points to the run_subagent
|
||||
// delegation entry point).
|
||||
expect(state.systemConfig.system_prompt).toContain("PenguinHarness");
|
||||
expect(state.systemConfig.system_prompt).not.toContain("exec_command");
|
||||
// Suggested workflows absorbs Subagent delegation and task conventions (self-reported
|
||||
// identity as a soft convention, parallelism, file exchange).
|
||||
expect(state.systemConfig.system_prompt).toContain("# Suggested workflows");
|
||||
expect(state.systemConfig.system_prompt).toContain("run_subagent");
|
||||
expect(state.systemConfig.system_prompt).toContain("Caller agent");
|
||||
// The default AGENTS.md is empty: it carries no preset guidance.
|
||||
expect(state.agentsMd).toBe("");
|
||||
expect(state.systemConfig.system_prompt).toContain(AGENTS_MD_PLACEHOLDER);
|
||||
expect(state.systemConfig.system_prompt).toContain(SESSION_ID_PLACEHOLDER);
|
||||
expect(state.systemConfig.system_prompt).toContain(CWD_PLACEHOLDER);
|
||||
expect(state.systemConfig.system_prompt).toContain(PLATFORM_PLACEHOLDER);
|
||||
expect(state.systemConfig.system_prompt).toContain(OS_VERSION_PLACEHOLDER);
|
||||
expect(state.systemConfig.system_prompt).toContain(DATE_PLACEHOLDER);
|
||||
// AGENTS.md and the Environment injection sit at the end of the template, with AGENTS.md
|
||||
// before Environment; the [developer_instructions] wrapper text is written directly into
|
||||
// the template (the Prompt is transparent about the config).
|
||||
expect(state.systemConfig.system_prompt).toContain("[developer_instructions]");
|
||||
expect(state.systemConfig.system_prompt).toContain("[/developer_instructions]");
|
||||
// The default template explains the semantics of system-synthesized markers to the model,
|
||||
// and recommends preferring tool use.
|
||||
expect(state.systemConfig.system_prompt).toContain("[turn_aborted]");
|
||||
expect(state.systemConfig.system_prompt).toContain("[turn_retried]");
|
||||
expect(state.systemConfig.system_prompt).toContain("[context_summary]");
|
||||
expect(state.systemConfig.system_prompt).toContain("[user_steering]");
|
||||
expect(state.systemConfig.system_prompt).toContain("# Tool use");
|
||||
// Privacy hardening: explicitly forbids reading .project_config.toml (the sole config file,
|
||||
// which holds API keys) and each Agent's .vault.toml, and states that config can only be
|
||||
// changed via the CLI (penguin config ...).
|
||||
expect(state.systemConfig.system_prompt).toContain("Never read");
|
||||
expect(state.systemConfig.system_prompt).toContain(".project_config.toml");
|
||||
expect(state.systemConfig.system_prompt).toContain("agent_state/.vault.toml");
|
||||
expect(state.systemConfig.system_prompt).toContain("CLI-only");
|
||||
expect(state.systemConfig.system_prompt).toContain("penguin config");
|
||||
expect(state.systemConfig.system_prompt).not.toContain(".credentials.toml");
|
||||
expect(state.systemConfig.system_prompt.indexOf(AGENTS_MD_PLACEHOLDER)).toBeLessThan(
|
||||
state.systemConfig.system_prompt.indexOf("# Environment"),
|
||||
);
|
||||
// The # Vault and # Skills body sections plus their placeholders: the default template
|
||||
// places them after [/developer_instructions] and before # Environment, in the order
|
||||
// Vault -> Skills (the statement text is part of the template body, kept even with no
|
||||
// keys/skills).
|
||||
const tpl = state.systemConfig.system_prompt;
|
||||
expect(tpl).toContain("# Vault");
|
||||
expect(tpl).toContain(VAULT_KEYS_PLACEHOLDER);
|
||||
expect(tpl).toContain("# Skills");
|
||||
expect(tpl).toContain(SKILL_METADATA_PLACEHOLDER);
|
||||
expect(tpl).toContain("[use_skills]");
|
||||
expect(tpl.indexOf("[/developer_instructions]")).toBeLessThan(tpl.indexOf("# Vault"));
|
||||
expect(tpl.indexOf("# Vault")).toBeLessThan(tpl.indexOf(VAULT_KEYS_PLACEHOLDER));
|
||||
expect(tpl.indexOf(VAULT_KEYS_PLACEHOLDER)).toBeLessThan(tpl.indexOf("# Skills"));
|
||||
expect(tpl.indexOf("# Skills")).toBeLessThan(tpl.indexOf(SKILL_METADATA_PLACEHOLDER));
|
||||
expect(tpl.indexOf(SKILL_METADATA_PLACEHOLDER)).toBeLessThan(tpl.indexOf("# Environment"));
|
||||
expect(state.systemConfig.model?.max_tokens).toBe(32000);
|
||||
expect(state.systemConfig.model?.thinking_level).toBe("medium");
|
||||
expect(state.systemConfig.model?.timeoutMs).toBe(120000);
|
||||
expect(state.systemConfig.tools?.mcpServers).toEqual([]);
|
||||
expect(Object.hasOwn(state.systemConfig, "description")).toBe(false);
|
||||
expect(Object.hasOwn(state.systemConfig, "subagents")).toBe(false);
|
||||
});
|
||||
// The default system Prompt states the Agent's identity, without repeating tool details
|
||||
// already in the tool schema (Suggested workflows only points to the run_subagent
|
||||
// delegation entry point).
|
||||
expect(state.systemConfig.system_prompt).toContain("PenguinHarness");
|
||||
expect(state.systemConfig.system_prompt).not.toContain("exec_command");
|
||||
// Suggested workflows absorbs Subagent delegation and task conventions (self-reported
|
||||
// identity as a soft convention, parallelism, file exchange).
|
||||
expect(state.systemConfig.system_prompt).toContain("# Suggested workflows");
|
||||
expect(state.systemConfig.system_prompt).toContain("run_subagent");
|
||||
expect(state.systemConfig.system_prompt).toContain("Caller agent");
|
||||
// The default AGENTS.md is empty: it carries no preset guidance.
|
||||
expect(state.agentsMd).toBe("");
|
||||
expect(state.systemConfig.system_prompt).toContain(AGENTS_MD_PLACEHOLDER);
|
||||
expect(state.systemConfig.system_prompt).toContain(SESSION_ID_PLACEHOLDER);
|
||||
expect(state.systemConfig.system_prompt).toContain(CWD_PLACEHOLDER);
|
||||
expect(state.systemConfig.system_prompt).toContain(PLATFORM_PLACEHOLDER);
|
||||
expect(state.systemConfig.system_prompt).toContain(OS_VERSION_PLACEHOLDER);
|
||||
expect(state.systemConfig.system_prompt).toContain(DATE_PLACEHOLDER);
|
||||
// AGENTS.md and the Environment injection sit at the end of the template, with AGENTS.md
|
||||
// before Environment; the [developer_instructions] wrapper text is written directly into
|
||||
// the template (the Prompt is transparent about the config).
|
||||
expect(state.systemConfig.system_prompt).toContain("[developer_instructions]");
|
||||
expect(state.systemConfig.system_prompt).toContain("[/developer_instructions]");
|
||||
// The default template explains the semantics of system-synthesized markers to the model,
|
||||
// and recommends preferring tool use.
|
||||
expect(state.systemConfig.system_prompt).toContain("[turn_aborted]");
|
||||
expect(state.systemConfig.system_prompt).toContain("[turn_retried]");
|
||||
expect(state.systemConfig.system_prompt).toContain("[context_summary]");
|
||||
expect(state.systemConfig.system_prompt).toContain("[user_steering]");
|
||||
expect(state.systemConfig.system_prompt).toContain("# Tool use");
|
||||
// Privacy hardening: explicitly forbids reading .project_config.toml (the sole config file,
|
||||
// which holds API keys) and each Agent's .vault.toml, and states that config can only be
|
||||
// changed via the CLI (penguin config ...).
|
||||
expect(state.systemConfig.system_prompt).toContain("Never read");
|
||||
expect(state.systemConfig.system_prompt).toContain(".project_config.toml");
|
||||
expect(state.systemConfig.system_prompt).toContain("agent_state/.vault.toml");
|
||||
expect(state.systemConfig.system_prompt).toContain("CLI-only");
|
||||
expect(state.systemConfig.system_prompt).toContain("penguin config");
|
||||
expect(state.systemConfig.system_prompt).not.toContain(".credentials.toml");
|
||||
expect(state.systemConfig.system_prompt.indexOf(AGENTS_MD_PLACEHOLDER)).toBeLessThan(
|
||||
state.systemConfig.system_prompt.indexOf("# Environment"),
|
||||
);
|
||||
// The # Vault and # Skills body sections plus their placeholders: the default template
|
||||
// places them after [/developer_instructions] and before # Environment, in the order
|
||||
// Vault -> Skills (the statement text is part of the template body, kept even with no
|
||||
// keys/skills).
|
||||
const tpl = state.systemConfig.system_prompt;
|
||||
expect(tpl).toContain("# Vault");
|
||||
expect(tpl).toContain(VAULT_KEYS_PLACEHOLDER);
|
||||
expect(tpl).toContain("# Skills");
|
||||
expect(tpl).toContain(SKILL_METADATA_PLACEHOLDER);
|
||||
expect(tpl).toContain("[use_skills]");
|
||||
expect(tpl.indexOf("[/developer_instructions]")).toBeLessThan(tpl.indexOf("# Vault"));
|
||||
expect(tpl.indexOf("# Vault")).toBeLessThan(tpl.indexOf(VAULT_KEYS_PLACEHOLDER));
|
||||
expect(tpl.indexOf(VAULT_KEYS_PLACEHOLDER)).toBeLessThan(tpl.indexOf("# Skills"));
|
||||
expect(tpl.indexOf("# Skills")).toBeLessThan(tpl.indexOf(SKILL_METADATA_PLACEHOLDER));
|
||||
expect(tpl.indexOf(SKILL_METADATA_PLACEHOLDER)).toBeLessThan(tpl.indexOf("# Environment"));
|
||||
expect(state.systemConfig.model?.max_tokens).toBe(32000);
|
||||
expect(state.systemConfig.model?.thinking_level).toBe("medium");
|
||||
expect(state.systemConfig.model?.timeoutMs).toBe(120000);
|
||||
expect(state.systemConfig.tools?.mcpServers).toEqual([]);
|
||||
expect(Object.hasOwn(state.systemConfig, "description")).toBe(false);
|
||||
expect(Object.hasOwn(state.systemConfig, "subagents")).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it("loads an existing agent directory and returns the same system prompt", async () => {
|
||||
const first = await loadOrInitAgentState();
|
||||
@@ -500,6 +513,7 @@ describe("assembleSystemPrompt", () => {
|
||||
`pdir=${PROJECT_DIR_PLACEHOLDER}`,
|
||||
`platform=${PLATFORM_PLACEHOLDER}`,
|
||||
`os=${OS_VERSION_PLACEHOLDER}`,
|
||||
`shell=${SHELL_PLACEHOLDER}`,
|
||||
`date=${DATE_PLACEHOLDER}`,
|
||||
"middle",
|
||||
AGENTS_MD_PLACEHOLDER,
|
||||
@@ -518,6 +532,7 @@ describe("assembleSystemPrompt", () => {
|
||||
modelId: "deepseek-v4-pro",
|
||||
platform: "darwin",
|
||||
osVersion: "Darwin 25.0.0",
|
||||
shell: "zsh",
|
||||
date: "2026-06-30",
|
||||
});
|
||||
expect(prompt).toBe(
|
||||
@@ -529,6 +544,7 @@ describe("assembleSystemPrompt", () => {
|
||||
"pdir=/tmp/proj",
|
||||
"platform=darwin",
|
||||
"os=Darwin 25.0.0",
|
||||
"shell=zsh",
|
||||
"date=2026-06-30",
|
||||
"middle",
|
||||
"# Agent Rules\nFollow local rules.",
|
||||
@@ -572,6 +588,7 @@ describe("assembleSystemPrompt", () => {
|
||||
modelId: "deepseek-v4-pro",
|
||||
platform: "darwin",
|
||||
osVersion: "Darwin 25.0.0",
|
||||
shell: "zsh",
|
||||
date: "2026-06-30",
|
||||
});
|
||||
expect(prompt).toBe("base prompt");
|
||||
@@ -657,9 +674,11 @@ describe("assembleSystemPrompt", () => {
|
||||
expect(prompt).toContain("Model ID: gpt-5.5");
|
||||
expect(prompt).toContain("Platform:");
|
||||
expect(prompt).toContain("OS Version:");
|
||||
expect(prompt).toContain("Shell:");
|
||||
expect(prompt).toContain("Date: 2026-06-30");
|
||||
expect(prompt.indexOf("Platform:")).toBeLessThan(prompt.indexOf("OS Version:"));
|
||||
expect(prompt.indexOf("OS Version:")).toBeLessThan(prompt.indexOf("Date:"));
|
||||
expect(prompt.indexOf("OS Version:")).toBeLessThan(prompt.indexOf("Shell:"));
|
||||
expect(prompt.indexOf("Shell:")).toBeLessThan(prompt.indexOf("Date:"));
|
||||
expect(prompt.indexOf("Date:")).toBeLessThan(prompt.indexOf("App Data Dir:"));
|
||||
expect(prompt.indexOf("App Data Dir:")).toBeLessThan(prompt.indexOf("Agent ID:"));
|
||||
expect(prompt.indexOf("Agent ID:")).toBeLessThan(prompt.indexOf("CWD:"));
|
||||
@@ -667,6 +686,114 @@ describe("assembleSystemPrompt", () => {
|
||||
expect(prompt.indexOf("Provider:")).toBeLessThan(prompt.indexOf("Model ID:"));
|
||||
expect(prompt.indexOf("Model ID:")).toBeLessThan(prompt.indexOf("Session ID:"));
|
||||
});
|
||||
|
||||
// The win32 Shell-line fallback for pre-{{SHELL}} templates (system_config.yaml is baked at
|
||||
// Agent creation and never auto-upgraded). Removable together with `withShellLineFallback`
|
||||
// once pre-{{SHELL}} Agent configs are no longer expected in the wild.
|
||||
describe("Shell line fallback for templates without {{SHELL}}", () => {
|
||||
const stateWithPrompt = (system_prompt: string) => ({
|
||||
root: tmpRoot,
|
||||
projectId: DEFAULT_PROJECT_ID,
|
||||
agentId: DEFAULT_AGENT_ID,
|
||||
stateDir: agentStateDir(tmpRoot, DEFAULT_PROJECT_ID, DEFAULT_AGENT_ID),
|
||||
systemConfig: { system_prompt },
|
||||
agentsMd: "",
|
||||
});
|
||||
const envFor = (platform: string) => ({
|
||||
sessionId: "session-1",
|
||||
cwd: "C:\\ws",
|
||||
agentId: "agent-x",
|
||||
projectDir: "C:\\proj",
|
||||
provider: "deepseek",
|
||||
modelId: "deepseek-v4-pro",
|
||||
platform,
|
||||
osVersion: "Windows 11 Pro 10.0.26100",
|
||||
shell: "pwsh",
|
||||
date: "2026-07-27",
|
||||
});
|
||||
// A pre-{{SHELL}} default-template Environment section (Platform/OS Version/Date, no Shell).
|
||||
const preShellTemplate = [
|
||||
"intro",
|
||||
"# Environment",
|
||||
`- Platform: ${PLATFORM_PLACEHOLDER}`,
|
||||
`- OS Version: ${OS_VERSION_PLACEHOLDER}`,
|
||||
`- Date: ${DATE_PLACEHOLDER}`,
|
||||
"",
|
||||
"# Tail section",
|
||||
"tail",
|
||||
].join("\n");
|
||||
|
||||
it("injects the line exactly once into the Environment section on win32", () => {
|
||||
const prompt = assembleSystemPrompt(stateWithPrompt(preShellTemplate), envFor("win32"));
|
||||
expect(prompt).toBe(
|
||||
[
|
||||
"intro",
|
||||
"# Environment",
|
||||
"- Shell: pwsh",
|
||||
"- Platform: win32",
|
||||
"- OS Version: Windows 11 Pro 10.0.26100",
|
||||
"- Date: 2026-07-27",
|
||||
"",
|
||||
"# Tail section",
|
||||
"tail",
|
||||
].join("\n"),
|
||||
);
|
||||
expect(prompt.split("- Shell: pwsh").length - 1).toBe(1);
|
||||
});
|
||||
|
||||
it("keeps POSIX output byte-identical (no injected line)", () => {
|
||||
for (const platform of ["linux", "darwin"]) {
|
||||
const prompt = assembleSystemPrompt(stateWithPrompt(preShellTemplate), {
|
||||
...envFor(platform),
|
||||
shell: "bash",
|
||||
osVersion: "Linux 6.1.0",
|
||||
});
|
||||
expect(prompt).toBe(
|
||||
[
|
||||
"intro",
|
||||
"# Environment",
|
||||
`- Platform: ${platform}`,
|
||||
"- OS Version: Linux 6.1.0",
|
||||
"- Date: 2026-07-27",
|
||||
"",
|
||||
"# Tail section",
|
||||
"tail",
|
||||
].join("\n"),
|
||||
);
|
||||
expect(prompt).not.toContain("Shell:");
|
||||
}
|
||||
});
|
||||
|
||||
it("does not duplicate the line when the template has {{SHELL}}", () => {
|
||||
const template = [
|
||||
"# Environment",
|
||||
`- Platform: ${PLATFORM_PLACEHOLDER}`,
|
||||
`- Shell: ${SHELL_PLACEHOLDER}`,
|
||||
].join("\n");
|
||||
const prompt = assembleSystemPrompt(stateWithPrompt(template), envFor("win32"));
|
||||
expect(prompt).toBe(["# Environment", "- Platform: win32", "- Shell: pwsh"].join("\n"));
|
||||
expect(prompt.split("- Shell:").length - 1).toBe(1);
|
||||
});
|
||||
|
||||
it("does not duplicate a hardcoded line and appends a minimal line without an Environment section", () => {
|
||||
// A custom template that hardcodes the exact line: left untouched (idempotent).
|
||||
const hardcoded = assembleSystemPrompt(
|
||||
stateWithPrompt("base prompt\n- Shell: pwsh"),
|
||||
envFor("win32"),
|
||||
);
|
||||
expect(hardcoded).toBe("base prompt\n- Shell: pwsh");
|
||||
// A hardcoded line with a different value is a deliberate template choice:
|
||||
// never add a second, contradicting Shell line.
|
||||
const pinned = assembleSystemPrompt(
|
||||
stateWithPrompt("base prompt\n- Shell: bash"),
|
||||
envFor("win32"),
|
||||
);
|
||||
expect(pinned).toBe("base prompt\n- Shell: bash");
|
||||
// No Environment section at all: the line is appended at the end.
|
||||
const appended = assembleSystemPrompt(stateWithPrompt("base prompt"), envFor("win32"));
|
||||
expect(appended).toBe("base prompt\n- Shell: pwsh");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("resetSystemConfigToDefaults", () => {
|
||||
@@ -1174,28 +1301,35 @@ describe("single hidden config file (.project_config.toml, credentials inlined)"
|
||||
// The sole config file is hidden (not shown by ls by default) and has 0600 permission
|
||||
// (owner read/write only).
|
||||
expect(path.basename(file)).toBe(".project_config.toml");
|
||||
expect((await fs.stat(file)).mode & 0o777).toBe(0o600);
|
||||
// POSIX-only: Windows has no owner-only mode bits (chmod maps to the read-only attribute).
|
||||
if (process.platform !== "win32") {
|
||||
expect((await fs.stat(file)).mode & 0o777).toBe(0o600);
|
||||
}
|
||||
expect(await fs.readFile(file, "utf8")).toContain("sk-split-1");
|
||||
// The old two-file layout is no longer produced.
|
||||
expect(await exists(path.join(tmpRoot, DEFAULT_PROJECT_ID, "project_config.toml"))).toBe(false);
|
||||
expect(await exists(path.join(tmpRoot, DEFAULT_PROJECT_ID, ".credentials.toml"))).toBe(false);
|
||||
});
|
||||
|
||||
it("chmod converges an existing file back to 0600 on save", async () => {
|
||||
await addModel(tmpRoot, DEFAULT_PROJECT_ID, {
|
||||
provider: "custom",
|
||||
model_id: "m-perm",
|
||||
api_key: "sk-1",
|
||||
});
|
||||
const file = projectConfigPath(tmpRoot, DEFAULT_PROJECT_ID);
|
||||
await fs.chmod(file, 0o644);
|
||||
await addModel(tmpRoot, DEFAULT_PROJECT_ID, {
|
||||
provider: "custom",
|
||||
model_id: "m-perm",
|
||||
api_key: "sk-2",
|
||||
});
|
||||
expect((await fs.stat(file)).mode & 0o777).toBe(0o600);
|
||||
});
|
||||
// POSIX-only: Windows has no owner-only mode bits to converge.
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"chmod converges an existing file back to 0600 on save",
|
||||
async () => {
|
||||
await addModel(tmpRoot, DEFAULT_PROJECT_ID, {
|
||||
provider: "custom",
|
||||
model_id: "m-perm",
|
||||
api_key: "sk-1",
|
||||
});
|
||||
const file = projectConfigPath(tmpRoot, DEFAULT_PROJECT_ID);
|
||||
await fs.chmod(file, 0o644);
|
||||
await addModel(tmpRoot, DEFAULT_PROJECT_ID, {
|
||||
provider: "custom",
|
||||
model_id: "m-perm",
|
||||
api_key: "sk-2",
|
||||
});
|
||||
expect((await fs.stat(file)).mode & 0o777).toBe(0o600);
|
||||
},
|
||||
);
|
||||
|
||||
it("writes provider and model_id as separate fields; refs are TOML inline tables", async () => {
|
||||
await addModel(
|
||||
@@ -1238,7 +1372,10 @@ describe("agent vault (agent_state/.vault.toml)", () => {
|
||||
// with 0600 permission (owner read/write only).
|
||||
const file = agentVaultPath(tmpRoot, DEFAULT_PROJECT_ID, DEFAULT_AGENT_ID);
|
||||
expect(path.basename(file)).toBe(".vault.toml");
|
||||
expect((await fs.stat(file)).mode & 0o777).toBe(0o600);
|
||||
// POSIX-only: Windows has no owner-only mode bits (chmod maps to the read-only attribute).
|
||||
if (process.platform !== "win32") {
|
||||
expect((await fs.stat(file)).mode & 0o777).toBe(0o600);
|
||||
}
|
||||
const raw = await fs.readFile(file, "utf8");
|
||||
expect(raw).toContain("sk-secret-2");
|
||||
// The Project config no longer carries the vault.
|
||||
|
||||
Reference in New Issue
Block a user