Files
tyx_AI_xhs/tests/integration/wp0-08-structured-log.test.ts

138 lines
5.7 KiB
TypeScript

import { randomUUID } from "node:crypto";
import { mkdirSync, readFileSync, readdirSync, statSync, utimesSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { afterEach, describe, expect, it } from "vitest";
import { ManagedStorage } from "../../apps/api/src/managed-storage.js";
import {
LOG_FILE_LIMIT,
LOG_RETENTION_DAYS,
LOG_SIZE_LIMIT_BYTES,
StructuredJsonlLogger,
sanitizeDiagnosticRecord,
} from "../../apps/api/src/structured-log.js";
const temporaryRoots: string[] = [];
function fixtureRoot() {
const root = join(tmpdir(), `dada-wp0-08-${randomUUID()}`);
mkdirSync(root, { recursive: true });
temporaryRoots.push(root);
return root;
}
function evidence(name: string, value: unknown) {
const directory = process.env.DADA_EVIDENCE_DIR_LOG;
if (!directory) return;
mkdirSync(directory, { recursive: true });
writeFileSync(join(directory, name), `${JSON.stringify(value, null, 2)}\n`);
}
afterEach(async () => {
const { rm } = await import("node:fs/promises");
await Promise.all(temporaryRoots.splice(0).map((root) => rm(root, { force: true, recursive: true })));
});
describe("TDD-WP0-LOG-001 rotation and redaction", () => {
it("enforces 10 MiB, 10 files, 30 days and emits only allowlisted JSONL fields", () => {
const root = fixtureRoot();
const logDirectory = join(root, "logs", "api");
mkdirSync(logDirectory, { recursive: true });
const activePath = join(logDirectory, "api.jsonl");
const seedLine = `${JSON.stringify({
component: "api",
correlation_id: "corr_fixture",
schema_version: "1.0",
status_category: "ready",
timestamp: "2026-01-01T00:00:00.000Z",
})}\n`;
const repeats = Math.floor(LOG_SIZE_LIMIT_BYTES / Buffer.byteLength(seedLine));
writeFileSync(activePath, seedLine.repeat(repeats));
const bytesBeforeBoundaryWrite = statSync(activePath).size;
const marker = `trap-${randomUUID()}`;
const logger = new StructuredJsonlLogger({ component: "api", directory: logDirectory });
logger.write({
absolute_path: join(root, marker),
api_key: `${marker}-key`,
correlation_id: "corr_boundary",
duration_ms: 12,
email: `${marker}@example.invalid`,
error_category: "none",
image: Buffer.from(marker),
object_id: "obj_boundary",
prompt: `${marker} private prompt`,
provider_error: `${marker} upstream body`,
status_category: "completed",
});
expect(statSync(join(logDirectory, "api.1.jsonl")).size).toBeLessThanOrEqual(LOG_SIZE_LIMIT_BYTES);
expect(statSync(activePath).size).toBeLessThanOrEqual(LOG_SIZE_LIMIT_BYTES);
for (let index = 2; index <= 14; index += 1) writeFileSync(join(logDirectory, `api.${index}.jsonl`), seedLine);
const stalePath = join(logDirectory, "api.4.jsonl");
const stale = new Date(Date.now() - (LOG_RETENTION_DAYS + 1) * 24 * 60 * 60 * 1000);
utimesSync(stalePath, stale, stale);
new StructuredJsonlLogger({ component: "api", directory: logDirectory }).maintain();
const files = readdirSync(logDirectory).sort();
const scanned = files.map((name) => readFileSync(join(logDirectory, name), "utf8")).join("");
expect(files.length).toBeLessThanOrEqual(LOG_FILE_LIMIT);
expect(files).not.toContain("api.4.jsonl");
expect(scanned).not.toContain(marker);
const activeEntry = JSON.parse(readFileSync(activePath, "utf8").trim()) as Record<string, unknown>;
expect(Object.keys(activeEntry).sort()).toEqual([
"component", "correlation_id", "duration_ms", "error_category", "object_id", "schema_version", "status_category", "timestamp",
]);
const diagnostic = sanitizeDiagnosticRecord({
app_version: "0.0.0",
checked_at: "2026-01-01T00:00:00.000Z",
component: "api",
message_key: "log_write_failed",
private_content: marker,
result: "fail",
stable_check_code: "log_writable",
user_path: join(root, marker),
});
expect(JSON.stringify(diagnostic)).not.toContain(marker);
evidence("log-manifest.json", {
bytes_before_boundary_write: bytesBeforeBoundaryWrite,
file_limit: LOG_FILE_LIMIT,
files: files.map((name) => ({ name, size: statSync(join(logDirectory, name)).size })),
retention_days: LOG_RETENTION_DAYS,
size_limit_bytes: LOG_SIZE_LIMIT_BYTES,
status: "passed",
total_generated_bytes: bytesBeforeBoundaryWrite + statSync(activePath).size,
});
evidence("redaction.json", { diagnostic, marker_absent: true, status: "passed" });
});
it("marks storage unavailable when the log directory cannot be written", () => {
const root = fixtureRoot();
const storage = new ManagedStorage({ dataRoot: root, databasePath: join(root, "db", "dada.sqlite3") });
const blockedDirectory = join(root, "logs-blocked");
writeFileSync(blockedDirectory, "not-a-directory");
const logger = new StructuredJsonlLogger({
component: "api",
directory: blockedDirectory,
onWriteFailure: () => storage.setLogAvailability(false),
});
expect(() => logger.write({ correlation_id: "corr_log_failure", error_category: "log_write_failed", status_category: "failed" })).toThrow("log_write_failed");
expect(storage.getState().storage_status).toBe("unavailable");
expect(storage.inspectAction("ai_call")).toBe("reject_unavailable");
evidence("response.json", {
ai_call: storage.inspectAction("ai_call"),
business_objects_changed: 0,
storage_status: storage.getState().storage_status,
status: "passed",
});
storage.setLogAvailability(true);
expect(storage.getState().storage_status).toBe("unavailable");
storage.applyControlledMeasurement(0);
expect(storage.getState().storage_status).toBe("active");
storage.close();
});
});