875 lines
30 KiB
JavaScript
875 lines
30 KiB
JavaScript
|
|
const test = require("node:test");
|
||
|
|
const assert = require("node:assert/strict");
|
||
|
|
const {
|
||
|
|
registerSdkStreamHandlers,
|
||
|
|
buildSdkTurnPrompt,
|
||
|
|
formatSdkHistoryReplaySection,
|
||
|
|
buildSdkModelCacheKey,
|
||
|
|
getSdkModelCacheEntry,
|
||
|
|
setSdkModelCacheEntry,
|
||
|
|
buildSdkSessionKey,
|
||
|
|
normalizeSdkListModelsResult,
|
||
|
|
resolveSdkPromptPlacement,
|
||
|
|
resolveSdkResumeSessionId,
|
||
|
|
shouldReplaySdkHistory,
|
||
|
|
expireSiblingCursorCliModeSessions,
|
||
|
|
expireSiblingGrokRuntimeSessions,
|
||
|
|
resolveBackendKey,
|
||
|
|
resolveSdkBackendBinPath,
|
||
|
|
shouldCacheSdkRuntimeModels,
|
||
|
|
} = require("./sdkStreamHandlers.cjs");
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Register the real IPC handlers against a stubbed ctx so lifecycle handlers
|
||
|
|
* (cleanup) can be invoked directly. registerSdkStreamHandlers exposes its
|
||
|
|
* request-scoped maps on ctx for exactly this kind of test.
|
||
|
|
*/
|
||
|
|
function registerWithStubbedCtx() {
|
||
|
|
const handlers = new Map();
|
||
|
|
const ctx = {
|
||
|
|
ipcMain: { handle: (channel, fn) => handlers.set(channel, fn) },
|
||
|
|
electronModule: undefined,
|
||
|
|
validateSender: () => true,
|
||
|
|
mcpServerBridge: {
|
||
|
|
setChatSessionCancelled: () => {},
|
||
|
|
cancelPtyExecsForSession: () => {},
|
||
|
|
cancelWorkerBackgroundJobsForSession: () => {},
|
||
|
|
cleanupScopedMetadata: async () => {},
|
||
|
|
},
|
||
|
|
};
|
||
|
|
registerSdkStreamHandlers(ctx);
|
||
|
|
return { handlers, ctx };
|
||
|
|
}
|
||
|
|
|
||
|
|
test("sdk-agent:cleanup aborts and removes request entries for the target chat only", async () => {
|
||
|
|
const { handlers, ctx } = registerWithStubbedCtx();
|
||
|
|
|
||
|
|
const targetController = new AbortController();
|
||
|
|
const otherController = new AbortController();
|
||
|
|
ctx.sdkActiveStreams.set("req-1", targetController);
|
||
|
|
ctx.sdkRequestSessions.set("req-1", "chat-1");
|
||
|
|
ctx.sdkRequestRuntimes.set("req-1", { backendKey: "codebuddy", codexRuntime: "sdk", binPath: "/bin/cb" });
|
||
|
|
ctx.sdkActiveStreams.set("req-2", otherController);
|
||
|
|
ctx.sdkRequestSessions.set("req-2", "chat-2");
|
||
|
|
ctx.sdkRequestRuntimes.set("req-2", { backendKey: "codex", codexRuntime: "sdk", binPath: "/bin/codex" });
|
||
|
|
|
||
|
|
const cleanup = handlers.get("netcatty:ai:sdk-agent:cleanup");
|
||
|
|
assert.equal(typeof cleanup, "function");
|
||
|
|
const result = await cleanup({ sender: {} }, { chatSessionId: "chat-1" });
|
||
|
|
assert.deepEqual(result, { ok: true });
|
||
|
|
|
||
|
|
// Target chat: controller aborted and every request-scoped entry removed.
|
||
|
|
assert.ok(targetController.signal.aborted);
|
||
|
|
assert.ok(!ctx.sdkActiveStreams.has("req-1"));
|
||
|
|
assert.ok(!ctx.sdkRequestSessions.has("req-1"));
|
||
|
|
assert.ok(!ctx.sdkRequestRuntimes.has("req-1"));
|
||
|
|
|
||
|
|
// Other chat: untouched.
|
||
|
|
assert.ok(!otherController.signal.aborted);
|
||
|
|
assert.equal(ctx.sdkActiveStreams.get("req-2"), otherController);
|
||
|
|
assert.equal(ctx.sdkRequestSessions.get("req-2"), "chat-2");
|
||
|
|
assert.deepEqual(ctx.sdkRequestRuntimes.get("req-2"), {
|
||
|
|
backendKey: "codex",
|
||
|
|
codexRuntime: "sdk",
|
||
|
|
binPath: "/bin/codex",
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
test("resolveBackendKey maps backend command/value to registry key", () => {
|
||
|
|
assert.equal(resolveBackendKey("claude"), "claude");
|
||
|
|
assert.equal(resolveBackendKey("codex"), "codex");
|
||
|
|
assert.equal(resolveBackendKey("copilot"), "copilot");
|
||
|
|
assert.equal(resolveBackendKey("codebuddy"), "codebuddy");
|
||
|
|
assert.equal(resolveBackendKey("opencode"), "opencode");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("resolveBackendKey returns null for unknown", () => {
|
||
|
|
assert.equal(resolveBackendKey("claude-agent-acp"), null);
|
||
|
|
assert.equal(resolveBackendKey(""), null);
|
||
|
|
assert.equal(resolveBackendKey(undefined), null);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("SDK session keys include backend and resolved CLI path", () => {
|
||
|
|
assert.notEqual(
|
||
|
|
buildSdkSessionKey("chat-1", "codex", "/usr/local/bin/codex"),
|
||
|
|
buildSdkSessionKey("chat-1", "codex", "/opt/homebrew/bin/codex"),
|
||
|
|
);
|
||
|
|
assert.notEqual(
|
||
|
|
buildSdkSessionKey("chat-1", "codex", "/usr/local/bin/codex"),
|
||
|
|
buildSdkSessionKey("chat-1", "claude", "/usr/local/bin/codex"),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("Cursor session keys isolate CLI login from API key auth modes", () => {
|
||
|
|
assert.notEqual(
|
||
|
|
buildSdkSessionKey("chat-1", "cursor", "/usr/bin/agent", "sdk", "cli-login"),
|
||
|
|
buildSdkSessionKey("chat-1", "cursor", "cursor", "sdk", "api-key"),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("SDK model cache keys include resolved CLI path", () => {
|
||
|
|
assert.notEqual(
|
||
|
|
buildSdkModelCacheKey("claude", "/usr/local/bin/claude"),
|
||
|
|
buildSdkModelCacheKey("claude", "/opt/homebrew/bin/claude"),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("SDK model cache keys include catalog-affecting agent environment", () => {
|
||
|
|
assert.notEqual(
|
||
|
|
buildSdkModelCacheKey("opencode", "/usr/bin/opencode", { HOME: "/Users/a", OPENCODE_CONFIG_DIR: "/a/config" }),
|
||
|
|
buildSdkModelCacheKey("opencode", "/usr/bin/opencode", { HOME: "/Users/b", OPENCODE_CONFIG_DIR: "/b/config" }),
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
buildSdkModelCacheKey("opencode", "/usr/bin/opencode", { HOME: "/Users/a" }),
|
||
|
|
buildSdkModelCacheKey("opencode", "/usr/bin/opencode", { HOME: "/Users/a" }),
|
||
|
|
);
|
||
|
|
assert.doesNotMatch(
|
||
|
|
buildSdkModelCacheKey("cursor", "/usr/bin/cursor", { CURSOR_API_KEY: "very-secret-key" }),
|
||
|
|
/very-secret-key/,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("SDK model cache removes expired entries instead of retaining tombstones", () => {
|
||
|
|
const cache = new Map([
|
||
|
|
["expired", { at: 1, currentModelId: null, models: [{ id: "old" }] }],
|
||
|
|
["fresh", { at: 95, currentModelId: null, models: [{ id: "new" }] }],
|
||
|
|
]);
|
||
|
|
|
||
|
|
assert.equal(getSdkModelCacheEntry(cache, "expired", { now: 100, ttlMs: 10, maxEntries: 8 }), null);
|
||
|
|
assert.equal(cache.has("expired"), false);
|
||
|
|
assert.equal(getSdkModelCacheEntry(cache, "fresh", { now: 100, ttlMs: 10, maxEntries: 8 }).models[0].id, "new");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("SDK model cache evicts the least recently used catalog at its hard limit", () => {
|
||
|
|
const cache = new Map();
|
||
|
|
setSdkModelCacheEntry(cache, "a", { at: 1, models: [{ id: "a" }] }, { now: 1, ttlMs: 100, maxEntries: 2 });
|
||
|
|
setSdkModelCacheEntry(cache, "b", { at: 2, models: [{ id: "b" }] }, { now: 2, ttlMs: 100, maxEntries: 2 });
|
||
|
|
assert.ok(getSdkModelCacheEntry(cache, "a", { now: 3, ttlMs: 100, maxEntries: 2 }));
|
||
|
|
setSdkModelCacheEntry(cache, "c", { at: 3, models: [{ id: "c" }] }, { now: 3, ttlMs: 100, maxEntries: 2 });
|
||
|
|
|
||
|
|
assert.deepEqual(Array.from(cache.keys()), ["a", "c"]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("normalizeSdkListModelsResult preserves current model ids from object results", () => {
|
||
|
|
assert.deepEqual(normalizeSdkListModelsResult({
|
||
|
|
currentModelId: "openai/gpt-5.1",
|
||
|
|
models: [{ id: "openai/gpt-5.1" }, null, { name: "missing-id" }],
|
||
|
|
}), {
|
||
|
|
currentModelId: "openai/gpt-5.1",
|
||
|
|
models: [{ id: "openai/gpt-5.1" }],
|
||
|
|
});
|
||
|
|
assert.deepEqual(normalizeSdkListModelsResult([{ id: "claude-sonnet" }]), {
|
||
|
|
currentModelId: null,
|
||
|
|
models: [{ id: "claude-sonnet" }],
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
test("CodeBuddy and OpenCode keep Netcatty context in the system prompt only", () => {
|
||
|
|
const input = {
|
||
|
|
turnPrompt: "user request",
|
||
|
|
contextualPrompt: "netcatty context\n\nuser request",
|
||
|
|
systemContext: "netcatty context",
|
||
|
|
};
|
||
|
|
assert.deepEqual(resolveSdkPromptPlacement({
|
||
|
|
...input,
|
||
|
|
backendKey: "codebuddy",
|
||
|
|
}), {
|
||
|
|
prompt: "user request",
|
||
|
|
systemPrompt: "netcatty context",
|
||
|
|
});
|
||
|
|
assert.deepEqual(resolveSdkPromptPlacement({
|
||
|
|
...input,
|
||
|
|
backendKey: "opencode",
|
||
|
|
}), {
|
||
|
|
prompt: "user request",
|
||
|
|
systemPrompt: "netcatty context",
|
||
|
|
});
|
||
|
|
assert.deepEqual(resolveSdkPromptPlacement({
|
||
|
|
...input,
|
||
|
|
backendKey: "claude",
|
||
|
|
}), {
|
||
|
|
prompt: "netcatty context\n\nuser request",
|
||
|
|
systemPrompt: undefined,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
test("shouldCacheSdkRuntimeModels caches all SDK backends including OpenCode", () => {
|
||
|
|
// OpenCode used to skip the cache, which re-spawned opencode servers on every
|
||
|
|
// model-catalog probe (#2184). TTL still bounds staleness.
|
||
|
|
assert.equal(shouldCacheSdkRuntimeModels("opencode"), true);
|
||
|
|
assert.equal(shouldCacheSdkRuntimeModels("claude"), true);
|
||
|
|
assert.equal(shouldCacheSdkRuntimeModels("codebuddy"), true);
|
||
|
|
assert.equal(shouldCacheSdkRuntimeModels("copilot"), true);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("SDK resume only uses the current backend/path session key", () => {
|
||
|
|
const sessions = new Map([
|
||
|
|
[buildSdkSessionKey("chat-1", "codex", "/old/codex"), "old-session"],
|
||
|
|
]);
|
||
|
|
|
||
|
|
assert.equal(
|
||
|
|
resolveSdkResumeSessionId({
|
||
|
|
sdkSessionIds: sessions,
|
||
|
|
sdkSessionKey: buildSdkSessionKey("chat-1", "codex", "/new/codex"),
|
||
|
|
backendKey: "codex",
|
||
|
|
binPath: "/new/codex",
|
||
|
|
hasConfiguredCommand: true,
|
||
|
|
}),
|
||
|
|
undefined,
|
||
|
|
);
|
||
|
|
sessions.set(buildSdkSessionKey("chat-1", "codex", "/new/codex"), "new-session");
|
||
|
|
assert.equal(
|
||
|
|
resolveSdkResumeSessionId({
|
||
|
|
sdkSessionIds: sessions,
|
||
|
|
sdkSessionKey: buildSdkSessionKey("chat-1", "codex", "/new/codex"),
|
||
|
|
backendKey: "codex",
|
||
|
|
binPath: "/new/codex",
|
||
|
|
hasConfiguredCommand: true,
|
||
|
|
}),
|
||
|
|
"new-session",
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("SDK resume uses persisted session identity only when backend and path match", () => {
|
||
|
|
const persisted = `netcatty-sdk-session:${encodeURIComponent(JSON.stringify({
|
||
|
|
v: 1,
|
||
|
|
id: "persisted-session",
|
||
|
|
backend: "codex",
|
||
|
|
binPath: "/opt/homebrew/bin/codex",
|
||
|
|
}))}`;
|
||
|
|
|
||
|
|
assert.equal(
|
||
|
|
resolveSdkResumeSessionId({
|
||
|
|
sdkSessionIds: new Map(),
|
||
|
|
sdkSessionKey: buildSdkSessionKey("chat-1", "codex", "/opt/homebrew/bin/codex"),
|
||
|
|
existingSessionId: persisted,
|
||
|
|
backendKey: "codex",
|
||
|
|
binPath: "/opt/homebrew/bin/codex",
|
||
|
|
hasConfiguredCommand: true,
|
||
|
|
}),
|
||
|
|
"persisted-session",
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
resolveSdkResumeSessionId({
|
||
|
|
sdkSessionIds: new Map(),
|
||
|
|
sdkSessionKey: buildSdkSessionKey("chat-1", "codex", "/other/codex"),
|
||
|
|
existingSessionId: persisted,
|
||
|
|
backendKey: "codex",
|
||
|
|
binPath: "/other/codex",
|
||
|
|
hasConfiguredCommand: true,
|
||
|
|
}),
|
||
|
|
undefined,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("Codex sessions never resume across SDK and App Server runtimes", () => {
|
||
|
|
const sdkIdentity = `netcatty-sdk-session:${encodeURIComponent(JSON.stringify({
|
||
|
|
v: 1,
|
||
|
|
id: "sdk-thread",
|
||
|
|
backend: "codex",
|
||
|
|
binPath: "/usr/bin/codex",
|
||
|
|
runtime: "sdk",
|
||
|
|
}))}`;
|
||
|
|
assert.equal(resolveSdkResumeSessionId({
|
||
|
|
sdkSessionIds: new Map(),
|
||
|
|
sdkSessionKey: buildSdkSessionKey("chat-1", "codex", "/usr/bin/codex", "app-server"),
|
||
|
|
existingSessionId: sdkIdentity,
|
||
|
|
backendKey: "codex",
|
||
|
|
binPath: "/usr/bin/codex",
|
||
|
|
runtime: "app-server",
|
||
|
|
hasConfiguredCommand: false,
|
||
|
|
}), undefined);
|
||
|
|
assert.equal(resolveSdkResumeSessionId({
|
||
|
|
sdkSessionIds: new Map(),
|
||
|
|
sdkSessionKey: buildSdkSessionKey("chat-1", "codex", "/usr/bin/codex", "app-server"),
|
||
|
|
existingSessionId: "legacy-thread",
|
||
|
|
backendKey: "codex",
|
||
|
|
binPath: "/usr/bin/codex",
|
||
|
|
runtime: "app-server",
|
||
|
|
hasConfiguredCommand: false,
|
||
|
|
}), undefined);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("SDK resume keeps legacy session ids only when no manual command is configured", () => {
|
||
|
|
assert.equal(
|
||
|
|
resolveSdkResumeSessionId({
|
||
|
|
sdkSessionIds: new Map(),
|
||
|
|
sdkSessionKey: buildSdkSessionKey("chat-1", "codex", "/usr/bin/codex"),
|
||
|
|
existingSessionId: "legacy-session",
|
||
|
|
backendKey: "codex",
|
||
|
|
binPath: "/usr/bin/codex",
|
||
|
|
hasConfiguredCommand: false,
|
||
|
|
}),
|
||
|
|
"legacy-session",
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
resolveSdkResumeSessionId({
|
||
|
|
sdkSessionIds: new Map(),
|
||
|
|
sdkSessionKey: buildSdkSessionKey("chat-1", "codex", "/manual/codex"),
|
||
|
|
existingSessionId: "legacy-session",
|
||
|
|
backendKey: "codex",
|
||
|
|
binPath: "/manual/codex",
|
||
|
|
hasConfiguredCommand: true,
|
||
|
|
}),
|
||
|
|
undefined,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("Cursor CLI login sessions do not resume on the API key SDK path", () => {
|
||
|
|
const cliIdentity = `netcatty-sdk-session:${encodeURIComponent(JSON.stringify({
|
||
|
|
v: 1,
|
||
|
|
id: "61668441-bfcb-4795-a575-c46d70ad01fe",
|
||
|
|
backend: "cursor",
|
||
|
|
binPath: "/usr/bin/agent",
|
||
|
|
runtime: "sdk",
|
||
|
|
authMode: "cli-login",
|
||
|
|
cliMode: "agent",
|
||
|
|
}))}`;
|
||
|
|
|
||
|
|
assert.equal(
|
||
|
|
resolveSdkResumeSessionId({
|
||
|
|
sdkSessionIds: new Map(),
|
||
|
|
sdkSessionKey: buildSdkSessionKey("chat-1", "cursor", "cursor", "sdk", "api-key"),
|
||
|
|
existingSessionId: cliIdentity,
|
||
|
|
backendKey: "cursor",
|
||
|
|
binPath: "cursor",
|
||
|
|
runtime: "sdk",
|
||
|
|
authMode: "api-key",
|
||
|
|
hasConfiguredCommand: false,
|
||
|
|
}),
|
||
|
|
undefined,
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
resolveSdkResumeSessionId({
|
||
|
|
sdkSessionIds: new Map(),
|
||
|
|
sdkSessionKey: buildSdkSessionKey("chat-1", "cursor", "/usr/bin/agent", "sdk", "cli-login", "agent"),
|
||
|
|
existingSessionId: cliIdentity,
|
||
|
|
backendKey: "cursor",
|
||
|
|
binPath: "/usr/bin/agent",
|
||
|
|
runtime: "sdk",
|
||
|
|
authMode: "cli-login",
|
||
|
|
cliMode: "agent",
|
||
|
|
hasConfiguredCommand: false,
|
||
|
|
}),
|
||
|
|
"61668441-bfcb-4795-a575-c46d70ad01fe",
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
resolveSdkResumeSessionId({
|
||
|
|
sdkSessionIds: new Map(),
|
||
|
|
sdkSessionKey: buildSdkSessionKey("chat-1", "cursor", "/usr/bin/agent", "sdk", "cli-login", "ask"),
|
||
|
|
existingSessionId: cliIdentity,
|
||
|
|
backendKey: "cursor",
|
||
|
|
binPath: "/usr/bin/agent",
|
||
|
|
runtime: "sdk",
|
||
|
|
authMode: "cli-login",
|
||
|
|
cliMode: "ask",
|
||
|
|
hasConfiguredCommand: false,
|
||
|
|
}),
|
||
|
|
undefined,
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
resolveSdkResumeSessionId({
|
||
|
|
sdkSessionIds: new Map(),
|
||
|
|
sdkSessionKey: buildSdkSessionKey("chat-1", "cursor", "cursor", "sdk", "cli-login"),
|
||
|
|
existingSessionId: "61668441-bfcb-4795-a575-c46d70ad01fe",
|
||
|
|
backendKey: "cursor",
|
||
|
|
binPath: "cursor",
|
||
|
|
runtime: "sdk",
|
||
|
|
authMode: "cli-login",
|
||
|
|
hasConfiguredCommand: false,
|
||
|
|
}),
|
||
|
|
undefined,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("expireSiblingCursorCliModeSessions drops the inactive Cursor CLI mode", () => {
|
||
|
|
const askKey = buildSdkSessionKey("chat-1", "cursor", "/bin/cursor-agent", "sdk", "cli-login", "ask");
|
||
|
|
const agentKey = buildSdkSessionKey("chat-1", "cursor", "/bin/cursor-agent", "sdk", "cli-login", "agent");
|
||
|
|
const otherChatAskKey = buildSdkSessionKey("chat-2", "cursor", "/bin/cursor-agent", "sdk", "cli-login", "ask");
|
||
|
|
const sessions = new Map([
|
||
|
|
[askKey, "ask-session"],
|
||
|
|
[agentKey, "agent-session"],
|
||
|
|
[otherChatAskKey, "other-ask"],
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Observer → Confirm: expire Ask so a later switch-back cannot revive it.
|
||
|
|
assert.equal(
|
||
|
|
expireSiblingCursorCliModeSessions(sessions, {
|
||
|
|
chatSessionId: "chat-1",
|
||
|
|
backendKey: "cursor",
|
||
|
|
binPath: "/bin/cursor-agent",
|
||
|
|
runtime: "sdk",
|
||
|
|
authMode: "cli-login",
|
||
|
|
cliMode: "agent",
|
||
|
|
}),
|
||
|
|
true,
|
||
|
|
);
|
||
|
|
assert.equal(sessions.has(askKey), false);
|
||
|
|
assert.equal(sessions.get(agentKey), "agent-session");
|
||
|
|
assert.equal(sessions.get(otherChatAskKey), "other-ask");
|
||
|
|
|
||
|
|
// Confirm → Observer: expire agent; Ask was already gone, so resume is fresh.
|
||
|
|
sessions.set(agentKey, "agent-session-2");
|
||
|
|
assert.equal(
|
||
|
|
expireSiblingCursorCliModeSessions(sessions, {
|
||
|
|
chatSessionId: "chat-1",
|
||
|
|
backendKey: "cursor",
|
||
|
|
binPath: "/bin/cursor-agent",
|
||
|
|
runtime: "sdk",
|
||
|
|
authMode: "cli-login",
|
||
|
|
cliMode: "ask",
|
||
|
|
}),
|
||
|
|
true,
|
||
|
|
);
|
||
|
|
assert.equal(sessions.has(agentKey), false);
|
||
|
|
assert.equal(
|
||
|
|
resolveSdkResumeSessionId({
|
||
|
|
sdkSessionIds: sessions,
|
||
|
|
sdkSessionKey: askKey,
|
||
|
|
existingSessionId: `netcatty-sdk-session:${encodeURIComponent(JSON.stringify({
|
||
|
|
v: 1,
|
||
|
|
id: "agent-session-2",
|
||
|
|
backend: "cursor",
|
||
|
|
binPath: "/bin/cursor-agent",
|
||
|
|
runtime: "sdk",
|
||
|
|
authMode: "cli-login",
|
||
|
|
cliMode: "agent",
|
||
|
|
}))}`,
|
||
|
|
backendKey: "cursor",
|
||
|
|
binPath: "/bin/cursor-agent",
|
||
|
|
runtime: "sdk",
|
||
|
|
authMode: "cli-login",
|
||
|
|
cliMode: "ask",
|
||
|
|
hasConfiguredCommand: false,
|
||
|
|
}),
|
||
|
|
undefined,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("buildSdkTurnPrompt replays history only when requested", () => {
|
||
|
|
const prompt = buildSdkTurnPrompt({
|
||
|
|
prompt: "latest question",
|
||
|
|
replayHistory: true,
|
||
|
|
historyMessages: [
|
||
|
|
{ role: "user", content: "previous question" },
|
||
|
|
{ role: "assistant", content: "previous answer" },
|
||
|
|
],
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.match(prompt, /Conversation context replay/);
|
||
|
|
assert.match(prompt, /USER: previous question/);
|
||
|
|
assert.match(prompt, /ASSISTANT: previous answer/);
|
||
|
|
assert.match(prompt, /latest question$/);
|
||
|
|
|
||
|
|
const steadyStatePrompt = buildSdkTurnPrompt({
|
||
|
|
prompt: "latest question",
|
||
|
|
replayHistory: false,
|
||
|
|
historyMessages: [{ role: "user", content: "previous question" }],
|
||
|
|
});
|
||
|
|
assert.equal(steadyStatePrompt, "latest question");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("formatSdkHistoryReplaySection matches buildSdkTurnPrompt history wording", () => {
|
||
|
|
const messages = [
|
||
|
|
{ role: "user", content: "previous question" },
|
||
|
|
{ role: "assistant", content: "previous answer" },
|
||
|
|
];
|
||
|
|
const section = formatSdkHistoryReplaySection(messages);
|
||
|
|
assert.match(section, /Conversation context replay/);
|
||
|
|
assert.match(section, /USER: previous question/);
|
||
|
|
assert.match(section, /ASSISTANT: previous answer/);
|
||
|
|
// Same section is embedded when replayHistory is true.
|
||
|
|
const full = buildSdkTurnPrompt({
|
||
|
|
prompt: "latest",
|
||
|
|
replayHistory: true,
|
||
|
|
historyMessages: messages,
|
||
|
|
});
|
||
|
|
assert.ok(full.startsWith(section));
|
||
|
|
assert.equal(formatSdkHistoryReplaySection([]), "");
|
||
|
|
assert.equal(formatSdkHistoryReplaySection(undefined), "");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("CodeBuddy does not replay renderer history when a persisted session can resume", () => {
|
||
|
|
assert.equal(shouldReplaySdkHistory({
|
||
|
|
backendKey: "codebuddy",
|
||
|
|
codexRuntime: "sdk",
|
||
|
|
resumeSessionId: "resumed-codebuddy",
|
||
|
|
hasInMemorySession: false,
|
||
|
|
}), false);
|
||
|
|
assert.equal(shouldReplaySdkHistory({
|
||
|
|
backendKey: "codebuddy",
|
||
|
|
codexRuntime: "sdk",
|
||
|
|
resumeSessionId: undefined,
|
||
|
|
hasInMemorySession: false,
|
||
|
|
}), true);
|
||
|
|
assert.equal(shouldReplaySdkHistory({
|
||
|
|
backendKey: "claude",
|
||
|
|
codexRuntime: "sdk",
|
||
|
|
resumeSessionId: "resumed-claude",
|
||
|
|
hasInMemorySession: false,
|
||
|
|
}), true);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("Grok does not replay renderer history when an ACP session can resume", () => {
|
||
|
|
// Mirrors CodeBuddy: session/load / resume already restores Grok transcript.
|
||
|
|
// Applies to both ACP and streaming-json once a resume id is present.
|
||
|
|
assert.equal(shouldReplaySdkHistory({
|
||
|
|
backendKey: "grok",
|
||
|
|
codexRuntime: "sdk",
|
||
|
|
resumeSessionId: "resumed-grok",
|
||
|
|
hasInMemorySession: false,
|
||
|
|
}), false);
|
||
|
|
assert.equal(shouldReplaySdkHistory({
|
||
|
|
backendKey: "grok",
|
||
|
|
codexRuntime: "sdk",
|
||
|
|
resumeSessionId: undefined,
|
||
|
|
hasInMemorySession: false,
|
||
|
|
}), true);
|
||
|
|
// Even with an in-memory map miss, resume id alone must suppress replay.
|
||
|
|
assert.equal(shouldReplaySdkHistory({
|
||
|
|
backendKey: "grok",
|
||
|
|
codexRuntime: "sdk",
|
||
|
|
resumeSessionId: "s1",
|
||
|
|
hasInMemorySession: true,
|
||
|
|
}), false);
|
||
|
|
// First turn (no resume) still seeds context even if in-memory key exists.
|
||
|
|
assert.equal(shouldReplaySdkHistory({
|
||
|
|
backendKey: "grok",
|
||
|
|
codexRuntime: "sdk",
|
||
|
|
resumeSessionId: undefined,
|
||
|
|
hasInMemorySession: true,
|
||
|
|
}), true);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("expireSiblingGrokRuntimeSessions drops the inactive Grok runtime", () => {
|
||
|
|
const acpKey = buildSdkSessionKey("chat-1", "grok", "/usr/bin/grok", "acp");
|
||
|
|
const headlessKey = buildSdkSessionKey("chat-1", "grok", "/usr/bin/grok", "streaming-json");
|
||
|
|
const otherChatAcpKey = buildSdkSessionKey("chat-2", "grok", "/usr/bin/grok", "acp");
|
||
|
|
const sessions = new Map([
|
||
|
|
[acpKey, "acp-session"],
|
||
|
|
[headlessKey, "json-session"],
|
||
|
|
[otherChatAcpKey, "other-acp"],
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Switch to streaming-json: expire ACP so switch-back cannot revive it.
|
||
|
|
assert.equal(
|
||
|
|
expireSiblingGrokRuntimeSessions(sessions, {
|
||
|
|
chatSessionId: "chat-1",
|
||
|
|
backendKey: "grok",
|
||
|
|
binPath: "/usr/bin/grok",
|
||
|
|
runtime: "streaming-json",
|
||
|
|
}),
|
||
|
|
true,
|
||
|
|
);
|
||
|
|
assert.equal(sessions.has(acpKey), false);
|
||
|
|
assert.equal(sessions.get(headlessKey), "json-session");
|
||
|
|
assert.equal(sessions.get(otherChatAcpKey), "other-acp");
|
||
|
|
|
||
|
|
// Switch back to ACP: expire headless; ACP was already gone → fresh resume.
|
||
|
|
sessions.set(headlessKey, "json-session-2");
|
||
|
|
assert.equal(
|
||
|
|
expireSiblingGrokRuntimeSessions(sessions, {
|
||
|
|
chatSessionId: "chat-1",
|
||
|
|
backendKey: "grok",
|
||
|
|
binPath: "/usr/bin/grok",
|
||
|
|
runtime: "acp",
|
||
|
|
}),
|
||
|
|
true,
|
||
|
|
);
|
||
|
|
assert.equal(sessions.has(headlessKey), false);
|
||
|
|
assert.equal(
|
||
|
|
resolveSdkResumeSessionId({
|
||
|
|
sdkSessionIds: sessions,
|
||
|
|
sdkSessionKey: acpKey,
|
||
|
|
existingSessionId: `netcatty-sdk-session:${encodeURIComponent(JSON.stringify({
|
||
|
|
v: 1,
|
||
|
|
id: "json-session-2",
|
||
|
|
backend: "grok",
|
||
|
|
binPath: "/usr/bin/grok",
|
||
|
|
runtime: "streaming-json",
|
||
|
|
}))}`,
|
||
|
|
backendKey: "grok",
|
||
|
|
binPath: "/usr/bin/grok",
|
||
|
|
runtime: "acp",
|
||
|
|
hasConfiguredCommand: false,
|
||
|
|
}),
|
||
|
|
undefined,
|
||
|
|
);
|
||
|
|
// Non-grok backends no-op.
|
||
|
|
assert.equal(
|
||
|
|
expireSiblingGrokRuntimeSessions(sessions, {
|
||
|
|
chatSessionId: "chat-1",
|
||
|
|
backendKey: "claude",
|
||
|
|
binPath: "/usr/bin/claude",
|
||
|
|
runtime: "sdk",
|
||
|
|
}),
|
||
|
|
false,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("Grok ACP and streaming-json session identities never cross-resume", () => {
|
||
|
|
const acpIdentity = `netcatty-sdk-session:${encodeURIComponent(JSON.stringify({
|
||
|
|
v: 1,
|
||
|
|
id: "grok-acp-thread",
|
||
|
|
backend: "grok",
|
||
|
|
binPath: "/usr/bin/grok",
|
||
|
|
runtime: "acp",
|
||
|
|
}))}`;
|
||
|
|
const headlessIdentity = `netcatty-sdk-session:${encodeURIComponent(JSON.stringify({
|
||
|
|
v: 1,
|
||
|
|
id: "grok-headless-thread",
|
||
|
|
backend: "grok",
|
||
|
|
binPath: "/usr/bin/grok",
|
||
|
|
runtime: "streaming-json",
|
||
|
|
}))}`;
|
||
|
|
|
||
|
|
// ACP identity must not resume onto streaming-json runtime.
|
||
|
|
assert.equal(resolveSdkResumeSessionId({
|
||
|
|
sdkSessionIds: new Map(),
|
||
|
|
sdkSessionKey: buildSdkSessionKey("chat-1", "grok", "/usr/bin/grok", "streaming-json"),
|
||
|
|
existingSessionId: acpIdentity,
|
||
|
|
backendKey: "grok",
|
||
|
|
binPath: "/usr/bin/grok",
|
||
|
|
runtime: "streaming-json",
|
||
|
|
hasConfiguredCommand: false,
|
||
|
|
}), undefined);
|
||
|
|
|
||
|
|
// streaming-json identity must not resume onto ACP runtime.
|
||
|
|
assert.equal(resolveSdkResumeSessionId({
|
||
|
|
sdkSessionIds: new Map(),
|
||
|
|
sdkSessionKey: buildSdkSessionKey("chat-1", "grok", "/usr/bin/grok", "acp"),
|
||
|
|
existingSessionId: headlessIdentity,
|
||
|
|
backendKey: "grok",
|
||
|
|
binPath: "/usr/bin/grok",
|
||
|
|
runtime: "acp",
|
||
|
|
hasConfiguredCommand: false,
|
||
|
|
}), undefined);
|
||
|
|
|
||
|
|
// Matching runtime resumes.
|
||
|
|
assert.equal(resolveSdkResumeSessionId({
|
||
|
|
sdkSessionIds: new Map(),
|
||
|
|
sdkSessionKey: buildSdkSessionKey("chat-1", "grok", "/usr/bin/grok", "acp"),
|
||
|
|
existingSessionId: acpIdentity,
|
||
|
|
backendKey: "grok",
|
||
|
|
binPath: "/usr/bin/grok",
|
||
|
|
runtime: "acp",
|
||
|
|
hasConfiguredCommand: false,
|
||
|
|
}), "grok-acp-thread");
|
||
|
|
assert.equal(resolveSdkResumeSessionId({
|
||
|
|
sdkSessionIds: new Map(),
|
||
|
|
sdkSessionKey: buildSdkSessionKey("chat-1", "grok", "/usr/bin/grok", "streaming-json"),
|
||
|
|
existingSessionId: headlessIdentity,
|
||
|
|
backendKey: "grok",
|
||
|
|
binPath: "/usr/bin/grok",
|
||
|
|
runtime: "streaming-json",
|
||
|
|
hasConfiguredCommand: false,
|
||
|
|
}), "grok-headless-thread");
|
||
|
|
|
||
|
|
// Bare legacy ids are only safe for runtime "sdk" — not Grok dual runtimes.
|
||
|
|
assert.equal(resolveSdkResumeSessionId({
|
||
|
|
sdkSessionIds: new Map(),
|
||
|
|
sdkSessionKey: buildSdkSessionKey("chat-1", "grok", "/usr/bin/grok", "acp"),
|
||
|
|
existingSessionId: "legacy-bare-id",
|
||
|
|
backendKey: "grok",
|
||
|
|
binPath: "/usr/bin/grok",
|
||
|
|
runtime: "acp",
|
||
|
|
hasConfiguredCommand: false,
|
||
|
|
}), undefined);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("buildSdkTurnPrompt stages attachments as local file hints", () => {
|
||
|
|
const staged = [];
|
||
|
|
const prompt = buildSdkTurnPrompt({
|
||
|
|
prompt: "describe it",
|
||
|
|
attachments: [
|
||
|
|
{ base64Data: Buffer.from("img").toString("base64"), mediaType: "image/png", filename: "screen.png" },
|
||
|
|
],
|
||
|
|
writeAttachmentToTemp: (attachment) => `/tmp/${attachment.filename}`,
|
||
|
|
onStagedAttachment: (attachment) => staged.push(attachment),
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.match(prompt, /Attached files/);
|
||
|
|
assert.match(prompt, /read_attachment/);
|
||
|
|
assert.match(prompt, /"screen\.png" \(image\/png\)/);
|
||
|
|
assert.match(prompt, /\/tmp\/screen\.png/);
|
||
|
|
assert.match(prompt, /describe it$/);
|
||
|
|
assert.deepEqual(staged, [{
|
||
|
|
filename: "screen.png",
|
||
|
|
mediaType: "image/png",
|
||
|
|
filePath: "/tmp/screen.png",
|
||
|
|
base64Data: Buffer.from("img").toString("base64"),
|
||
|
|
}]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("buildSdkTurnPrompt directs Skills-mode attachments to the controlled CLI", () => {
|
||
|
|
const prompt = buildSdkTurnPrompt({
|
||
|
|
prompt: "read it",
|
||
|
|
toolIntegrationMode: "skills",
|
||
|
|
attachments: [
|
||
|
|
{ base64Data: "ZGF0YQ==", mediaType: "text/plain", filename: "notes.txt" },
|
||
|
|
],
|
||
|
|
writeAttachmentToTemp: (attachment) => `/tmp/${attachment.filename}`,
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.match(prompt, /attachment list\/read CLI commands/);
|
||
|
|
assert.doesNotMatch(prompt, /list_attachments|read_attachment/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("resolveSdkBackendBinPath prefers configured CodeBuddy path", () => {
|
||
|
|
const out = resolveSdkBackendBinPath({
|
||
|
|
backendKey: "codebuddy",
|
||
|
|
shellEnv: { PATH: "/usr/bin" },
|
||
|
|
env: { CODEBUDDY_CODE_PATH: "/shim/bin/codebuddy" },
|
||
|
|
resolveCliFromPath: () => "/usr/bin/codebuddy",
|
||
|
|
normalizeCliPathForPlatform: (value) => value,
|
||
|
|
realpath: () => "/opt/codebuddy/bin/codebuddy",
|
||
|
|
});
|
||
|
|
assert.equal(out, "/opt/codebuddy/bin/codebuddy");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("resolveSdkBackendBinPath prefers the renderer-configured command path", () => {
|
||
|
|
const out = resolveSdkBackendBinPath({
|
||
|
|
backendKey: "codex",
|
||
|
|
configuredCommand: "/opt/homebrew/bin/codex",
|
||
|
|
shellEnv: { PATH: "/usr/bin" },
|
||
|
|
env: {},
|
||
|
|
resolveCliFromPath: () => "/usr/bin/codex",
|
||
|
|
normalizeCliPathForPlatform: (value) => value,
|
||
|
|
resolveSdkBinPath: () => "/usr/bin/codex",
|
||
|
|
realpath: () => "/opt/homebrew/bin/codex",
|
||
|
|
});
|
||
|
|
assert.equal(out, "/opt/homebrew/bin/codex");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("resolveSdkBackendBinPath rejects invalid renderer-configured command paths", () => {
|
||
|
|
assert.throws(
|
||
|
|
() => resolveSdkBackendBinPath({
|
||
|
|
backendKey: "codex",
|
||
|
|
configuredCommand: "/missing/codex",
|
||
|
|
shellEnv: { PATH: "/usr/bin" },
|
||
|
|
env: {},
|
||
|
|
resolveCliFromPath: () => "/usr/bin/codex",
|
||
|
|
normalizeCliPathForPlatform: () => null,
|
||
|
|
resolveSdkBinPath: () => "/usr/bin/codex",
|
||
|
|
}),
|
||
|
|
/Agent CLI path not found: \/missing\/codex/,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("resolveSdkBackendBinPath applies Codex SDK normalization to configured command paths", () => {
|
||
|
|
const out = resolveSdkBackendBinPath({
|
||
|
|
backendKey: "codex",
|
||
|
|
configuredCommand: "C:\\Users\\me\\AppData\\Roaming\\npm\\codex.cmd",
|
||
|
|
shellEnv: { Path: "C:\\Windows\\System32" },
|
||
|
|
env: {},
|
||
|
|
resolveCliFromPath: () => "C:\\Windows\\System32\\codex.cmd",
|
||
|
|
normalizeCliPathForPlatform: (value) => value,
|
||
|
|
resolveCodexExecutableForSdk: (p) =>
|
||
|
|
p.endsWith("codex.cmd")
|
||
|
|
? "C:\\Users\\me\\AppData\\Roaming\\npm\\node_modules\\@openai\\codex-win32-x64\\vendor\\x86_64-pc-windows-msvc\\bin\\codex.exe"
|
||
|
|
: p,
|
||
|
|
realpath: (p) => p,
|
||
|
|
});
|
||
|
|
assert.equal(
|
||
|
|
out,
|
||
|
|
"C:\\Users\\me\\AppData\\Roaming\\npm\\node_modules\\@openai\\codex-win32-x64\\vendor\\x86_64-pc-windows-msvc\\bin\\codex.exe",
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("resolveSdkBackendBinPath applies CodeBuddy SDK normalization to configured command paths", () => {
|
||
|
|
const out = resolveSdkBackendBinPath({
|
||
|
|
backendKey: "codebuddy",
|
||
|
|
configuredCommand: "C:\\Users\\me\\AppData\\Roaming\\npm\\codebuddy.cmd",
|
||
|
|
shellEnv: { Path: "C:\\Windows\\System32" },
|
||
|
|
env: {},
|
||
|
|
resolveCliFromPath: () => "C:\\Windows\\System32\\codebuddy.cmd",
|
||
|
|
normalizeCliPathForPlatform: (value) => value,
|
||
|
|
resolveCodebuddyExecutableForSdk: (p) =>
|
||
|
|
p.endsWith("codebuddy.cmd")
|
||
|
|
? "C:\\Users\\me\\AppData\\Roaming\\npm\\node_modules\\@tencent-ai\\codebuddy-code\\bin\\codebuddy"
|
||
|
|
: p,
|
||
|
|
realpath: (p) => p,
|
||
|
|
});
|
||
|
|
assert.equal(
|
||
|
|
out,
|
||
|
|
"C:\\Users\\me\\AppData\\Roaming\\npm\\node_modules\\@tencent-ai\\codebuddy-code\\bin\\codebuddy",
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("resolveSdkBackendBinPath falls back to PATH when CodeBuddy path is invalid", () => {
|
||
|
|
const out = resolveSdkBackendBinPath({
|
||
|
|
backendKey: "codebuddy",
|
||
|
|
shellEnv: { PATH: "/usr/bin" },
|
||
|
|
env: { CODEBUDDY_CODE_PATH: "/missing/codebuddy" },
|
||
|
|
resolveCliFromPath: () => "/usr/bin/codebuddy",
|
||
|
|
normalizeCliPathForPlatform: () => null,
|
||
|
|
});
|
||
|
|
assert.equal(out, "/usr/bin/codebuddy");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("resolveSdkBackendBinPath realpaths CodeBuddy PATH discovery fallback", () => {
|
||
|
|
const out = resolveSdkBackendBinPath({
|
||
|
|
backendKey: "codebuddy",
|
||
|
|
shellEnv: { PATH: "/usr/bin" },
|
||
|
|
env: {},
|
||
|
|
resolveCliFromPath: () => "/shim/bin/codebuddy",
|
||
|
|
normalizeCliPathForPlatform: () => null,
|
||
|
|
realpath: () => "/opt/codebuddy/bin/codebuddy",
|
||
|
|
});
|
||
|
|
assert.equal(out, "/opt/codebuddy/bin/codebuddy");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("resolveSdkBackendBinPath resolves Windows CodeBuddy shim to the package JS entry", () => {
|
||
|
|
const out = resolveSdkBackendBinPath({
|
||
|
|
backendKey: "codebuddy",
|
||
|
|
shellEnv: { Path: "C:\\Users\\me\\AppData\\Roaming\\npm" },
|
||
|
|
env: {},
|
||
|
|
resolveCliFromPath: () => "C:\\Users\\me\\AppData\\Roaming\\npm\\codebuddy.cmd",
|
||
|
|
normalizeCliPathForPlatform: () => null,
|
||
|
|
realpath: (p) => p,
|
||
|
|
resolveCodebuddyExecutableForSdk: (p) =>
|
||
|
|
p.endsWith("codebuddy.cmd")
|
||
|
|
? "C:\\Users\\me\\AppData\\Roaming\\npm\\node_modules\\@tencent-ai\\codebuddy-code\\bin\\codebuddy"
|
||
|
|
: p,
|
||
|
|
});
|
||
|
|
assert.equal(
|
||
|
|
out,
|
||
|
|
"C:\\Users\\me\\AppData\\Roaming\\npm\\node_modules\\@tencent-ai\\codebuddy-code\\bin\\codebuddy",
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("resolveSdkBackendBinPath falls back to bundled CLI when Windows CodeBuddy shim is unresolvable", () => {
|
||
|
|
const out = resolveSdkBackendBinPath({
|
||
|
|
backendKey: "codebuddy",
|
||
|
|
shellEnv: { Path: "C:\\Users\\me\\AppData\\Roaming\\npm" },
|
||
|
|
env: {},
|
||
|
|
resolveCliFromPath: () => "C:\\Users\\me\\AppData\\Roaming\\npm\\codebuddy.cmd",
|
||
|
|
normalizeCliPathForPlatform: () => null,
|
||
|
|
realpath: (p) => p,
|
||
|
|
resolveCodebuddyExecutableForSdk: () => null,
|
||
|
|
});
|
||
|
|
assert.equal(out, undefined);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("resolveSdkBackendBinPath keeps non-CodeBuddy SDK path normalization", () => {
|
||
|
|
const out = resolveSdkBackendBinPath({
|
||
|
|
backendKey: "codex",
|
||
|
|
shellEnv: { PATH: "C:\\Users\\me\\AppData\\Roaming\\npm" },
|
||
|
|
env: {},
|
||
|
|
resolveCliFromPath: () => "C:\\Users\\me\\AppData\\Roaming\\npm\\codex.cmd",
|
||
|
|
resolveSdkBinPath: () => "C:\\Users\\me\\AppData\\Roaming\\npm\\node_modules\\@openai\\codex\\bin\\codex.js",
|
||
|
|
});
|
||
|
|
assert.equal(out, "C:\\Users\\me\\AppData\\Roaming\\npm\\node_modules\\@openai\\codex\\bin\\codex.js");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("resolveSdkBackendBinPath does not fall back to Windows shell shims for non-CodeBuddy", () => {
|
||
|
|
const out = resolveSdkBackendBinPath({
|
||
|
|
backendKey: "codex",
|
||
|
|
shellEnv: { PATH: "C:\\Users\\me\\AppData\\Roaming\\npm" },
|
||
|
|
env: {},
|
||
|
|
resolveCliFromPath: () => "C:\\Users\\me\\AppData\\Roaming\\npm\\codex.cmd",
|
||
|
|
resolveSdkBinPath: () => null,
|
||
|
|
});
|
||
|
|
assert.equal(out, undefined);
|
||
|
|
});
|