mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-14 18:01:20 +08:00
30fcede44e
Follow-up to #1328. That PR loosened the Cursor TERM_PROGRAM check from exact `=== "cursor"` to `?.toLowerCase() === "cursor"` "for parity with Windsurf" — but the parity is false. Windsurf is matched case-insensitively because its sources genuinely disagree on casing ("windsurf" vs "Windsurf"); Cursor consistently emits lowercase "cursor", so nothing justified loosening an existing, working, exact-match rule. Per review feedback on #1328 (Magi/Hermes), revert Cursor to exact match and drop the TERM_PROGRAM=Cursor test. Windsurf stays case-insensitive (sourced); its comment now documents the asymmetry as intentional. No functional change — Cursor always emitted lowercase, so detection is unchanged; this just removes an unsourced false-positive surface. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
440 lines
17 KiB
TypeScript
440 lines
17 KiB
TypeScript
import { describe, expect, it, beforeEach, afterEach, vi } from "vitest";
|
|
|
|
// agent_runtime.ts reads node:os via release/platform and node:fs for the
|
|
// /proc files. detectAgentRuntime is exercised by mutating process.env;
|
|
// detectSandboxRuntime is exercised through a small set of node:os mocks.
|
|
|
|
const VENDOR_ENV_KEYS = [
|
|
"CLAUDECODE",
|
|
"CLAUDE_CODE_ENTRYPOINT",
|
|
"CODEX_THREAD_ID",
|
|
"CODEX_CI",
|
|
"CODEX_SANDBOX_NETWORK_DISABLED",
|
|
"TERM_PROGRAM",
|
|
"GITHUB_ACTIONS",
|
|
"COPILOT_AGENT_ID",
|
|
"RUNNER_NAME",
|
|
"REPL_ID",
|
|
"REPLIT_USER",
|
|
"HERMES_QUIET",
|
|
"OPENCLAW_STATE_DIR",
|
|
"OPENCLAW_CONFIG_PATH",
|
|
"PI_CODING_AGENT",
|
|
"CLINE_ACTIVE",
|
|
"GEMINI_CLI",
|
|
"CRUSH",
|
|
] as const;
|
|
|
|
function stripVendorEnv(): void {
|
|
for (const key of VENDOR_ENV_KEYS) delete process.env[key];
|
|
}
|
|
|
|
describe("detectAgentRuntime — base behavior", () => {
|
|
const savedEnv = { ...process.env };
|
|
beforeEach(stripVendorEnv);
|
|
afterEach(() => {
|
|
process.env = { ...savedEnv };
|
|
});
|
|
|
|
it("returns null on a plain shell with no agent markers", async () => {
|
|
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
|
expect(detectAgentRuntime()).toBeNull();
|
|
});
|
|
|
|
it("first matching vendor wins (rule order)", async () => {
|
|
// Claude Code marker set alongside a Codex marker — Claude Code is the
|
|
// first rule, so it wins.
|
|
process.env["CLAUDECODE"] = "1";
|
|
process.env["CODEX_THREAD_ID"] = "thread-1";
|
|
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
|
expect(detectAgentRuntime()).toBe("claude_code");
|
|
});
|
|
|
|
it("never reads env-var values — even API-key-shaped values stay unread", async () => {
|
|
process.env["CODEX_THREAD_ID"] = "thread-1";
|
|
process.env["CODEX_API_KEY"] = "sk-supersecret-DO-NOT-LEAK";
|
|
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
|
const result = detectAgentRuntime();
|
|
expect(result).toBe("codex");
|
|
expect(typeof result).toBe("string");
|
|
expect((result ?? "").includes("supersecret")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("detectAgentRuntime — Claude Code", () => {
|
|
const savedEnv = { ...process.env };
|
|
beforeEach(stripVendorEnv);
|
|
afterEach(() => {
|
|
process.env = { ...savedEnv };
|
|
});
|
|
|
|
it("detects via CLAUDECODE=1", async () => {
|
|
process.env["CLAUDECODE"] = "1";
|
|
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
|
expect(detectAgentRuntime()).toBe("claude_code");
|
|
});
|
|
|
|
it("detects via CLAUDE_CODE_ENTRYPOINT", async () => {
|
|
process.env["CLAUDE_CODE_ENTRYPOINT"] = "cli";
|
|
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
|
expect(detectAgentRuntime()).toBe("claude_code");
|
|
});
|
|
});
|
|
|
|
describe("detectAgentRuntime — OpenAI Codex", () => {
|
|
const savedEnv = { ...process.env };
|
|
beforeEach(stripVendorEnv);
|
|
afterEach(() => {
|
|
process.env = { ...savedEnv };
|
|
});
|
|
|
|
it("detects via CODEX_THREAD_ID (set on every spawned shell command)", async () => {
|
|
process.env["CODEX_THREAD_ID"] = "01234567-89ab-cdef-0123-456789abcdef";
|
|
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
|
expect(detectAgentRuntime()).toBe("codex");
|
|
});
|
|
|
|
it("detects via CODEX_CI (hardcoded in UNIFIED_EXEC_ENV)", async () => {
|
|
process.env["CODEX_CI"] = "1";
|
|
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
|
expect(detectAgentRuntime()).toBe("codex");
|
|
});
|
|
|
|
it("detects via CODEX_SANDBOX_NETWORK_DISABLED (default-on)", async () => {
|
|
process.env["CODEX_SANDBOX_NETWORK_DISABLED"] = "1";
|
|
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
|
expect(detectAgentRuntime()).toBe("codex");
|
|
});
|
|
});
|
|
|
|
describe("detectAgentRuntime — Cursor / Copilot / cohort", () => {
|
|
const savedEnv = { ...process.env };
|
|
beforeEach(stripVendorEnv);
|
|
afterEach(() => {
|
|
process.env = { ...savedEnv };
|
|
});
|
|
|
|
it("detects Cursor via TERM_PROGRAM=cursor", async () => {
|
|
process.env["TERM_PROGRAM"] = "cursor";
|
|
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
|
expect(detectAgentRuntime()).toBe("cursor");
|
|
});
|
|
|
|
it("detects Copilot Coding Agent via GITHUB_ACTIONS + COPILOT_AGENT_ID", async () => {
|
|
process.env["GITHUB_ACTIONS"] = "true";
|
|
process.env["COPILOT_AGENT_ID"] = "abc123";
|
|
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
|
expect(detectAgentRuntime()).toBe("copilot_agent");
|
|
});
|
|
|
|
it("does NOT flag generic GitHub Actions as copilot_agent", async () => {
|
|
process.env["GITHUB_ACTIONS"] = "true";
|
|
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
|
expect(detectAgentRuntime()).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("detectAgentRuntime — Replit / Hermes / openclaw / Pi", () => {
|
|
const savedEnv = { ...process.env };
|
|
beforeEach(stripVendorEnv);
|
|
afterEach(() => {
|
|
process.env = { ...savedEnv };
|
|
});
|
|
|
|
it("detects Replit via REPL_ID", async () => {
|
|
process.env["REPL_ID"] = "repl-1";
|
|
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
|
expect(detectAgentRuntime()).toBe("replit");
|
|
});
|
|
|
|
it("detects Hermes via HERMES_QUIET (set unconditionally by cli.py:50)", async () => {
|
|
process.env["HERMES_QUIET"] = "1";
|
|
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
|
expect(detectAgentRuntime()).toBe("hermes");
|
|
});
|
|
|
|
it("detects openclaw via inherited OPENCLAW_STATE_DIR", async () => {
|
|
process.env["OPENCLAW_STATE_DIR"] = "/tmp/openclaw";
|
|
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
|
expect(detectAgentRuntime()).toBe("openclaw");
|
|
});
|
|
|
|
it("detects Pi via PI_CODING_AGENT (set unconditionally by cli.ts:13)", async () => {
|
|
process.env["PI_CODING_AGENT"] = "true";
|
|
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
|
expect(detectAgentRuntime()).toBe("pi");
|
|
});
|
|
});
|
|
|
|
describe("detectAgentRuntime — Gemini managed agent", () => {
|
|
// Gemini managed agent is detected via the `/.agents/` platform mount (a
|
|
// DIRECTORY) and the gVisor kernel string, NOT env vars — so these tests
|
|
// mock node:fs statSync and node:os rather than mutating process.env. We key
|
|
// on the `/.agents/` directory (not the optional AGENTS.md file) so
|
|
// skills-only and inline-instruction agents are still detected.
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
stripVendorEnv();
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Clear the node:os / node:fs doMock registrations so they don't leak into
|
|
// the env-var-only suites that follow (restoreAllMocks does not undo
|
|
// doMock, and those suites don't resetModules in beforeEach).
|
|
vi.doUnmock("node:os");
|
|
vi.doUnmock("node:fs");
|
|
vi.resetModules();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
// Mock node:fs so statSync("/.agents") reports a directory; everything else
|
|
// delegates to the real fs.
|
|
const mockAgentsDir = () =>
|
|
vi.doMock("node:fs", async () => {
|
|
const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
|
|
return {
|
|
...actual,
|
|
statSync: (path: string) =>
|
|
path === "/.agents"
|
|
? ({ isDirectory: () => true } as unknown as import("node:fs").Stats)
|
|
: actual.statSync(path),
|
|
};
|
|
});
|
|
|
|
it("reports gemini_managed_agent when /.agents/ is a directory AND the kernel is gVisor", async () => {
|
|
vi.doMock("node:os", async () => {
|
|
const actual = await vi.importActual<typeof import("node:os")>("node:os");
|
|
return { ...actual, release: () => "4.19.0-gvisor", platform: () => "linux" };
|
|
});
|
|
mockAgentsDir();
|
|
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
|
expect(detectAgentRuntime()).toBe("gemini_managed_agent");
|
|
});
|
|
|
|
it("detects a skills-only managed agent (no AGENTS.md) — the generalizability case", async () => {
|
|
// AGENTS.md is OPTIONAL: an agent may use inline `system_instruction` or a
|
|
// skills-only definition and ship no AGENTS.md. Keying on the `/.agents/`
|
|
// directory mount (not the file) must still detect it — the mock makes
|
|
// `/.agents` a directory with no AGENTS.md present.
|
|
vi.doMock("node:os", async () => {
|
|
const actual = await vi.importActual<typeof import("node:os")>("node:os");
|
|
return { ...actual, release: () => "4.19.0-gvisor", platform: () => "linux" };
|
|
});
|
|
mockAgentsDir();
|
|
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
|
expect(detectAgentRuntime()).toBe("gemini_managed_agent");
|
|
});
|
|
|
|
it("does NOT report gemini_managed_agent when /.agents/ is absent (even on gVisor)", async () => {
|
|
// A generic gVisor surface (GKE Sandbox / Cloud Run gen2) that doesn't
|
|
// mount the managed-agent layout must fall through to env-var rules.
|
|
vi.doMock("node:os", async () => {
|
|
const actual = await vi.importActual<typeof import("node:os")>("node:os");
|
|
return { ...actual, release: () => "4.19.0-gvisor", platform: () => "linux" };
|
|
});
|
|
vi.doMock("node:fs", async () => {
|
|
const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
|
|
return {
|
|
...actual,
|
|
statSync: (path: string) => {
|
|
if (path === "/.agents") throw new Error("ENOENT: no such file or directory");
|
|
return actual.statSync(path);
|
|
},
|
|
};
|
|
});
|
|
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
|
expect(detectAgentRuntime()).toBeNull();
|
|
});
|
|
|
|
it("does NOT report gemini_managed_agent when /.agents/ is a directory but the kernel is not gVisor", async () => {
|
|
// A dev box that happens to have a stray /.agents/ must not false-positive
|
|
// — the gVisor conjunction is what makes the signal safe.
|
|
vi.doMock("node:os", async () => {
|
|
const actual = await vi.importActual<typeof import("node:os")>("node:os");
|
|
return { ...actual, release: () => "6.8.0-100-generic", platform: () => "linux" };
|
|
});
|
|
vi.doMock("node:fs", async () => {
|
|
const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
|
|
return {
|
|
...actual,
|
|
statSync: (path: string) =>
|
|
path === "/.agents"
|
|
? ({ isDirectory: () => true } as unknown as import("node:fs").Stats)
|
|
: actual.statSync(path),
|
|
readFileSync: (path: string) =>
|
|
path === "/proc/version"
|
|
? "Linux version 6.8.0-100-generic (buildd@lcy01)"
|
|
: actual.readFileSync(path),
|
|
};
|
|
});
|
|
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
|
expect(detectAgentRuntime()).toBeNull();
|
|
});
|
|
|
|
it("returns gemini_managed_agent over an env-var rule when both signals match", async () => {
|
|
// If a user happens to set CLAUDECODE=1 inside a Gemini sandbox (or any
|
|
// odd config), the filesystem+kernel signal wins — Gemini is more
|
|
// specific than a generic env-var marker.
|
|
process.env["CLAUDECODE"] = "1";
|
|
vi.doMock("node:os", async () => {
|
|
const actual = await vi.importActual<typeof import("node:os")>("node:os");
|
|
return { ...actual, release: () => "4.19.0-gvisor", platform: () => "linux" };
|
|
});
|
|
mockAgentsDir();
|
|
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
|
expect(detectAgentRuntime()).toBe("gemini_managed_agent");
|
|
});
|
|
});
|
|
|
|
describe("detectAgentRuntime — Windsurf / Cline / Gemini CLI / Crush", () => {
|
|
const savedEnv = { ...process.env };
|
|
beforeEach(stripVendorEnv);
|
|
afterEach(() => {
|
|
process.env = { ...savedEnv };
|
|
});
|
|
|
|
it("detects Windsurf via TERM_PROGRAM=windsurf", async () => {
|
|
process.env["TERM_PROGRAM"] = "windsurf";
|
|
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
|
expect(detectAgentRuntime()).toBe("windsurf");
|
|
});
|
|
|
|
it("detects Windsurf case-insensitively (TERM_PROGRAM=Windsurf)", async () => {
|
|
process.env["TERM_PROGRAM"] = "Windsurf";
|
|
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
|
expect(detectAgentRuntime()).toBe("windsurf");
|
|
});
|
|
|
|
it("detects Cline via CLINE_ACTIVE (default vscode-terminal path)", async () => {
|
|
process.env["CLINE_ACTIVE"] = "true";
|
|
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
|
expect(detectAgentRuntime()).toBe("cline");
|
|
});
|
|
|
|
it("detects Gemini CLI via GEMINI_CLI", async () => {
|
|
process.env["GEMINI_CLI"] = "1";
|
|
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
|
expect(detectAgentRuntime()).toBe("gemini_cli");
|
|
});
|
|
|
|
it("detects Crush via CRUSH (set unconditionally on every spawned shell)", async () => {
|
|
process.env["CRUSH"] = "1";
|
|
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
|
expect(detectAgentRuntime()).toBe("crush");
|
|
});
|
|
|
|
it("does NOT misread the user-set value (existence only) — GEMINI_CLI key shape ignored", async () => {
|
|
process.env["GEMINI_CLI"] = "anything";
|
|
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
|
expect(detectAgentRuntime()).toBe("gemini_cli");
|
|
});
|
|
});
|
|
|
|
describe("detectSandboxRuntime — file-system path", () => {
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.resetModules();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("reports docker when /.dockerenv exists", async () => {
|
|
vi.doMock("node:os", async () => {
|
|
const actual = await vi.importActual<typeof import("node:os")>("node:os");
|
|
return { ...actual, release: () => "6.8.0-100-generic", platform: () => "linux" };
|
|
});
|
|
vi.doMock("node:fs", async () => {
|
|
const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
|
|
return {
|
|
...actual,
|
|
existsSync: (path: string) => path === "/.dockerenv" || actual.existsSync(path),
|
|
readFileSync: (path: string) =>
|
|
path === "/proc/version" ? "Linux version 6.8.0-100-generic" : actual.readFileSync(path),
|
|
};
|
|
});
|
|
const { detectSandboxRuntime } = await import("./agent_runtime.js");
|
|
expect(detectSandboxRuntime()).toBe("docker");
|
|
});
|
|
|
|
it("returns null on a plain non-sandboxed Linux laptop", async () => {
|
|
vi.doMock("node:os", async () => {
|
|
const actual = await vi.importActual<typeof import("node:os")>("node:os");
|
|
return { ...actual, release: () => "6.8.0-100-generic", platform: () => "linux" };
|
|
});
|
|
vi.doMock("node:fs", async () => {
|
|
const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
|
|
return {
|
|
...actual,
|
|
existsSync: () => false,
|
|
readFileSync: (path: string) =>
|
|
path === "/proc/version"
|
|
? "Linux version 6.8.0-100-generic (buildd@lcy01)"
|
|
: path === "/proc/1/cgroup"
|
|
? "0::/user.slice/user-1000.slice"
|
|
: actual.readFileSync(path),
|
|
};
|
|
});
|
|
const { detectSandboxRuntime } = await import("./agent_runtime.js");
|
|
expect(detectSandboxRuntime()).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("detectSandboxRuntime — kernel-string path", () => {
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.resetModules();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("reports gvisor for a 4.19.0-gvisor kernel string", async () => {
|
|
vi.doMock("node:os", async () => {
|
|
const actual = await vi.importActual<typeof import("node:os")>("node:os");
|
|
return { ...actual, release: () => "4.19.0-gvisor", platform: () => "linux" };
|
|
});
|
|
const { detectSandboxRuntime } = await import("./agent_runtime.js");
|
|
expect(detectSandboxRuntime()).toBe("gvisor");
|
|
});
|
|
|
|
it("reports gvisor for kernel 4.4.0 only when /proc/version confirms gVisor", async () => {
|
|
vi.doMock("node:os", async () => {
|
|
const actual = await vi.importActual<typeof import("node:os")>("node:os");
|
|
return { ...actual, release: () => "4.4.0", platform: () => "linux" };
|
|
});
|
|
vi.doMock("node:fs", async () => {
|
|
const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
|
|
return {
|
|
...actual,
|
|
readFileSync: (path: string) =>
|
|
path === "/proc/version" ? "Linux version 4.4.0 (gVisor)" : actual.readFileSync(path),
|
|
};
|
|
});
|
|
const { detectSandboxRuntime } = await import("./agent_runtime.js");
|
|
expect(detectSandboxRuntime()).toBe("gvisor");
|
|
});
|
|
|
|
it("does NOT report gvisor for kernel 4.4.0 on a real Ubuntu 16.04 box (no gVisor in /proc/version)", async () => {
|
|
// Ubuntu 16.04 LTS ships kernel 4.4.0 too — make sure we don't false-positive.
|
|
vi.doMock("node:os", async () => {
|
|
const actual = await vi.importActual<typeof import("node:os")>("node:os");
|
|
return { ...actual, release: () => "4.4.0", platform: () => "linux" };
|
|
});
|
|
vi.doMock("node:fs", async () => {
|
|
const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
|
|
return {
|
|
...actual,
|
|
readFileSync: (path: string) =>
|
|
path === "/proc/version"
|
|
? "Linux version 4.4.0-1128-aws (buildd@lcy01)"
|
|
: actual.readFileSync(path),
|
|
};
|
|
});
|
|
const { detectSandboxRuntime } = await import("./agent_runtime.js");
|
|
expect(detectSandboxRuntime()).not.toBe("gvisor");
|
|
});
|
|
});
|