Harden MCP schema and wire Claude OAuth/token handling
This commit is contained in:
217
tests/provider-adapters.test.ts
Normal file
217
tests/provider-adapters.test.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
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("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",
|
||||
});
|
||||
|
||||
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?.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("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)");
|
||||
});
|
||||
Reference in New Issue
Block a user