Files
OmniRoute/tests/unit/log-export-extensibility.test.ts
Damian Pozimski 385e90f444 feat(dashboard): continuous call-log export to pluggable destinations (BigQuery first) (#11945)
Feature grande e bem construída: exportação contínua de call logs para destinos plugáveis (BigQuery primeiro). Revisei especificamente o tratamento de segredos (`src/lib/logExport/secrets.ts`) e a migração — encryption gate real (`requiresEncryptionKey` recusa gravação em texto plano quando `STORAGE_ENCRYPTION_KEY` não está setada), redação antes de qualquer resposta de API, e a migração cria a tabela com `enabled=0`/`include_bodies=0` por padrão (opt-in, sem exportar nada até o operador configurar). 62/62 testes focados verdes, typecheck limpo.

Resolvido o conflito com o barrel `src/lib/localDb.ts` (removido nesta mesma sessão, #11795 fase 5 — todo consumidor já migrado para `src/lib/db/*`); a PR só adicionava um re-export nele, que não é mais necessário. Obrigado pela contribuição!
2026-08-30 09:16:52 -03:00

207 lines
7.5 KiB
TypeScript

/**
* Proves the log-export pipeline is destination-agnostic: a fake destination registered
* through the public registry API drives the whole runner, and no BigQuery vocabulary
* leaks into the runner, the REST layer, the persistence module or the dashboard.
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { z } from "zod";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omni-log-export-ext-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.STORAGE_ENCRYPTION_KEY = "log-export-extensibility-test-key";
const REPO_ROOT = path.resolve(import.meta.dirname, "..", "..");
const coreDb = await import("../../src/lib/db/core.ts");
const destinationsDb = await import("../../src/lib/db/logExportDestinations.ts");
const registry = await import("../../src/lib/logExport/registry.ts");
const secrets = await import("../../src/lib/logExport/secrets.ts");
const runner = await import("../../src/lib/logExport/runner.ts");
const presenter = await import("../../src/lib/logExport/presenter.ts");
/** A minimal third-party destination, the shape a Datadog or Grafana plugin would take. */
const sent: unknown[][] = [];
let prepareCalls = 0;
const fakeDestination = {
id: "unit-test-sink",
labelFallback: "Unit Test Sink",
descriptionFallback: "In-memory destination used to prove the pipeline is generic.",
secretFields: ["apiToken"] as const,
fields: [
{ key: "endpoint", labelFallback: "Endpoint", type: "text" as const, required: true },
{
key: "apiToken",
labelFallback: "API token",
type: "password" as const,
required: true,
secret: true,
},
],
configSchema: z.object({ endpoint: z.string().min(1), apiToken: z.string().min(1) }),
createClient(config: { endpoint: string; apiToken: string }) {
return {
async test() {
return { ok: true, detail: `reachable at ${config.endpoint}` };
},
async prepare() {
prepareCalls += 1;
},
async send(records: readonly unknown[]) {
sent.push([...records]);
},
};
},
};
function insertCallLogs(count: number) {
const db = coreDb.getDbInstance();
const statement = db.prepare(
`INSERT INTO call_logs (id, timestamp, method, path, status, model, provider, duration,
tokens_in, tokens_out)
VALUES (?, ?, 'POST', '/v1/chat/completions', 200, 'claude-opus-5', 'anthropic', 10, 1, 2)`
);
for (let i = 0; i < count; i++) {
statement.run(`ext-log-${i}`, new Date(Date.UTC(2026, 7, 28, 12, 0, i)).toISOString());
}
}
test.beforeEach(() => {
coreDb.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
sent.length = 0;
prepareCalls = 0;
registry.__registerLogExportDestinationTypeForTest(fakeDestination as never);
});
test.afterEach(() => {
registry.__resetLogExportDestinationTypesForTest();
});
test.after(() => {
coreDb.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("runner_ThirdPartyDestination_DrivesTheWholePipelineWithNoBigQueryCode", async () => {
insertCallLogs(5);
const destination = destinationsDb.createLogExportDestination({
name: "Fake sink",
type: "unit-test-sink",
enabled: true,
batchSize: 2,
maxRowsPerRun: 1000,
config: secrets.encryptDestinationConfig("unit-test-sink", {
endpoint: "https://sink.example.com",
apiToken: "super-secret-token",
}),
});
const result = await runner.runDestinationExport(destination);
assert.equal(result.success, true, result.error ?? "");
assert.equal(result.exported, 5);
assert.equal(prepareCalls, 1, "prepare runs once per drain");
assert.deepEqual(
sent.map((batch) => batch.length),
[2, 2, 1]
);
assert.equal(destinationsDb.getLogExportDestination(destination.id)?.exportedTotal, 5);
});
test("secrets_ThirdPartyDestination_EncryptsAndRedactsItsOwnDeclaredSecretFields", () => {
const stored = secrets.encryptDestinationConfig("unit-test-sink", {
endpoint: "https://sink.example.com",
apiToken: "super-secret-token",
});
assert.match(String(stored.apiToken), /^enc:v1:/);
assert.equal(stored.endpoint, "https://sink.example.com");
const redacted = secrets.redactDestinationConfig("unit-test-sink", stored);
assert.equal(redacted.apiToken, secrets.SECRET_PLACEHOLDER);
assert.equal(JSON.stringify(redacted).includes("super-secret-token"), false);
});
test("registry_ThirdPartyDestination_IsRenderableByTheDashboardWithoutUiChanges", () => {
const descriptors = registry.describeLogExportDestinationTypes();
const fake = descriptors.find((entry) => entry.id === "unit-test-sink");
assert.ok(fake, "a registered destination must be described to the UI");
assert.deepEqual(
fake.fields.map((field) => field.key),
["endpoint", "apiToken"]
);
});
test("presenter_ThirdPartyDestination_ProjectsWithoutKnowingTheType", () => {
const destination = destinationsDb.createLogExportDestination({
name: "Fake sink",
type: "unit-test-sink",
enabled: false,
config: secrets.encryptDestinationConfig("unit-test-sink", {
endpoint: "https://sink.example.com",
apiToken: "super-secret-token",
}),
});
const view = presenter.toDestinationView(destination);
assert.equal(view.typeAvailable, true);
assert.equal(view.typeLabel, "Unit Test Sink");
assert.equal(view.config.apiToken, secrets.SECRET_PLACEHOLDER);
});
test("presenter_UnregisteredType_DegradesInsteadOfCrashing", () => {
const destination = destinationsDb.createLogExportDestination({
name: "Removed plugin",
type: "some-removed-plugin",
enabled: true,
config: { anything: "value" },
});
const view = presenter.toDestinationView(destination);
assert.equal(view.typeAvailable, false);
assert.equal(view.typeLabel, "some-removed-plugin");
});
test("pipeline_GenericLayers_CarryNoBigQueryVocabulary", () => {
const genericFiles = [
"src/lib/logExport/runner.ts",
"src/lib/logExport/secrets.ts",
"src/lib/logExport/presenter.ts",
"src/lib/logExport/types.ts",
"src/lib/db/logExportDestinations.ts",
"src/lib/usage/callLogExportSource.ts",
"src/lib/jobs/logExportJob.ts",
"src/app/api/log-export/types/route.ts",
"src/app/api/log-export/status/route.ts",
"src/app/api/log-export/destinations/route.ts",
"src/app/api/log-export/destinations/[id]/route.ts",
"src/app/api/log-export/destinations/[id]/run/route.ts",
"src/app/api/log-export/destinations/[id]/test/route.ts",
"src/app/(dashboard)/dashboard/log-export/LogExportPageClient.tsx",
"src/app/(dashboard)/dashboard/log-export/components/DestinationCard.tsx",
"src/app/(dashboard)/dashboard/log-export/components/DestinationFormModal.tsx",
"src/app/(dashboard)/dashboard/log-export/types.ts",
];
const forbidden = /bigquery|projectId|datasetId|serviceAccountJson|insertAll/i;
const offenders: string[] = [];
for (const relative of genericFiles) {
const source = fs.readFileSync(path.join(REPO_ROOT, relative), "utf8");
for (const [index, line] of source.split("\n").entries()) {
// The runner's file header cites BigQuery once as an example of insertId dedup.
if (line.trim().startsWith("*") || line.trim().startsWith("//")) continue;
if (forbidden.test(line)) offenders.push(`${relative}:${index + 1}: ${line.trim()}`);
}
}
assert.deepEqual(
offenders,
[],
`destination-specific vocabulary leaked into generic layers:\n${offenders.join("\n")}`
);
});