feat: complete TASK-WP0-08 logging

This commit is contained in:
suyx
2026-07-28 00:11:02 +08:00
parent 096a1f1279
commit 361bda2f22
21 changed files with 1050 additions and 22 deletions
@@ -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();
});
});
@@ -0,0 +1,52 @@
import { randomUUID } from "node:crypto";
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { expect, it, vi } from "vitest";
import { ManagedStorage } from "../../apps/api/src/managed-storage.js";
import { WorkerAiCallGate } from "../../apps/worker/src/ai-call-gate.js";
import { WorkerStorageStatus } from "../../apps/worker/src/storage-status.js";
import { StructuredJsonlLogger } from "../../apps/worker/src/structured-log.js";
it("TDD-WP0-LOG-001 blocks AI before an upstream call when logging becomes unavailable", async () => {
const root = join(tmpdir(), `dada-wp0-08-worker-${randomUUID()}`);
mkdirSync(root, { recursive: true });
const blockedDirectory = join(root, "blocked-log-directory");
writeFileSync(blockedDirectory, "not-a-directory");
const databasePath = join(root, "db", "dada.sqlite3");
const managedStorage = new ManagedStorage({ dataRoot: root, databasePath });
const workerStorage = new WorkerStorageStatus(databasePath);
let storageStatus: "active" | "unavailable" = "active";
const logger = new StructuredJsonlLogger({
component: "worker",
directory: blockedDirectory,
onWriteFailure: () => {
storageStatus = "unavailable";
workerStorage.markLogUnavailable();
},
});
const externalCall = vi.fn(async () => ({ provider_request_id: "must-not-exist" }));
const gate = new WorkerAiCallGate({ getStorageStatus: () => storageStatus, logger });
await expect(gate.execute("corr_worker_log_failure", "job_internal", externalCall)).resolves.toEqual({
reason: "storage_unavailable",
status: "blocked",
});
expect(storageStatus).toBe("unavailable");
expect(workerStorage.getStatus()).toBe("unavailable");
expect(managedStorage.getState().storage_status).toBe("unavailable");
expect(externalCall).not.toHaveBeenCalled();
const directory = process.env.DADA_EVIDENCE_DIR_LOG;
if (directory) {
mkdirSync(directory, { recursive: true });
writeFileSync(join(directory, "external-calls.json"), `${JSON.stringify({ calls: 0, reason: "storage_unavailable", status: "passed" }, null, 2)}\n`);
}
const { rm } = await import("node:fs/promises");
workerStorage.close();
managedStorage.close();
await rm(root, { force: true, recursive: true });
});