test(mitm): handlers + targets + detection unit tests (F3)

- mitm-handler-base: hookBufferStart returns InterceptedRequest with
  sanitized headers, extractSourceModel parses body.model, writeError
  emits JSON with sanitized message (Hard Rule #12).
- mitm-handler-<id>: nine per-agent happy-path tests (antigravity,
  kiro, copilot, codex, cursor, zed, claude-code, open-code) exercise
  full intercept() with mocked fetch via _mitmHandlerHarness.ts;
  asserts mapped model is forwarded and AgentBridge correlation
  headers are present. Trae test confirms intercept() rejects with a
  structured error.
- mitm-targets-resolve: ALL_TARGETS contains 9 entries; resolveTarget
  is case-insensitive and returns null for unknown hosts.
- mitm-targets-route: bypass > target > passthrough precedence
  validated against representative hostnames.
- mitm-detection: DETECTORS covers every AgentId; detectAgent never
  throws and returns DetectionResult-shaped objects for all probes.
This commit is contained in:
diegosouzapw
2026-05-27 22:33:56 -03:00
parent a0e9535769
commit 1196d08aad
14 changed files with 554 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
/**
* Test harness for MitmHandlerBase subclasses.
*
* Mocks `globalThis.fetch` so handlers exercise their full intercept() path
* (router round-trip + SSE pipe) without touching the network. Returns the
* captured payload, response chunks written to the fake ServerResponse, and
* the final status code.
*/
import type { IncomingMessage, ServerResponse } from "node:http";
import { Readable } from "node:stream";
import type { MitmHandlerBase } from "../../src/mitm/handlers/base.ts";
export interface HarnessResult {
fetchCalled: boolean;
fetchUrl: string | null;
fetchHeaders: Record<string, string>;
fetchBody: string;
status: number;
responseChunks: string[];
}
function fakeReq(headers: Record<string, string> = {}): IncomingMessage {
return {
method: "POST",
url: "/v1/chat/completions",
headers: {
host: "api.example.com",
"user-agent": "ut",
...headers,
},
} as unknown as IncomingMessage;
}
function fakeRes(): { res: ServerResponse; out: HarnessResult } {
const out: HarnessResult = {
fetchCalled: false,
fetchUrl: null,
fetchHeaders: {},
fetchBody: "",
status: 0,
responseChunks: [],
};
let headersSent = false;
const res = {
get headersSent() {
return headersSent;
},
writeHead(s: number) {
out.status = s;
headersSent = true;
},
write(c: Buffer | string) {
out.responseChunks.push(typeof c === "string" ? c : c.toString());
return true;
},
end(c?: Buffer | string) {
if (c) out.responseChunks.push(typeof c === "string" ? c : c.toString());
},
} as unknown as ServerResponse;
return { res, out };
}
export async function runHandler(
handler: MitmHandlerBase,
body: unknown,
mappedModel: string,
opts: {
upstreamStatus?: number;
upstreamBody?: string;
headers?: Record<string, string>;
} = {}
): Promise<HarnessResult> {
const { res, out } = fakeRes();
const req = fakeReq(opts.headers);
const buf = Buffer.from(typeof body === "string" ? body : JSON.stringify(body));
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (url: string, init: RequestInit) => {
out.fetchCalled = true;
out.fetchUrl = String(url);
out.fetchHeaders = (init?.headers ?? {}) as Record<string, string>;
out.fetchBody = typeof init?.body === "string" ? init.body : "";
const upstreamBody = opts.upstreamBody ?? "data: hello\n\n";
const status = opts.upstreamStatus ?? 200;
const stream = Readable.toWeb(
Readable.from(Buffer.from(upstreamBody))
) as unknown as ReadableStream<Uint8Array>;
return new Response(stream, { status });
}) as unknown as typeof fetch;
try {
await handler.intercept(req, res, buf, mappedModel);
} finally {
globalThis.fetch = originalFetch;
}
return out;
}

View File

@@ -0,0 +1,77 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import { detectAgent, DETECTORS } from "../../src/mitm/detection/index.ts";
import type { AgentId } from "../../src/mitm/types.ts";
test("DETECTORS — provides an entry for every AgentId", () => {
const ids: AgentId[] = [
"antigravity",
"kiro",
"copilot",
"codex",
"cursor",
"zed",
"claude-code",
"open-code",
"trae",
];
for (const id of ids) {
assert.equal(typeof DETECTORS[id], "function", `missing detector for ${id}`);
}
});
test("detectAgent — trae always reports not installed (investigating)", () => {
const r = detectAgent("trae");
assert.equal(r.installed, false);
});
test("detectAgent — returns installed=true when fs.existsSync hits a known path", () => {
// Inject a mock existsSync that returns true for every probed path so the
// dispatch path is exercised regardless of host environment.
const original = fs.existsSync;
let called = 0;
(fs as unknown as { existsSync: (p: fs.PathLike) => boolean }).existsSync = () => {
called++;
return true;
};
try {
// antigravity uses pure existsSync probes — first hit wins.
const r = detectAgent("antigravity");
assert.equal(r.installed, true);
assert.equal(typeof r.path, "string");
} finally {
(fs as unknown as { existsSync: typeof fs.existsSync }).existsSync = original;
}
assert.ok(called >= 1);
});
test("detectAgent — antigravity probe returns DetectionResult shape", () => {
const r = detectAgent("antigravity");
assert.equal(typeof r.installed, "boolean");
if (r.installed) assert.equal(typeof r.path, "string");
});
test("detectAgent — gracefully handles thrown detectors", () => {
// Unknown id falls through to default false branch.
const r = detectAgent("nonexistent" as AgentId);
assert.equal(r.installed, false);
});
test("detectAgent — runs all detectors without throwing", () => {
const ids: AgentId[] = [
"antigravity",
"kiro",
"copilot",
"codex",
"cursor",
"zed",
"claude-code",
"open-code",
"trae",
];
for (const id of ids) {
const r = detectAgent(id);
assert.equal(typeof r.installed, "boolean");
}
});

View File

@@ -0,0 +1,29 @@
import test from "node:test";
import assert from "node:assert/strict";
import { AntigravityHandler } from "../../src/mitm/handlers/antigravity.ts";
import { runHandler } from "./_mitmHandlerHarness.ts";
test("antigravity handler — forwards to OmniRoute and pipes SSE", async () => {
const r = await runHandler(
new AntigravityHandler(),
{ model: "gpt-4o", messages: [{ role: "user", content: "hi" }] },
"claude-3.5-sonnet",
{ upstreamBody: "data: hello\n\ndata: world\n\n" }
);
assert.ok(r.fetchCalled);
assert.equal(r.status, 200);
assert.ok(r.responseChunks.join("").includes("hello"));
});
test("antigravity handler — propagates upstream failure as 500", async () => {
const r = await runHandler(
new AntigravityHandler(),
{ model: "gpt-4o" },
"claude-3.5-sonnet",
{ upstreamStatus: 500, upstreamBody: "boom" }
);
assert.equal(r.status, 500);
const body = r.responseChunks.join("");
// Error must NOT include raw stack trace (Hard Rule #12 sanitization).
assert.ok(!body.includes("at /"));
});

View File

@@ -0,0 +1,115 @@
import test from "node:test";
import assert from "node:assert/strict";
import type { IncomingMessage, ServerResponse } from "node:http";
import type { AgentId } from "../../src/mitm/types.ts";
import { MitmHandlerBase } from "../../src/mitm/handlers/base.ts";
// Concrete subclass exposing the protected helpers for testing.
class TestHandler extends MitmHandlerBase {
readonly agentId: AgentId = "antigravity";
async intercept(): Promise<void> {
// Not exercised in this suite.
}
publicExtract(buf: Buffer): string | null {
return this.extractSourceModel(buf);
}
async publicHookStart(
req: IncomingMessage,
body: Buffer,
mapped: string
): Promise<ReturnType<MitmHandlerBase["hookBufferStart"]>> {
return this.hookBufferStart(req, body, mapped);
}
}
function fakeReq(headers: Record<string, string> = {}): IncomingMessage {
return {
method: "POST",
url: "/v1/chat/completions",
headers: {
host: "api.example.com",
"user-agent": "ut",
// 50-char opaque token — long enough to be masked by LONG_TOKEN rule.
authorization: "Bearer abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKL",
...headers,
},
} as unknown as IncomingMessage;
}
test("base.extractSourceModel — reads body.model from JSON", () => {
const h = new TestHandler();
const buf = Buffer.from(JSON.stringify({ model: "gpt-4o", messages: [] }));
assert.equal(h.publicExtract(buf), "gpt-4o");
});
test("base.extractSourceModel — non-JSON body returns null", () => {
const h = new TestHandler();
assert.equal(h.publicExtract(Buffer.from("not json")), null);
});
test("base.extractSourceModel — missing model field returns null", () => {
const h = new TestHandler();
const buf = Buffer.from(JSON.stringify({ messages: [] }));
assert.equal(h.publicExtract(buf), null);
});
test("base.hookBufferStart — local stub returns InterceptedRequest with sanitized headers", async () => {
const h = new TestHandler();
const req = fakeReq();
const body = Buffer.from(JSON.stringify({ model: "gpt-4o" }));
const r = await h.publicHookStart(req, body, "claude-3.5-sonnet");
assert.equal(r.agent, "antigravity");
assert.equal(r.source, "agent-bridge");
assert.equal(r.mappedModel, "claude-3.5-sonnet");
assert.equal(r.sourceModel, "gpt-4o");
assert.equal(r.host, "api.example.com");
assert.equal(r.status, "in-flight");
// sanitizeHeaders should mask the long opaque token in `authorization`.
const auth = r.requestHeaders["authorization"];
assert.ok(
!auth ||
(typeof auth === "string" &&
!auth.includes("abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKL")),
`sanitizeHeaders failed to mask authorization: ${JSON.stringify(auth)}`
);
});
test("base.hookBufferStart — body is captured (default shouldCaptureBody=true)", async () => {
const h = new TestHandler();
const req = fakeReq();
const body = Buffer.from(JSON.stringify({ model: "gpt-4o" }));
const r = await h.publicHookStart(req, body, "x");
assert.equal(r.requestSize, body.length);
assert.ok(typeof r.requestBody === "string");
});
test("base.writeError — writes sanitized JSON error body", async () => {
const h = new TestHandler();
let status = 0;
let payload = "";
const res = {
headersSent: false,
writeHead(s: number) {
status = s;
},
end(p: string) {
payload = p;
},
} as unknown as ServerResponse;
// Calling a protected method through `any` to avoid leaking it on
// the production surface area.
await (h as unknown as { writeError: MitmHandlerBase["writeError"] }).writeError(
res,
new Error("boom"),
502
);
assert.equal(status, 502);
const obj = JSON.parse(payload);
assert.equal(obj.error.type, "mitm_error");
assert.ok(typeof obj.error.message === "string");
});

View File

@@ -0,0 +1,17 @@
import test from "node:test";
import assert from "node:assert/strict";
import { ClaudeCodeHandler } from "../../src/mitm/handlers/claudeCode.ts";
import { runHandler } from "./_mitmHandlerHarness.ts";
test("claude-code handler — happy path forwards to /v1/messages", async () => {
const r = await runHandler(
new ClaudeCodeHandler(),
{ model: "claude-3.5-sonnet", messages: [] },
"claude-opus-4.5"
);
assert.ok(r.fetchCalled);
assert.equal(r.status, 200);
assert.ok(r.fetchUrl?.endsWith("/v1/messages"));
const sent = JSON.parse(r.fetchBody);
assert.equal(sent.model, "claude-opus-4.5");
});

View File

@@ -0,0 +1,17 @@
import test from "node:test";
import assert from "node:assert/strict";
import { CodexHandler } from "../../src/mitm/handlers/codex.ts";
import { runHandler } from "./_mitmHandlerHarness.ts";
test("codex handler — forwards Chat Completions payload via OmniRoute", async () => {
const r = await runHandler(
new CodexHandler(),
{ model: "gpt-4.1", messages: [] },
"gpt-4o-mini"
);
assert.ok(r.fetchCalled);
assert.equal(r.status, 200);
assert.ok(r.fetchUrl?.endsWith("/v1/chat/completions"));
const sent = JSON.parse(r.fetchBody);
assert.equal(sent.model, "gpt-4o-mini");
});

View File

@@ -0,0 +1,21 @@
import test from "node:test";
import assert from "node:assert/strict";
import { CopilotHandler } from "../../src/mitm/handlers/copilot.ts";
import { runHandler } from "./_mitmHandlerHarness.ts";
test("copilot handler — rewrites model and forwards to /v1/chat/completions", async () => {
const r = await runHandler(
new CopilotHandler(),
{ model: "gpt-4o", messages: [] },
"claude-3.5-sonnet"
);
assert.ok(r.fetchCalled);
assert.equal(r.status, 200);
assert.ok(r.fetchUrl?.endsWith("/v1/chat/completions"));
const sent = JSON.parse(r.fetchBody);
assert.equal(sent.model, "claude-3.5-sonnet");
// AgentBridge correlation headers must be present.
const headers = r.fetchHeaders as Record<string, string>;
assert.equal(headers["x-omniroute-source"], "agent-bridge");
assert.equal(headers["x-omniroute-agent"], "copilot");
});

View File

@@ -0,0 +1,16 @@
import test from "node:test";
import assert from "node:assert/strict";
import { CursorHandler } from "../../src/mitm/handlers/cursor.ts";
import { runHandler } from "./_mitmHandlerHarness.ts";
test("cursor handler — happy path forwards mapped model", async () => {
const r = await runHandler(
new CursorHandler(),
{ model: "claude-sonnet-4.5", messages: [] },
"gpt-4o"
);
assert.ok(r.fetchCalled);
assert.equal(r.status, 200);
const sent = JSON.parse(r.fetchBody);
assert.equal(sent.model, "gpt-4o");
});

View File

@@ -0,0 +1,20 @@
import test from "node:test";
import assert from "node:assert/strict";
import { KiroHandler } from "../../src/mitm/handlers/kiro.ts";
import { runHandler } from "./_mitmHandlerHarness.ts";
test("kiro handler — forwards Anthropic-style body to OmniRoute /v1/messages", async () => {
const r = await runHandler(
new KiroHandler(),
{ model: "claude-3.5-sonnet", messages: [{ role: "user", content: "hi" }] },
"claude-sonnet-4.5",
{ upstreamBody: "event: message_start\n\n" }
);
assert.ok(r.fetchCalled);
assert.equal(r.status, 200);
// Router URL must point at /v1/messages for the Anthropic path.
assert.ok(r.fetchUrl?.endsWith("/v1/messages"));
// Body must have been rewritten with mapped model.
const sent = JSON.parse(r.fetchBody);
assert.equal(sent.model, "claude-sonnet-4.5");
});

View File

@@ -0,0 +1,16 @@
import test from "node:test";
import assert from "node:assert/strict";
import { OpenCodeHandler } from "../../src/mitm/handlers/openCode.ts";
import { runHandler } from "./_mitmHandlerHarness.ts";
test("open-code handler — happy path forwards Chat Completions payload", async () => {
const r = await runHandler(
new OpenCodeHandler(),
{ model: "gpt-4o", messages: [] },
"claude-3.5-sonnet"
);
assert.ok(r.fetchCalled);
assert.equal(r.status, 200);
const sent = JSON.parse(r.fetchBody);
assert.equal(sent.model, "claude-3.5-sonnet");
});

View File

@@ -0,0 +1,23 @@
import test from "node:test";
import assert from "node:assert/strict";
import type { IncomingMessage, ServerResponse } from "node:http";
import { TraeHandler } from "../../src/mitm/handlers/trae.ts";
test("trae handler — intercept throws structured error (viability=investigating)", async () => {
const h = new TraeHandler();
const req = {
method: "POST",
url: "/x",
headers: { host: "trae.invalid" },
} as unknown as IncomingMessage;
const res = {
headersSent: false,
writeHead() {},
end() {},
} as unknown as ServerResponse;
await assert.rejects(
() => h.intercept(req, res, Buffer.from("{}"), "gpt-4o"),
/investigation|invalid|not.*implement/i
);
});

View File

@@ -0,0 +1,16 @@
import test from "node:test";
import assert from "node:assert/strict";
import { ZedHandler } from "../../src/mitm/handlers/zed.ts";
import { runHandler } from "./_mitmHandlerHarness.ts";
test("zed handler — happy path forwards Chat Completions payload", async () => {
const r = await runHandler(
new ZedHandler(),
{ model: "gpt-4o", messages: [] },
"claude-3.5-sonnet"
);
assert.ok(r.fetchCalled);
assert.equal(r.status, 200);
const sent = JSON.parse(r.fetchBody);
assert.equal(sent.model, "claude-3.5-sonnet");
});

View File

@@ -0,0 +1,53 @@
import test from "node:test";
import assert from "node:assert/strict";
import { ALL_TARGETS, resolveTarget } from "../../src/mitm/targets/index.ts";
test("resolveTarget — antigravity host resolves to antigravity target", () => {
const t = resolveTarget("cloudcode-pa.googleapis.com");
assert.ok(t);
assert.equal(t?.id, "antigravity");
});
test("resolveTarget — kiro host resolves to kiro target (Anthropic)", () => {
// api.anthropic.com is shared by kiro and claude-code; the first match wins.
const t = resolveTarget("api.anthropic.com");
assert.ok(t);
assert.ok(t?.id === "kiro" || t?.id === "claude-code");
});
test("resolveTarget — copilot host resolves", () => {
assert.equal(resolveTarget("api.githubcopilot.com")?.id, "copilot");
});
test("resolveTarget — cursor host resolves", () => {
assert.equal(resolveTarget("api2.cursor.sh")?.id, "cursor");
});
test("resolveTarget — case-insensitive match", () => {
assert.equal(resolveTarget("API.ZED.DEV")?.id, "zed");
});
test("resolveTarget — unknown host returns null", () => {
assert.equal(resolveTarget("example.com"), null);
assert.equal(resolveTarget(""), null);
});
test("ALL_TARGETS — registers exactly nine targets", () => {
assert.equal(ALL_TARGETS.length, 9);
const ids = new Set(ALL_TARGETS.map((t) => t.id));
assert.equal(ids.size, 9);
assert.ok(ids.has("antigravity"));
assert.ok(ids.has("kiro"));
assert.ok(ids.has("copilot"));
assert.ok(ids.has("codex"));
assert.ok(ids.has("cursor"));
assert.ok(ids.has("zed"));
assert.ok(ids.has("claude-code"));
assert.ok(ids.has("open-code"));
assert.ok(ids.has("trae"));
});
test("ALL_TARGETS — trae is marked viability=investigating", () => {
const trae = ALL_TARGETS.find((t) => t.id === "trae");
assert.equal(trae?.viability, "investigating");
});

View File

@@ -0,0 +1,37 @@
import test from "node:test";
import assert from "node:assert/strict";
import { routeConnection } from "../../src/mitm/targets/index.ts";
test("routeConnection — default bypass (bank) wins over target match", () => {
const r = routeConnection("my.bank.example", []);
assert.equal(r.kind, "bypass");
});
test("routeConnection — user bypass glob beats target", () => {
const r = routeConnection("api.githubcopilot.com", ["*githubcopilot*"]);
assert.equal(r.kind, "bypass");
});
test("routeConnection — known target returns target route", () => {
const r = routeConnection("api.githubcopilot.com", []);
assert.equal(r.kind, "target");
if (r.kind === "target") assert.equal(r.target.id, "copilot");
});
test("routeConnection — unknown host returns passthrough", () => {
const r = routeConnection("example.com", []);
assert.equal(r.kind, "passthrough");
});
test("routeConnection — empty hostname returns passthrough", () => {
const r = routeConnection("", []);
assert.equal(r.kind, "passthrough");
});
test("routeConnection — precedence: bypass > target > passthrough", () => {
// Default bypass (bank) takes precedence even if we add the host to the
// copilot target hypothetically — here we just exercise the three branches.
assert.equal(routeConnection("acme.bank.com", []).kind, "bypass");
assert.equal(routeConnection("api.zed.dev", []).kind, "target");
assert.equal(routeConnection("unrelated.example", []).kind, "passthrough");
});