329 lines
9.0 KiB
TypeScript
329 lines
9.0 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
import { loadConfig } from "../src/config.js";
|
|
import {
|
|
parseCodexFinalResponse,
|
|
runCodexPrompt,
|
|
} from "../src/examples/codex.js";
|
|
import {
|
|
readClaudeResult,
|
|
runClaudePrompt,
|
|
} from "../src/examples/claude.js";
|
|
import type { SessionContext } from "../src/examples/session-context.js";
|
|
|
|
type ClaudeQueryFunction = (typeof import("@anthropic-ai/claude-agent-sdk"))["query"];
|
|
|
|
function createMessageStream(
|
|
messages: SDKMessage[],
|
|
): AsyncIterable<SDKMessage> {
|
|
return {
|
|
async *[Symbol.asyncIterator]() {
|
|
for (const message of messages) {
|
|
yield message;
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
test("runCodexPrompt wires client options and parses final output", async () => {
|
|
const config = loadConfig({
|
|
CODEX_API_KEY: "codex-token",
|
|
OPENAI_BASE_URL: "https://api.example.com/v1",
|
|
});
|
|
|
|
let capturedClientInput: Record<string, unknown> | undefined;
|
|
let capturedThreadInput:
|
|
| {
|
|
workingDirectory: string;
|
|
skipGitRepoCheck: boolean;
|
|
}
|
|
| undefined;
|
|
let capturedPrompt = "";
|
|
let closed = false;
|
|
let output = "";
|
|
|
|
const sessionContext: SessionContext = {
|
|
provider: "codex",
|
|
sessionId: "session-codex",
|
|
mcp: {
|
|
codexConfig: {
|
|
mcp_servers: {
|
|
local: {
|
|
command: "node",
|
|
args: ["server.js"],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
promptWithContext: "prompt with context",
|
|
runtimeInjection: {
|
|
workingDirectory: "/tmp/worktree",
|
|
env: {
|
|
PATH: "/usr/bin",
|
|
},
|
|
discoveryFilePath: "/tmp/worktree/.agent-context/resources.json",
|
|
},
|
|
runInSession: async <T>(run: () => Promise<T>) => run(),
|
|
close: async () => {
|
|
closed = true;
|
|
},
|
|
};
|
|
|
|
await runCodexPrompt("ignored", {
|
|
config,
|
|
createSessionContextFn: async () => sessionContext,
|
|
createCodexClient: (input) => {
|
|
capturedClientInput = input as Record<string, unknown>;
|
|
return {
|
|
startThread: (threadInput) => {
|
|
capturedThreadInput = threadInput;
|
|
return {
|
|
run: async (prompt) => {
|
|
capturedPrompt = prompt;
|
|
return {
|
|
finalResponse: " completed ",
|
|
};
|
|
},
|
|
};
|
|
},
|
|
};
|
|
},
|
|
writeOutput: (line) => {
|
|
output = line;
|
|
},
|
|
});
|
|
|
|
assert.equal(capturedClientInput?.["apiKey"], "codex-token");
|
|
assert.equal(capturedClientInput?.["baseUrl"], "https://api.example.com/v1");
|
|
assert.deepEqual(capturedClientInput?.["config"], sessionContext.mcp.codexConfig);
|
|
assert.deepEqual(capturedClientInput?.["env"], sessionContext.runtimeInjection.env);
|
|
assert.deepEqual(capturedThreadInput, {
|
|
workingDirectory: "/tmp/worktree",
|
|
skipGitRepoCheck: true,
|
|
});
|
|
assert.equal(capturedPrompt, "prompt with context");
|
|
assert.equal(output, "completed");
|
|
assert.equal(closed, true);
|
|
});
|
|
|
|
test("runCodexPrompt omits apiKey when OPENAI_AUTH_MODE=chatgpt", async () => {
|
|
const config = loadConfig({
|
|
OPENAI_AUTH_MODE: "chatgpt",
|
|
CODEX_API_KEY: "codex-token",
|
|
OPENAI_API_KEY: "openai-token",
|
|
OPENAI_BASE_URL: "https://api.example.com/v1",
|
|
});
|
|
|
|
let capturedClientInput: Record<string, unknown> | undefined;
|
|
|
|
const sessionContext: SessionContext = {
|
|
provider: "codex",
|
|
sessionId: "session-codex-chatgpt",
|
|
mcp: {},
|
|
promptWithContext: "prompt with context",
|
|
runtimeInjection: {
|
|
workingDirectory: "/tmp/worktree",
|
|
env: {
|
|
HOME: "/home/tester",
|
|
},
|
|
discoveryFilePath: "/tmp/worktree/.agent-context/resources.json",
|
|
},
|
|
runInSession: async <T>(run: () => Promise<T>) => run(),
|
|
close: async () => {},
|
|
};
|
|
|
|
await runCodexPrompt("ignored", {
|
|
config,
|
|
createSessionContextFn: async () => sessionContext,
|
|
createCodexClient: (input) => {
|
|
capturedClientInput = input as Record<string, unknown>;
|
|
return {
|
|
startThread: () => ({
|
|
run: async () => ({
|
|
finalResponse: "ok",
|
|
}),
|
|
}),
|
|
};
|
|
},
|
|
writeOutput: () => {},
|
|
});
|
|
|
|
assert.equal(capturedClientInput?.["apiKey"], undefined);
|
|
assert.equal(capturedClientInput?.["baseUrl"], "https://api.example.com/v1");
|
|
assert.deepEqual(capturedClientInput?.["env"], sessionContext.runtimeInjection.env);
|
|
});
|
|
|
|
test("runClaudePrompt wires auth env, stream parsing, and output", async () => {
|
|
const config = loadConfig({
|
|
CLAUDE_CODE_OAUTH_TOKEN: "oauth-token",
|
|
ANTHROPIC_API_KEY: "legacy-api-key",
|
|
CLAUDE_MODEL: "claude-sonnet-4-6",
|
|
CLAUDE_CODE_PATH: "/usr/local/bin/claude",
|
|
CLAUDE_MAX_TURNS: "5",
|
|
});
|
|
|
|
let closed = false;
|
|
let output = "";
|
|
let queryInput:
|
|
| {
|
|
prompt: string;
|
|
options?: Record<string, unknown>;
|
|
}
|
|
| undefined;
|
|
|
|
const sessionContext: SessionContext = {
|
|
provider: "claude",
|
|
sessionId: "session-claude",
|
|
mcp: {
|
|
claudeMcpServers: {
|
|
local: {
|
|
type: "stdio",
|
|
command: "node",
|
|
args: ["server.js"],
|
|
},
|
|
},
|
|
},
|
|
promptWithContext: "augmented prompt",
|
|
runtimeInjection: {
|
|
workingDirectory: "/tmp/claude-worktree",
|
|
env: {
|
|
PATH: "/usr/bin",
|
|
ANTHROPIC_API_KEY: "ambient-api-key",
|
|
},
|
|
discoveryFilePath: "/tmp/claude-worktree/.agent-context/resources.json",
|
|
},
|
|
runInSession: async <T>(run: () => Promise<T>) => run(),
|
|
close: async () => {
|
|
closed = true;
|
|
},
|
|
};
|
|
|
|
const queryFn: ClaudeQueryFunction = ((input: {
|
|
prompt: string;
|
|
options?: Record<string, unknown>;
|
|
}) => {
|
|
queryInput = input;
|
|
const stream = createMessageStream([
|
|
{
|
|
type: "result",
|
|
subtype: "success",
|
|
result: " response text ",
|
|
} as SDKMessage,
|
|
]);
|
|
|
|
return {
|
|
...stream,
|
|
close: () => {},
|
|
} as ReturnType<ClaudeQueryFunction>;
|
|
}) as ClaudeQueryFunction;
|
|
|
|
await runClaudePrompt("ignored", {
|
|
config,
|
|
createSessionContextFn: async () => sessionContext,
|
|
queryFn,
|
|
writeOutput: (line) => {
|
|
output = line;
|
|
},
|
|
});
|
|
|
|
assert.equal(queryInput?.prompt, "augmented prompt");
|
|
assert.equal(queryInput?.options?.model, "claude-sonnet-4-6");
|
|
assert.equal(queryInput?.options?.pathToClaudeCodeExecutable, "/usr/local/bin/claude");
|
|
assert.equal(queryInput?.options?.maxTurns, 5);
|
|
assert.equal(queryInput?.options?.cwd, "/tmp/claude-worktree");
|
|
assert.equal(queryInput?.options?.authToken, "oauth-token");
|
|
assert.deepEqual(queryInput?.options?.mcpServers, sessionContext.mcp.claudeMcpServers);
|
|
|
|
const env = queryInput?.options?.env as Record<string, string | undefined> | undefined;
|
|
assert.equal(env?.CLAUDE_CODE_OAUTH_TOKEN, "oauth-token");
|
|
assert.equal(env?.ANTHROPIC_API_KEY, undefined);
|
|
assert.equal(output, "response text");
|
|
assert.equal(closed, true);
|
|
});
|
|
|
|
test("runClaudePrompt uses ambient Claude login when no token env is configured", async () => {
|
|
const config = loadConfig({});
|
|
|
|
let queryInput:
|
|
| {
|
|
prompt: string;
|
|
options?: Record<string, unknown>;
|
|
}
|
|
| undefined;
|
|
|
|
const sessionContext: SessionContext = {
|
|
provider: "claude",
|
|
sessionId: "session-claude-no-key",
|
|
mcp: {},
|
|
promptWithContext: "augmented prompt",
|
|
runtimeInjection: {
|
|
workingDirectory: "/tmp/claude-worktree",
|
|
env: {
|
|
HOME: "/home/tester",
|
|
PATH: "/usr/bin",
|
|
},
|
|
discoveryFilePath: "/tmp/claude-worktree/.agent-context/resources.json",
|
|
},
|
|
runInSession: async <T>(run: () => Promise<T>) => run(),
|
|
close: async () => {},
|
|
};
|
|
|
|
const queryFn: ClaudeQueryFunction = ((input: {
|
|
prompt: string;
|
|
options?: Record<string, unknown>;
|
|
}) => {
|
|
queryInput = input;
|
|
const stream = createMessageStream([
|
|
{
|
|
type: "result",
|
|
subtype: "success",
|
|
result: "ok",
|
|
} as SDKMessage,
|
|
]);
|
|
|
|
return {
|
|
...stream,
|
|
close: () => {},
|
|
} as ReturnType<ClaudeQueryFunction>;
|
|
}) as ClaudeQueryFunction;
|
|
|
|
await runClaudePrompt("ignored", {
|
|
config,
|
|
createSessionContextFn: async () => sessionContext,
|
|
queryFn,
|
|
writeOutput: () => {},
|
|
});
|
|
|
|
assert.equal(queryInput?.options?.apiKey, undefined);
|
|
assert.equal(queryInput?.options?.authToken, undefined);
|
|
|
|
const env = queryInput?.options?.env as Record<string, string | undefined> | undefined;
|
|
assert.equal(env?.HOME, "/home/tester");
|
|
assert.equal(env?.CLAUDE_CODE_OAUTH_TOKEN, undefined);
|
|
assert.equal(env?.ANTHROPIC_API_KEY, undefined);
|
|
});
|
|
|
|
test("readClaudeResult throws on non-success result events", async () => {
|
|
const stream = createMessageStream([
|
|
{
|
|
type: "result",
|
|
subtype: "error_during_execution",
|
|
errors: ["first", "second"],
|
|
} as SDKMessage,
|
|
]);
|
|
|
|
await assert.rejects(
|
|
() => readClaudeResult(stream),
|
|
/Claude query failed \(error_during_execution\): first; second/,
|
|
);
|
|
});
|
|
|
|
test("parseCodexFinalResponse keeps fallback text for empty responses", () => {
|
|
const response = parseCodexFinalResponse({
|
|
finalResponse: " ",
|
|
});
|
|
|
|
assert.equal(response, "(No response text returned)");
|
|
});
|