Files
diegosouzapw--omniroute/tests/unit/copilot-m365-web-logging-6210.test.ts
2026-07-13 13:39:12 +08:00

141 lines
5.2 KiB
TypeScript

/**
* Regression test for #6210 (observability follow-up) — copilot-m365-web streaming
* path emitted NO logs, so an empty `content:null` response was undiagnosable even at
* `APP_LOG_LEVEL=debug`. The tier parser itself was fixed in #6234; this guards the
* remaining defect: the WS path must emit debug diagnostics (connect / handshake /
* per-frame) AND must never leak the access_token that rides in the WS query string.
*
* The stub drives the executor with a scripted SignalR frame sequence (handshake ack →
* bot update → completion) and a captured `log`, asserting (a) a debug log fires for the
* handshake / first frame and (b) the logged connect URL is redacted — the raw URL handed
* to the WebSocket constructor still carries the secret, proving redaction is real.
*/
import test from "node:test";
import assert from "node:assert/strict";
import {
CopilotM365WebExecutor,
__setCopilotM365WebSocketForTesting,
} from "../../open-sse/executors/copilot-m365-web.ts";
import { encodeFrame } from "../../open-sse/executors/copilot-m365-frames.ts";
import type { ExecutorLog } from "../../open-sse/executors/base.ts";
const SECRET = "SUPERSECRETTOKEN-do-not-log-123";
type LogEntry = { level: string; tag: string; message: string };
function makeCapturingLog(sink: LogEntry[]): ExecutorLog {
const push = (level: string) => (tag: string, message: string) =>
sink.push({ level, tag, message });
return { debug: push("debug"), info: push("info"), warn: push("warn"), error: push("error") };
}
/**
* Minimal `ws`-shaped stub: emits `open` on next tick, answers the handshake request
* frame with `{}` (ack), and once the chat invocation is sent replays `frames`.
*/
function makeFakeWsCtor(frames: Array<Record<string, unknown>>, captured: { url?: string }) {
return class FakeWS {
private handlers: Record<string, (arg?: unknown) => void> = {};
constructor(url: string) {
captured.url = url;
setImmediate(() => this.handlers.open?.());
}
on(event: string, cb: (arg?: unknown) => void) {
this.handlers[event] = cb;
return this;
}
send(data: unknown) {
const str = typeof data === "string" ? data : String(data);
if (str.includes('"protocol":"json"')) {
setImmediate(() => this.handlers.message?.(Buffer.from(encodeFrame({}))));
} else if (str.includes('"target":"chat"')) {
setImmediate(() => {
for (const frame of frames) this.handlers.message?.(Buffer.from(encodeFrame(frame)));
});
}
}
close() {
/* no-op */
}
};
}
async function drainStream(executor: CopilotM365WebExecutor, log: ExecutorLog): Promise<void> {
const { response } = await executor.execute({
model: "copilot-m365",
body: { messages: [{ role: "user", content: "hi" }] },
stream: true,
credentials: {
apiKey: `access_token=${SECRET}`,
providerSpecificData: { chathubPath: "user-oid@tenant-id" },
},
log,
});
await response.text();
}
test("copilot-m365-web: WS path emits debug logs for handshake + first frame [#6210]", async () => {
const captured: { url?: string } = {};
const frames = [
{ type: 1, target: "update", arguments: [{ messages: [{ text: "Hi there", author: "bot" }] }] },
{ type: 3 },
];
const restore = __setCopilotM365WebSocketForTesting(
makeFakeWsCtor(frames, captured) as never
);
const sink: LogEntry[] = [];
try {
await drainStream(new CopilotM365WebExecutor(), makeCapturingLog(sink));
} finally {
restore();
}
const wsLogs = sink.filter((e) => e.level === "debug" && e.tag === "M365_WS");
assert.ok(wsLogs.length > 0, "expected at least one M365_WS debug log");
// (a) handshake is logged.
assert.ok(
wsLogs.some((e) => /handshake complete/i.test(e.message)),
"expected a handshake-complete debug log"
);
// (a) the first received frame's type/target is logged.
assert.ok(
wsLogs.some((e) => e.message.includes("frame type=1 target=update")),
"expected a per-frame type/target debug log for the first update frame"
);
});
test("copilot-m365-web: logged WS URL is redacted — access_token never leaks [#6210]", async () => {
const captured: { url?: string } = {};
const frames = [{ type: 3 }];
const restore = __setCopilotM365WebSocketForTesting(
makeFakeWsCtor(frames, captured) as never
);
const sink: LogEntry[] = [];
try {
await drainStream(new CopilotM365WebExecutor(), makeCapturingLog(sink));
} finally {
restore();
}
// The raw URL handed to the WebSocket constructor DOES carry the secret — proving the
// redaction assertion below would fail on an un-redacted URL and passes only because
// the executor redacts before logging.
assert.ok(captured.url?.includes(SECRET), "sanity: raw WS URL should contain the token");
const connectLog = sink.find(
(e) => e.tag === "M365_WS" && e.message.startsWith("connecting")
);
assert.ok(connectLog, "expected a 'connecting' debug log");
assert.ok(
connectLog.message.includes("access_token=REDACTED"),
"connect log should show the token as REDACTED"
);
// (b) NO logged message anywhere may contain the raw secret.
for (const entry of sink) {
assert.ok(
!entry.message.includes(SECRET),
`log leaked the access_token: ${entry.tag} ${entry.message}`
);
}
});