mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 13:12:52 +00:00
Adds the opt-in bundled GitHub Copilot agent runtime, pinned SDK install path, docs/inventory, SDK/tool/sandbox/auth wiring, and replay/tool-safety fixes.
Verification:
- Local: git diff --check; fnm exec --using 24.15.0 pnpm tsgo:extensions; fnm exec --using 24.15.0 pnpm check:test-types; fnm exec --using 24.15.0 pnpm build.
- Autoreview local: clean for the replay-safety fix; branch autoreview engine returned empty output twice, so local autoreview plus local/Crabbox/CI proof was used.
- Crabbox focused Copilot: run_2c0db9f48a4a, 19 files / 485 tests passed.
- Crabbox additional boundary shard: run_26a246a1aa24, prompt snapshots and plugin SDK boundary/export checks passed.
- Crabbox live Copilot: run_d128e4048b4e, real gpt-4.1 turn with live_echo phase-1-green and clean session-file check.
- GitHub checks: green on head 7cc8657e0d, including Dependency Guard after exact-head approval.
Co-authored-by: Ramraj Balasubramanian <ramrajba@microsoft.com>
525 lines
18 KiB
TypeScript
Executable File
525 lines
18 KiB
TypeScript
Executable File
import { createHash } from "node:crypto";
|
|
import { resolve, join } from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
COPILOT_DEFAULT_AGENT_ID,
|
|
COPILOT_TOKEN_PROFILE_ERROR,
|
|
normalizeCopilotHomePath,
|
|
resolveCopilotAuth,
|
|
sanitizeAgentId,
|
|
tokenFingerprint,
|
|
} from "./auth-bridge.js";
|
|
|
|
function cleanEnv(): NodeJS.ProcessEnv {
|
|
return {} as NodeJS.ProcessEnv;
|
|
}
|
|
|
|
const FAKE_HOME = "/fake-home";
|
|
const fakeHomeDir = () => FAKE_HOME;
|
|
|
|
describe("sanitizeAgentId", () => {
|
|
it("returns default for null/undefined/empty", () => {
|
|
expect(sanitizeAgentId(undefined)).toBe(COPILOT_DEFAULT_AGENT_ID);
|
|
expect(sanitizeAgentId(null)).toBe(COPILOT_DEFAULT_AGENT_ID);
|
|
expect(sanitizeAgentId("")).toBe(COPILOT_DEFAULT_AGENT_ID);
|
|
expect(sanitizeAgentId(" ")).toBe(COPILOT_DEFAULT_AGENT_ID);
|
|
});
|
|
|
|
it("lowercases and accepts alnum + dash + underscore", () => {
|
|
expect(sanitizeAgentId("Agent-1")).toBe("agent-1");
|
|
expect(sanitizeAgentId("my_agent_42")).toBe("my_agent_42");
|
|
expect(sanitizeAgentId("a")).toBe("a");
|
|
});
|
|
|
|
it("rejects path-traversal segments and falls back to default", () => {
|
|
expect(sanitizeAgentId("../etc/passwd")).toBe(COPILOT_DEFAULT_AGENT_ID);
|
|
expect(sanitizeAgentId("../..")).toBe(COPILOT_DEFAULT_AGENT_ID);
|
|
expect(sanitizeAgentId("a/b")).toBe(COPILOT_DEFAULT_AGENT_ID);
|
|
expect(sanitizeAgentId("a\\b")).toBe(COPILOT_DEFAULT_AGENT_ID);
|
|
expect(sanitizeAgentId("a\u0000b")).toBe(COPILOT_DEFAULT_AGENT_ID);
|
|
});
|
|
|
|
it("rejects ids that do not start with alnum", () => {
|
|
expect(sanitizeAgentId("-foo")).toBe(COPILOT_DEFAULT_AGENT_ID);
|
|
expect(sanitizeAgentId("_bar")).toBe(COPILOT_DEFAULT_AGENT_ID);
|
|
});
|
|
|
|
it("rejects ids longer than 64 chars", () => {
|
|
expect(sanitizeAgentId("a".repeat(64))).toBe("a".repeat(64));
|
|
expect(sanitizeAgentId("a".repeat(65))).toBe(COPILOT_DEFAULT_AGENT_ID);
|
|
});
|
|
});
|
|
|
|
describe("tokenFingerprint", () => {
|
|
it("returns a stable sha256-prefixed 12-hex fingerprint", () => {
|
|
const a = tokenFingerprint("hello");
|
|
const b = tokenFingerprint("hello");
|
|
expect(a).toBe(b);
|
|
expect(a.startsWith("sha256:")).toBe(true);
|
|
expect(a.length).toBe("sha256:".length + 12);
|
|
const expected = "sha256:" + createHash("sha256").update("hello").digest("hex").slice(0, 12);
|
|
expect(a).toBe(expected);
|
|
});
|
|
|
|
it("differs across distinct inputs (no collision for common values)", () => {
|
|
expect(tokenFingerprint("alpha")).not.toBe(tokenFingerprint("beta"));
|
|
expect(tokenFingerprint("token-v1")).not.toBe(tokenFingerprint("token-v2"));
|
|
});
|
|
|
|
it("never contains the raw token", () => {
|
|
const token = "ghp_abcdefghijklmnop";
|
|
expect(tokenFingerprint(token).includes(token)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("resolveCopilotAuth - copilotHome resolution", () => {
|
|
it("uses explicit copilotHome when provided", () => {
|
|
const result = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
copilotHome: "/explicit/home",
|
|
env: cleanEnv(),
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(result.copilotHome).toBe(resolve("/explicit/home"));
|
|
});
|
|
|
|
it("falls back to <agentDir>/copilot when copilotHome is absent", () => {
|
|
const result = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
agentDir: "/agent/dir",
|
|
env: cleanEnv(),
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(result.copilotHome).toBe(resolve(join("/agent/dir", "copilot")));
|
|
});
|
|
|
|
it("synthesises per-agent default from homeDir when no path is given", () => {
|
|
const result = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
env: cleanEnv(),
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(result.copilotHome).toBe(
|
|
resolve(join(FAKE_HOME, ".openclaw", "agents", "agent-1", "copilot")),
|
|
);
|
|
});
|
|
|
|
it("respects OPENCLAW_HOME env var as the home root", () => {
|
|
const result = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
env: { OPENCLAW_HOME: "/custom/openclaw" } as NodeJS.ProcessEnv,
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(result.copilotHome).toBe(
|
|
resolve(join("/custom/openclaw", ".openclaw", "agents", "agent-1", "copilot")),
|
|
);
|
|
});
|
|
|
|
it("uses the default agent id when agentId is invalid/missing", () => {
|
|
const result = resolveCopilotAuth({
|
|
agentId: undefined,
|
|
env: cleanEnv(),
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(result.agentId).toBe(COPILOT_DEFAULT_AGENT_ID);
|
|
expect(result.copilotHome).toBe(
|
|
resolve(join(FAKE_HOME, ".openclaw", "agents", COPILOT_DEFAULT_AGENT_ID, "copilot")),
|
|
);
|
|
});
|
|
|
|
it("isolates per-agent copilotHome between agents", () => {
|
|
const a = resolveCopilotAuth({
|
|
agentId: "agent-a",
|
|
env: cleanEnv(),
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
const b = resolveCopilotAuth({
|
|
agentId: "agent-b",
|
|
env: cleanEnv(),
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(a.copilotHome).not.toBe(b.copilotHome);
|
|
expect(a.copilotHome.endsWith(join("agent-a", "copilot"))).toBe(true);
|
|
expect(b.copilotHome.endsWith(join("agent-b", "copilot"))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("resolveCopilotAuth - auth mode resolution", () => {
|
|
it("returns useLoggedInUser when auth.useLoggedInUser=true (ignoring gitHubToken)", () => {
|
|
const result = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
auth: { useLoggedInUser: true, gitHubToken: "should-be-ignored" },
|
|
env: { GITHUB_TOKEN: "env-token" } as NodeJS.ProcessEnv,
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(result.authMode).toBe("useLoggedInUser");
|
|
expect(result.gitHubToken).toBeUndefined();
|
|
expect(result.authProfileId).toBeUndefined();
|
|
expect(result.authProfileVersion).toBeUndefined();
|
|
});
|
|
|
|
it("returns gitHubToken when explicit token + profile id/version provided", () => {
|
|
const result = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
auth: { gitHubToken: "tok", profileId: "p", profileVersion: "v1" },
|
|
env: cleanEnv(),
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(result.authMode).toBe("gitHubToken");
|
|
expect(result.gitHubToken).toBe("tok");
|
|
expect(result.authProfileId).toBe("p");
|
|
expect(result.authProfileVersion).toBe("v1");
|
|
});
|
|
|
|
it("accepts legacy top-level profileVersion + authProfileId fallbacks", () => {
|
|
const result = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
auth: { gitHubToken: "tok" },
|
|
authProfileId: "legacy-p",
|
|
profileVersion: "legacy-v1",
|
|
env: cleanEnv(),
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(result.authMode).toBe("gitHubToken");
|
|
expect(result.authProfileId).toBe("legacy-p");
|
|
expect(result.authProfileVersion).toBe("legacy-v1");
|
|
});
|
|
|
|
it("throws when explicit gitHubToken is given without both profileId + profileVersion", () => {
|
|
expect(() =>
|
|
resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
auth: { gitHubToken: "tok" },
|
|
env: cleanEnv(),
|
|
homeDir: fakeHomeDir,
|
|
}),
|
|
).toThrow(COPILOT_TOKEN_PROFILE_ERROR);
|
|
|
|
expect(() =>
|
|
resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
auth: { gitHubToken: "tok", profileId: "p" },
|
|
env: cleanEnv(),
|
|
homeDir: fakeHomeDir,
|
|
}),
|
|
).toThrow(COPILOT_TOKEN_PROFILE_ERROR);
|
|
|
|
expect(() =>
|
|
resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
auth: { gitHubToken: "tok", profileVersion: "v" },
|
|
env: cleanEnv(),
|
|
homeDir: fakeHomeDir,
|
|
}),
|
|
).toThrow(COPILOT_TOKEN_PROFILE_ERROR);
|
|
});
|
|
|
|
it("defaults to useLoggedInUser when no auth signal at all", () => {
|
|
const result = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
env: cleanEnv(),
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(result.authMode).toBe("useLoggedInUser");
|
|
expect(result.gitHubToken).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("resolveCopilotAuth - contract-resolved auth (resolvedApiKey + authProfileId)", () => {
|
|
it("consumes resolvedApiKey + authProfileId from the EmbeddedRunAttemptParams contract", () => {
|
|
const result = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
resolvedApiKey: "contract-token-xyz",
|
|
authProfileId: "github-copilot:main",
|
|
env: cleanEnv(),
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(result.authMode).toBe("gitHubToken");
|
|
expect(result.gitHubToken).toBe("contract-token-xyz");
|
|
expect(result.authProfileId).toBe("github-copilot:main");
|
|
expect(result.authProfileVersion).toBe(tokenFingerprint("contract-token-xyz"));
|
|
});
|
|
|
|
it("synthesises authProfileId when contract-resolved token has no profile id", () => {
|
|
const result = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
resolvedApiKey: "contract-token-xyz",
|
|
env: cleanEnv(),
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(result.authMode).toBe("gitHubToken");
|
|
expect(result.gitHubToken).toBe("contract-token-xyz");
|
|
expect(result.authProfileId).toBe("pi:resolved");
|
|
expect(result.authProfileVersion).toBe(tokenFingerprint("contract-token-xyz"));
|
|
});
|
|
|
|
it("auth.useLoggedInUser=true takes precedence over contract resolvedApiKey", () => {
|
|
const result = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
auth: { useLoggedInUser: true },
|
|
resolvedApiKey: "should-be-ignored",
|
|
authProfileId: "p",
|
|
env: cleanEnv(),
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(result.authMode).toBe("useLoggedInUser");
|
|
expect(result.gitHubToken).toBeUndefined();
|
|
});
|
|
|
|
it("explicit auth.gitHubToken takes precedence over contract resolvedApiKey", () => {
|
|
const result = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
auth: { gitHubToken: "explicit", profileId: "p", profileVersion: "v1" },
|
|
resolvedApiKey: "contract-should-be-ignored",
|
|
authProfileId: "contract-profile",
|
|
env: cleanEnv(),
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(result.authMode).toBe("gitHubToken");
|
|
expect(result.gitHubToken).toBe("explicit");
|
|
expect(result.authProfileId).toBe("p");
|
|
expect(result.authProfileVersion).toBe("v1");
|
|
});
|
|
|
|
it("contract resolvedApiKey takes precedence over env fallback", () => {
|
|
const result = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
resolvedApiKey: "contract-token",
|
|
authProfileId: "p",
|
|
env: {
|
|
OPENCLAW_GITHUB_TOKEN: "env-should-be-ignored",
|
|
COPILOT_GITHUB_TOKEN: "copilot-env-should-be-ignored",
|
|
GH_TOKEN: "gh-env-should-be-ignored",
|
|
GITHUB_TOKEN: "github-env-should-be-ignored",
|
|
} as NodeJS.ProcessEnv,
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(result.gitHubToken).toBe("contract-token");
|
|
expect(result.authProfileId).toBe("p");
|
|
});
|
|
|
|
it("falls back to env when resolvedApiKey is absent", () => {
|
|
const result = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
authProfileId: "p",
|
|
env: { GITHUB_TOKEN: "env-only" } as NodeJS.ProcessEnv,
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(result.gitHubToken).toBe("env-only");
|
|
expect(result.authProfileId).toBe("env:GITHUB_TOKEN");
|
|
});
|
|
});
|
|
|
|
describe("resolveCopilotAuth - env var fallbacks", () => {
|
|
it("falls back to GITHUB_TOKEN with synthesised profile id + fingerprint", () => {
|
|
const result = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
env: { GITHUB_TOKEN: "env-token-123" } as NodeJS.ProcessEnv,
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(result.authMode).toBe("gitHubToken");
|
|
expect(result.gitHubToken).toBe("env-token-123");
|
|
expect(result.authProfileId).toBe("env:GITHUB_TOKEN");
|
|
expect(result.authProfileVersion).toBe(tokenFingerprint("env-token-123"));
|
|
});
|
|
|
|
it("OPENCLAW_GITHUB_TOKEN takes precedence over GITHUB_TOKEN", () => {
|
|
const result = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
env: {
|
|
OPENCLAW_GITHUB_TOKEN: "openclaw-tok",
|
|
GITHUB_TOKEN: "github-tok",
|
|
} as NodeJS.ProcessEnv,
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(result.gitHubToken).toBe("openclaw-tok");
|
|
expect(result.authProfileId).toBe("env:OPENCLAW_GITHUB_TOKEN");
|
|
expect(result.authProfileVersion).toBe(tokenFingerprint("openclaw-tok"));
|
|
});
|
|
|
|
it("falls back to COPILOT_GITHUB_TOKEN with synthesised profile id + fingerprint", () => {
|
|
const result = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
env: { COPILOT_GITHUB_TOKEN: "copilot-tok-123" } as NodeJS.ProcessEnv,
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(result.authMode).toBe("gitHubToken");
|
|
expect(result.gitHubToken).toBe("copilot-tok-123");
|
|
expect(result.authProfileId).toBe("env:COPILOT_GITHUB_TOKEN");
|
|
expect(result.authProfileVersion).toBe(tokenFingerprint("copilot-tok-123"));
|
|
});
|
|
|
|
it("falls back to GH_TOKEN with synthesised profile id + fingerprint", () => {
|
|
const result = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
env: { GH_TOKEN: "gh-tok-456" } as NodeJS.ProcessEnv,
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(result.authMode).toBe("gitHubToken");
|
|
expect(result.gitHubToken).toBe("gh-tok-456");
|
|
expect(result.authProfileId).toBe("env:GH_TOKEN");
|
|
expect(result.authProfileVersion).toBe(tokenFingerprint("gh-tok-456"));
|
|
});
|
|
|
|
it("OPENCLAW_GITHUB_TOKEN takes precedence over COPILOT_GITHUB_TOKEN, GH_TOKEN and GITHUB_TOKEN", () => {
|
|
const result = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
env: {
|
|
OPENCLAW_GITHUB_TOKEN: "openclaw-tok",
|
|
COPILOT_GITHUB_TOKEN: "copilot-tok",
|
|
GH_TOKEN: "gh-tok",
|
|
GITHUB_TOKEN: "github-tok",
|
|
} as NodeJS.ProcessEnv,
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(result.gitHubToken).toBe("openclaw-tok");
|
|
expect(result.authProfileId).toBe("env:OPENCLAW_GITHUB_TOKEN");
|
|
});
|
|
|
|
it("COPILOT_GITHUB_TOKEN takes precedence over GH_TOKEN and GITHUB_TOKEN", () => {
|
|
const result = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
env: {
|
|
COPILOT_GITHUB_TOKEN: "copilot-tok",
|
|
GH_TOKEN: "gh-tok",
|
|
GITHUB_TOKEN: "github-tok",
|
|
} as NodeJS.ProcessEnv,
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(result.gitHubToken).toBe("copilot-tok");
|
|
expect(result.authProfileId).toBe("env:COPILOT_GITHUB_TOKEN");
|
|
});
|
|
|
|
it("GH_TOKEN takes precedence over GITHUB_TOKEN", () => {
|
|
const result = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
env: {
|
|
GH_TOKEN: "gh-tok",
|
|
GITHUB_TOKEN: "github-tok",
|
|
} as NodeJS.ProcessEnv,
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(result.gitHubToken).toBe("gh-tok");
|
|
expect(result.authProfileId).toBe("env:GH_TOKEN");
|
|
});
|
|
|
|
it("token rotation in env changes the pool fingerprint (cache-busting)", () => {
|
|
const a = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
env: { GITHUB_TOKEN: "v1" } as NodeJS.ProcessEnv,
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
const b = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
env: { GITHUB_TOKEN: "v2" } as NodeJS.ProcessEnv,
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(a.authProfileVersion).not.toBe(b.authProfileVersion);
|
|
});
|
|
|
|
it("explicit auth.useLoggedInUser=true wins over env tokens", () => {
|
|
const result = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
auth: { useLoggedInUser: true },
|
|
env: { OPENCLAW_GITHUB_TOKEN: "env-tok" } as NodeJS.ProcessEnv,
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(result.authMode).toBe("useLoggedInUser");
|
|
});
|
|
|
|
it("explicit auth.gitHubToken wins over env tokens", () => {
|
|
const result = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
auth: { gitHubToken: "explicit", profileId: "p", profileVersion: "v" },
|
|
env: { OPENCLAW_GITHUB_TOKEN: "env-tok" } as NodeJS.ProcessEnv,
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(result.authMode).toBe("gitHubToken");
|
|
expect(result.gitHubToken).toBe("explicit");
|
|
expect(result.authProfileId).toBe("p");
|
|
expect(result.authProfileVersion).toBe("v");
|
|
});
|
|
|
|
it("ignores empty-string env tokens (treated as absent)", () => {
|
|
const result = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
env: {
|
|
GITHUB_TOKEN: "",
|
|
OPENCLAW_GITHUB_TOKEN: "",
|
|
COPILOT_GITHUB_TOKEN: "",
|
|
GH_TOKEN: "",
|
|
} as NodeJS.ProcessEnv,
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(result.authMode).toBe("useLoggedInUser");
|
|
});
|
|
});
|
|
|
|
describe("resolveCopilotAuth - defaults wiring", () => {
|
|
let originalEnv: NodeJS.ProcessEnv;
|
|
|
|
beforeEach(() => {
|
|
originalEnv = process.env;
|
|
process.env = { ...originalEnv };
|
|
delete process.env.GITHUB_TOKEN;
|
|
delete process.env.OPENCLAW_GITHUB_TOKEN;
|
|
delete process.env.COPILOT_GITHUB_TOKEN;
|
|
delete process.env.GH_TOKEN;
|
|
delete process.env.OPENCLAW_HOME;
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.env = originalEnv;
|
|
});
|
|
|
|
it("uses process.env when env is not injected", () => {
|
|
process.env.GITHUB_TOKEN = "from-process-env";
|
|
const result = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
homeDir: fakeHomeDir,
|
|
});
|
|
expect(result.authMode).toBe("gitHubToken");
|
|
expect(result.gitHubToken).toBe("from-process-env");
|
|
});
|
|
|
|
it("uses os.homedir() when homeDir is not injected", () => {
|
|
const result = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
});
|
|
// We don't know the actual home, just that the resolver did not throw and
|
|
// produced an absolute path containing the per-agent suffix.
|
|
expect(result.copilotHome.endsWith(join(".openclaw", "agents", "agent-1", "copilot"))).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("falls back to process.cwd() if homeDir throws", () => {
|
|
const result = resolveCopilotAuth({
|
|
agentId: "agent-1",
|
|
env: cleanEnv(),
|
|
homeDir: () => {
|
|
throw new Error("no home");
|
|
},
|
|
});
|
|
// Should not throw; should produce a path under cwd.
|
|
expect(result.copilotHome.includes(join(".openclaw", "agents", "agent-1", "copilot"))).toBe(
|
|
true,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("normalizeCopilotHomePath", () => {
|
|
it("resolves to absolute and strips trailing separators", () => {
|
|
const normalized = normalizeCopilotHomePath("./foo/bar/");
|
|
expect(normalized).toBe(resolve("./foo/bar"));
|
|
expect(normalized.endsWith("/")).toBe(false);
|
|
expect(normalized.endsWith("\\")).toBe(false);
|
|
});
|
|
|
|
it("is idempotent", () => {
|
|
const once = normalizeCopilotHomePath("/some/path/");
|
|
const twice = normalizeCopilotHomePath(once);
|
|
expect(twice).toBe(once);
|
|
});
|
|
});
|