mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-13 18:32:12 +03:00
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!
207 lines
7.5 KiB
TypeScript
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")}`
|
|
);
|
|
});
|