// Real-transport regression proof for GitHub Copilot embedding error redaction. // Drives the production discovery + embeddings paths against a loopback HTTP // server with NO ssrf-runtime or global fetch mocks, so redactSensitiveText is // exercised end to end over real sockets rather than synthetic stubs. import fs from "node:fs"; import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; import type { AddressInfo } from "node:net"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const resolveFirstGithubTokenMock = vi.hoisted(() => vi.fn()); const resolveCopilotRuntimeAuthMock = vi.hoisted(() => vi.fn()); vi.mock("./auth.js", () => ({ resolveFirstGithubToken: resolveFirstGithubTokenMock, })); vi.mock("./runtime-auth.js", () => ({ DEFAULT_COPILOT_API_BASE_URL: "https://api.githubcopilot.test", resolveCopilotRuntimeAuth: resolveCopilotRuntimeAuthMock, })); // Intentionally NOT mocked: openclaw/plugin-sdk/ssrf-runtime, global fetch, and // openclaw/plugin-sdk/logging-core (redactSensitiveText is the unit under proof). import { githubCopilotMemoryEmbeddingProviderAdapter } from "./embeddings.js"; type CopilotServer = { baseUrl: string; requests: Array<{ method: string | undefined; url: string | undefined }>; }; const servers: Array<{ close: () => Promise }> = []; const DISCOVERY_MODELS_BODY = JSON.stringify({ data: [{ id: "text-embedding-3-small", supported_endpoints: ["/v1/embeddings"] }], }); async function startCopilotServer(handle: { models: { status: number; body: string }; embeddings?: { status: number; body: string }; }): Promise { const requests: CopilotServer["requests"] = []; const server = createServer((req: IncomingMessage, res: ServerResponse) => { void (async () => { // Drain the request body so keep-alive sockets close cleanly. await new Promise((resolve) => { req.resume(); req.on("end", resolve); }); requests.push({ method: req.method, url: req.url }); const isEmbeddings = req.method === "POST" && req.url === "/embeddings"; const route = isEmbeddings && handle.embeddings ? handle.embeddings : handle.models; res.writeHead(route.status, { "content-type": "application/json" }); res.end(route.body); })(); }); await new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", () => { server.off("error", reject); resolve(); }); }); servers.push({ close: () => new Promise((resolve, reject) => { server.close((error) => (error ? reject(error) : resolve())); }), }); const address = server.address() as AddressInfo; return { baseUrl: `http://127.0.0.1:${address.port}`, requests }; } function pointTokenAt(baseUrl: string): void { resolveCopilotRuntimeAuthMock.mockResolvedValue({ apiKey: "copilot_test_token_abc", source: "test", baseUrl, }); } function defaultCreateOptions() { return { config: {} as Record, agentDir: "/tmp/test-agent", model: "", }; } // Points the global logging-config reader at an on-disk config with sensitive // redaction turned off, so the test proves the error paths force masking rather // than inheriting the operator's `logging.redactSensitive` preference. function withRedactionDisabledConfig(): () => void { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "copilot-redact-off-")); const configPath = path.join(dir, "openclaw.json"); fs.writeFileSync(configPath, JSON.stringify({ logging: { redactSensitive: "off" } })); const previous = process.env.OPENCLAW_CONFIG_PATH; process.env.OPENCLAW_CONFIG_PATH = configPath; return () => { if (previous === undefined) { delete process.env.OPENCLAW_CONFIG_PATH; } else { process.env.OPENCLAW_CONFIG_PATH = previous; } fs.rmSync(dir, { recursive: true, force: true }); }; } describe("githubCopilotMemoryEmbeddingProviderAdapter real transport", () => { beforeEach(() => { resolveFirstGithubTokenMock.mockResolvedValue({ githubToken: "gh_test_token_123", hasProfile: false, }); }); afterEach(async () => { const pending = servers.splice(0); await Promise.all(pending.map((server) => server.close())); resolveFirstGithubTokenMock.mockReset(); resolveCopilotRuntimeAuthMock.mockReset(); }); it("redacts credential-shaped text in model discovery errors over real transport", async () => { const server = await startCopilotServer({ models: { status: 401, body: '{"error":{"message":"authentication failed"},"access_token":"ghu_AAAAUNIQUESECRETXXXX111122223333"}', }, }); pointTokenAt(server.baseUrl); let caught: Error | undefined; try { await githubCopilotMemoryEmbeddingProviderAdapter.create(defaultCreateOptions()); } catch (error) { caught = error as Error; } expect(server.requests).toEqual([{ method: "GET", url: "/models" }]); expect(caught?.message).toContain("GitHub Copilot model discovery HTTP 401"); expect(caught?.message).toContain("authentication failed"); expect(caught?.message).not.toContain("ghu_AAAAUNIQUESECRETXXXX111122223333"); expect(caught?.message).not.toContain("UNIQUESECRET"); }); it("redacts credential-shaped text in embeddings errors over real transport", async () => { const server = await startCopilotServer({ models: { status: 200, body: DISCOVERY_MODELS_BODY }, embeddings: { status: 429, body: '{"error":{"message":"rate limit exceeded"},"access_token":"gho_BBBBUNIQUEEMBSECRETYYYY444455556666"}', }, }); pointTokenAt(server.baseUrl); const result = await githubCopilotMemoryEmbeddingProviderAdapter.create(defaultCreateOptions()); let caught: Error | undefined; try { await result.provider?.embedQuery("hello"); } catch (error) { caught = error as Error; } expect(server.requests).toEqual([ { method: "GET", url: "/models" }, { method: "POST", url: "/embeddings" }, ]); expect(caught?.message).toContain("GitHub Copilot embeddings HTTP 429"); expect(caught?.message).toContain("rate limit exceeded"); expect(caught?.message).not.toContain("gho_BBBBUNIQUEEMBSECRETYYYY444455556666"); expect(caught?.message).not.toContain("UNIQUEEMBSECRET"); }); it("still redacts when logging.redactSensitive is off", async () => { const restoreConfig = withRedactionDisabledConfig(); try { const server = await startCopilotServer({ models: { status: 403, body: '{"error":{"message":"forbidden"},"access_token":"ghu_CCCCUNIQUEOFFSECRETZZZZ777788889999"}', }, }); pointTokenAt(server.baseUrl); let caught: Error | undefined; try { await githubCopilotMemoryEmbeddingProviderAdapter.create(defaultCreateOptions()); } catch (error) { caught = error as Error; } expect(caught?.message).toContain("GitHub Copilot model discovery HTTP 403"); expect(caught?.message).toContain("forbidden"); // Forced `tools` mode must mask the token even though on-disk config // disables general log redaction; a config-honoring call would leak it. expect(caught?.message).not.toContain("ghu_CCCCUNIQUEOFFSECRETZZZZ777788889999"); expect(caught?.message).not.toContain("UNIQUEOFFSECRET"); } finally { restoreConfig(); } }); it("returns embedding vectors on a successful response over real transport", async () => { const server = await startCopilotServer({ models: { status: 200, body: DISCOVERY_MODELS_BODY }, embeddings: { status: 200, body: JSON.stringify({ data: [{ index: 0, embedding: [0.1, 0.2, 0.3] }] }), }, }); pointTokenAt(server.baseUrl); const result = await githubCopilotMemoryEmbeddingProviderAdapter.create(defaultCreateOptions()); const vector = await result.provider?.embedQuery("hello"); expect(server.requests).toEqual([ { method: "GET", url: "/models" }, { method: "POST", url: "/embeddings" }, ]); expect(Array.isArray(vector)).toBe(true); expect(vector).toHaveLength(3); expect(vector?.every((value) => typeof value === "number")).toBe(true); }); });