Files
Jyotisha/frontend/tests/rectification-agent-contracts.test.ts
T
2026-07-28 13:04:30 +08:00

140 lines
5.2 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import {
validateRectificationDecision,
type DiagnosticsSummary,
type QuestionOpportunity,
} from "../src/lib/rectification-agent/contracts.ts";
import { recordRectificationAgentTelemetry } from "../src/lib/rectification-agent/telemetry.ts";
const caseId = "00000000-0000-4000-8000-000000000800";
const opportunityId = "00000000-0000-4000-8000-000000000801";
const snapshotId = "00000000-0000-4000-8000-000000000802";
const diagnostics: DiagnosticsSummary = {
id: "00000000-0000-4000-8000-000000000803",
caseId,
snapshotId,
primaryClusterRetentionRate: 0.8,
leaveOneEventOutRetentionRate: 0.75,
leaveOneDomainOutRetentionRate: 0.7,
dateSensitivityRetentionRate: 0.72,
neighborSupportMinutes: 8,
primarySecondaryMarginPercent: 12,
clusterMassRatio: 0.65,
unstableEventIds: [],
mostDiscriminatingLayers: ["D9"],
eventDateSensitivity: [],
candidateSplits: [],
calculationHash: "c".repeat(64),
createdAt: "2026-07-28T00:00:00.000Z",
};
const opportunity: QuestionOpportunity = {
opportunityId,
kind: "ask_new_event",
domain: "career",
targetEventId: null,
prompt: "请补充一个有明确年月的重要事件。",
reason: "当前证据领域覆盖不足。",
expectedInformationGain: 0.8,
dateSensitivity: 0.5,
candidateSplitRelevance: 0.7,
domainCoverageGain: 0.6,
recallEase: 0.8,
novelty: 1,
repetitionPenalty: 0,
privacyCost: 0.1,
utility: 0.69,
active: true,
};
function validate(decision: unknown, overrides: Partial<Parameters<typeof validateRectificationDecision>[0]> = {}) {
return validateRectificationDecision({
decision,
caseId,
snapshotId,
opportunities: [opportunity],
diagnostics,
candidateRangeOfferAllowed: true,
...overrides,
});
}
test("only an active server-owned opportunity can be selected", () => {
assert.deepEqual(validate({
action: "ask_question", opportunityId, narrativeFocus: ["latest_event"],
}, { opportunities: [{ ...opportunity, active: false }] }).issues, ["opportunity_not_active"]);
assert.deepEqual(validate({
action: "ask_question", opportunityId: "00000000-0000-4000-8000-000000000899", narrativeFocus: [],
}).issues, ["opportunity_not_active"]);
});
test("candidate ranges require both the policy gate and the current snapshot", () => {
assert.deepEqual(validate({ action: "offer_candidate_range", snapshotId }, {
candidateRangeOfferAllowed: false,
}).issues, ["candidate_range_gate_failed"]);
assert.deepEqual(validate({ action: "offer_candidate_range", snapshotId }, {
snapshotId: "00000000-0000-4000-8000-000000000898",
}).issues, ["snapshot_not_current"]);
});
test("diagnostic reads are bounded and cannot target another case", () => {
assert.deepEqual(validate({ action: "run_diagnostic", diagnostic: "neighbor_stability" }, {
usedDiagnostics: ["neighbor_stability"],
}).issues, ["diagnostic_already_run"]);
assert.deepEqual(validate({ action: "ask_question", opportunityId, narrativeFocus: [] }, {
caseId: "00000000-0000-4000-8000-000000000897",
toolCallCount: 2,
maxToolCalls: 1,
}).issues, ["diagnostics_case_mismatch", "tool_call_budget_exceeded"]);
});
test("model output cannot inject a minute, question, event, or score", () => {
for (const extra of [
{ birthMinute: "06:21" },
{ prompt: "模型自己写的问题" },
{ eventId: "00000000-0000-4000-8000-000000000896" },
{ score: 99 },
]) {
assert.deepEqual(validate({ action: "offer_candidate_range", snapshotId, ...extra }).issues, ["decision_schema_invalid"]);
}
});
test("agent telemetry rejects malformed events and warns on failures", () => {
const info: string[] = [];
const warnings: string[] = [];
const originalInfo = console.info;
const originalWarn = console.warn;
console.info = (message) => info.push(String(message));
console.warn = (message) => warnings.push(String(message));
try {
recordRectificationAgentTelemetry({
caseId, phase: "reasoner", outcome: "failed", modelId: "test-model", toolName: null,
decisionAction: null, durationMs: 12, errorCode: "model_unavailable", deploymentSha: "test-sha",
});
recordRectificationAgentTelemetry({
caseId, phase: "reasoner", outcome: "failed", modelId: "", toolName: null,
decisionAction: null, durationMs: 12, errorCode: "model_unavailable", deploymentSha: "test-sha",
});
} finally {
console.info = originalInfo;
console.warn = originalWarn;
}
assert.equal(info.length, 0);
assert.equal(warnings.length, 1);
assert.match(warnings[0] ?? "", /\[rectification-agent\].*"outcome":"failed"/);
});
test("durable event semantics migration validates and persists subject fields", () => {
const migration = readFileSync(new URL(
"../supabase/migrations/20260728010000_conversational_event_semantics.sql",
import.meta.url,
), "utf8");
assert.match(migration, /subject in \('self', 'family', 'partner', 'other'\)/);
assert.match(migration, /related_person/);
assert.match(migration, /save_conversational_rectification_turn/);
assert.match(migration, /import_legacy_conversational_rectification_case/);
assert.match(migration, /event_kind/);
assert.match(migration, /scoreability/);
});