feat: complete TASK-WP0-08 logging
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user