padhle - post migration

This commit is contained in:
2026-09-15 04:08:55 -04:00
parent 534b51b41b
commit b2d8109019
509 changed files with 147378 additions and 303 deletions
+109
View File
@@ -0,0 +1,109 @@
/**
* Log redaction — unit + integration tests.
*
* Anonymous trial ids are capabilities (no server-side ownership check), so a
* trial/session UUID embedded in an error message must never reach a log sink.
* These tests prove:
* 1. `redactSecrets` scrubs UUIDs (trial/session ids) and API keys.
* 2. The real chat error-logging path (via `logError`) never writes a trial
* id to stderr even when an error message contains one.
*/
import test, { before, after, mock } from "node:test";
import assert from "node:assert/strict";
import express from "express";
import { randomUUID } from "node:crypto";
import { redactSecrets } from "../src/services/logRedact.js";
import { createChatRouter } from "../src/routes/chat.js";
import { optionalAuth } from "../src/middleware/supabaseAuth.js";
const TRIAL_ID = "7c2f4a1e-9b3d-4a5b-8c1e-2f3a4b5c6d7e";
const SESSION_ID = "00000000-1111-2222-3333-444444444444";
// ─── fake AI streamer that throws an error whose message contains the trial
// id (simulates an AI provider echoing an id back). It records the exact
// message it throws so tests can prove the raw id was really present. ───
let throwId = null;
const thrownMessages = [];
async function throwingStream() {
const msg = `provider failed for chat ${throwId} · key sk-abcdefghijklmnopqrstuvwxyz123456789012`;
thrownMessages.push(msg);
throw new Error(msg);
}
let server;
let base;
before(async () => {
const app = express();
app.use(express.json());
app.use("/api/chat", optionalAuth, createChatRouter({ streamFn: throwingStream }));
server = app.listen(0, "127.0.0.1");
await new Promise((resolve) => server.once("listening", resolve));
base = `http://127.0.0.1:${server.address().port}`;
});
after(async () => {
if (server) await new Promise((resolve) => server.close(resolve));
mock.restoreAll();
});
test("redactSecrets scrubs UUIDs (trial/session ids) and API keys", () => {
assert.equal(redactSecrets(`chat ${TRIAL_ID}`), "chat [REDACTED_UUID]");
assert.equal(redactSecrets(SESSION_ID), "[REDACTED_UUID]");
assert.equal(redactSecrets("sk-abcdefghijklmnopqrstuvwxyz123456789012"), "[REDACTED_KEY]");
assert.equal(redactSecrets("sb_secret_abcDEF123==="), "[REDACTED_KEY]");
assert.equal(redactSecrets("no secrets here"), "no secrets here");
assert.equal(redactSecrets(42), 42, "non-strings pass through unchanged");
});
test("anonymous chat error logging never writes the trial id to stderr", async () => {
// First message creates a trial; capture its real id from the session event.
const res1 = await fetch(`${base}/api/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: "first" }),
});
const events1 = parseSse(await res1.text());
const trialId = events1.find((e) => e.type === "session").id;
assert.ok(trialId, "trial id present");
// Configure the fake AI to fail WITH the trial id embedded in its message.
throwId = trialId;
// Spy on console.error and drive a second message on the same trial.
const calls = [];
mock.method(console, "error", (...args) => calls.push(args));
const res2 = await fetch(`${base}/api/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: "second", chatId: trialId }),
});
// The router catches the throw, logs via logError, and returns a clean SSE error.
assert.equal(res2.status, 200);
const events2 = parseSse(await res2.text());
assert.ok(events2.some((e) => e.type === "error"), "in-stream error event");
// The raw throw happened, so console.error must have been called...
assert.ok(calls.length > 0, "console.error was called for the error");
const joined = calls.map((c) => c.join(" ")).join("\n");
assert.ok(joined.includes("Anonymous chat error"), "tag present");
// Prove the REAL trial id was genuinely in the message the provider threw,
// so the redaction below is meaningful (not a vacuous pass on a null id).
// The fake streamer throws on the first (id-setting) call too, so the
// assertion targets the LAST throw, which carries the real trial id.
const lastThrow = thrownMessages[thrownMessages.length - 1];
assert.ok(lastThrow.includes(trialId), "raw error message contained the trial id");
assert.ok(lastThrow.includes("sk-abcdefghijklmnopqrstuvwxyz"), "raw error message contained the API key");
// ...but the trial id and the sk- key must NOT appear in the log output.
assert.ok(!joined.includes(trialId), "trial id redacted from log output");
assert.ok(!joined.includes("sk-abcdefghijklmnopqrstuvwxyz"), "API key redacted from log output");
assert.ok(joined.includes("[REDACTED_UUID]"), "redaction marker present");
assert.ok(joined.includes("[REDACTED_KEY]"), "key redaction marker present");
});
/** Parse SSE text into events. */
function parseSse(text) {
return text
.split("\n\n")
.filter((block) => block.startsWith("data: "))
.map((block) => JSON.parse(block.slice(6)));
}