fix(core): align forensic activity event metadata
This commit is contained in:
parent
2b21ee0a88
commit
4618fc4c6b
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { mkdirSync, readFileSync, rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
|
@ -18,17 +18,25 @@ import {
|
|||
} from "../agent-report.js";
|
||||
import { writeMetadata, writeCanonicalLifecycle, readMetadataRaw } from "../metadata.js";
|
||||
import { createInitialCanonicalLifecycle } from "../lifecycle-state.js";
|
||||
import { recordActivityEvent } from "../activity-events.js";
|
||||
import type { CanonicalSessionLifecycle } from "../types.js";
|
||||
|
||||
vi.mock("../activity-events.js", () => ({
|
||||
recordActivityEvent: vi.fn(),
|
||||
}));
|
||||
|
||||
let dataDir: string;
|
||||
let tempRoot: string;
|
||||
|
||||
beforeEach(() => {
|
||||
dataDir = join(tmpdir(), `ao-test-agent-report-${randomUUID()}`);
|
||||
tempRoot = join(tmpdir(), `ao-test-agent-report-${randomUUID()}`);
|
||||
dataDir = join(tempRoot, "projects", "project-alpha", "sessions");
|
||||
mkdirSync(dataDir, { recursive: true });
|
||||
vi.mocked(recordActivityEvent).mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(dataDir, { recursive: true, force: true });
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function seedWorkerSession(
|
||||
|
|
@ -433,6 +441,13 @@ describe("applyAgentReport", () => {
|
|||
sessionState: "terminated",
|
||||
},
|
||||
});
|
||||
expect(recordActivityEvent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
projectId: "project-alpha",
|
||||
sessionId,
|
||||
kind: "api.agent_report.transition_rejected",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("throws when the session does not exist", () => {
|
||||
|
|
@ -442,6 +457,13 @@ describe("applyAgentReport", () => {
|
|||
now: new Date(),
|
||||
}),
|
||||
).toThrow(/not found/);
|
||||
expect(recordActivityEvent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
projectId: "project-alpha",
|
||||
sessionId: "missing-session",
|
||||
kind: "api.agent_report.session_not_found",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// 260 atomic-write cycles are slow on Windows (rename + AV scan); bump the
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { mkdirSync, rmSync, readFileSync, existsSync, writeFileSync, readdirSync } from "node:fs";
|
||||
import { mkdirSync, rmSync, readFileSync, existsSync, writeFileSync, readdirSync, renameSync } from "node:fs";
|
||||
import type * as NodeFs from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
|
@ -15,6 +16,16 @@ import {
|
|||
} from "../metadata.js";
|
||||
import { recordActivityEvent } from "../activity-events.js";
|
||||
|
||||
vi.mock("node:fs", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof NodeFs>();
|
||||
return {
|
||||
...actual,
|
||||
renameSync: vi.fn((...args: Parameters<typeof actual.renameSync>) =>
|
||||
actual.renameSync(...args),
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../activity-events.js", () => ({
|
||||
recordActivityEvent: vi.fn(),
|
||||
}));
|
||||
|
|
@ -25,6 +36,7 @@ beforeEach(() => {
|
|||
dataDir = join(tmpdir(), `ao-test-metadata-${randomUUID()}`);
|
||||
mkdirSync(dataDir, { recursive: true });
|
||||
vi.mocked(recordActivityEvent).mockClear();
|
||||
vi.mocked(renameSync).mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -355,6 +367,7 @@ describe("mutateMetadata corrupt-file handling", () => {
|
|||
source: "session-manager",
|
||||
kind: "metadata.corrupt_detected",
|
||||
level: "error",
|
||||
summary: expect.stringContaining("renamed to"),
|
||||
data: expect.objectContaining({
|
||||
renamedTo: expect.stringMatching(/\.corrupt-\d+$/),
|
||||
renameSucceeded: true,
|
||||
|
|
@ -365,6 +378,37 @@ describe("mutateMetadata corrupt-file handling", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("emits a rename-failed summary when corrupt metadata cannot be renamed", () => {
|
||||
const sessionPath = join(dataDir, "ao-rename-failed.json");
|
||||
writeFileSync(sessionPath, "{ broken json", "utf-8");
|
||||
vi.mocked(renameSync).mockImplementationOnce(() => {
|
||||
throw new Error("rename denied");
|
||||
});
|
||||
|
||||
const result = mutateMetadata(
|
||||
dataDir,
|
||||
"ao-rename-failed",
|
||||
(existing) => ({ ...existing, branch: "feat/x" }),
|
||||
{ createIfMissing: true },
|
||||
);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
const call = vi
|
||||
.mocked(recordActivityEvent)
|
||||
.mock.calls.find((c) => c[0].kind === "metadata.corrupt_detected");
|
||||
expect(call).toBeDefined();
|
||||
expect(call![0]).toMatchObject({
|
||||
sessionId: "ao-rename-failed",
|
||||
summary: expect.stringContaining("failed to rename"),
|
||||
data: expect.objectContaining({
|
||||
renamedTo: null,
|
||||
renameSucceeded: false,
|
||||
path: sessionPath,
|
||||
}),
|
||||
});
|
||||
expect(call![0].summary).not.toContain("renamed to");
|
||||
});
|
||||
|
||||
it("truncates contentSample to 200 chars in metadata.corrupt_detected", () => {
|
||||
const sessionPath = join(dataDir, "ao-4.json");
|
||||
// 250 char garbage payload — sanitizer cap is 16KB but invariant B11 caps
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { basename, dirname, join } from "node:path";
|
||||
import type {
|
||||
CanonicalSessionLifecycle,
|
||||
CanonicalSessionReason,
|
||||
|
|
@ -255,6 +255,11 @@ function buildAuditDir(dataDir: string): string {
|
|||
return join(dataDir, ".agent-report-audit");
|
||||
}
|
||||
|
||||
function inferProjectIdFromDataDir(dataDir: string): string | undefined {
|
||||
// dataDir is `.../projects/{projectId}/sessions`; recover projectId for filtering.
|
||||
return basename(dirname(dataDir)) || undefined;
|
||||
}
|
||||
|
||||
const AGENT_REPORT_AUDIT_MAX_BYTES = 256 * 1024;
|
||||
const AGENT_REPORT_AUDIT_MAX_ENTRIES = 200;
|
||||
|
||||
|
|
@ -384,9 +389,11 @@ export function applyAgentReport(
|
|||
sessionId: SessionId,
|
||||
input: ApplyAgentReportInput,
|
||||
): ApplyAgentReportResult {
|
||||
const projectId = inferProjectIdFromDataDir(dataDir);
|
||||
const raw = readMetadataRaw(dataDir, sessionId);
|
||||
if (!raw) {
|
||||
recordActivityEvent({
|
||||
projectId,
|
||||
sessionId,
|
||||
source: "api",
|
||||
kind: "api.agent_report.session_not_found",
|
||||
|
|
@ -468,6 +475,7 @@ export function applyAgentReport(
|
|||
after: before,
|
||||
});
|
||||
recordActivityEvent({
|
||||
projectId,
|
||||
sessionId,
|
||||
source: "api",
|
||||
kind: "api.agent_report.transition_rejected",
|
||||
|
|
|
|||
|
|
@ -375,13 +375,16 @@ export function mutateMetadata(
|
|||
content.length > 200 ? `${content.slice(0, 200)}` : content;
|
||||
// dataDir is `.../projects/{projectId}/sessions`; recover projectId for filtering.
|
||||
const inferredProjectId = basename(dirname(dataDir));
|
||||
const summary = renamed
|
||||
? `Corrupt metadata for session ${sessionId} renamed to ${basename(corruptPath)}`
|
||||
: `Corrupt metadata detected for session ${sessionId}; failed to rename forensic copy before rewrite`;
|
||||
recordActivityEvent({
|
||||
projectId: inferredProjectId || undefined,
|
||||
sessionId,
|
||||
source: "session-manager",
|
||||
kind: "metadata.corrupt_detected",
|
||||
level: "error",
|
||||
summary: `Corrupt metadata for session ${sessionId} renamed to ${basename(corruptPath)}`,
|
||||
summary,
|
||||
data: {
|
||||
path,
|
||||
renamedTo: renamed ? basename(corruptPath) : null,
|
||||
|
|
|
|||
Loading…
Reference in New Issue