Files
Jyotisha/frontend/tests/consult-evidence-card-telemetry-20260927.test.ts
T
Jesse_ChenandClaude Opus 5.5 db17074430 feat(consult): evidence card v2 per the astrologer's review
TASK-consult-evidence-card-v2-20260927 T3. Card version evidence-card-v2.

- Base section (every card): the engine's D9 summary (D9 lagna, each
  planet's D9 sign and dignity, Vargottama, D1/D9 reversals); D9 houses and
  aspects stay out.
- Career: + AL (engine pada A1), 10H SAV, SAV of the Jupiter / Saturn
  transit signs.
- Marriage: + 5H / 5L, day / night, Punarphoo (observation_only), Double
  Transit on 7H / 7L (house-7 run) and DK / UL, Vivah Saham, gender unknown.
- Wealth: 8H / 12H named.
- Annual: the annual Tajika chart verbatim (parameter_sensitive), or
  年盘未接入 when the pack is blocked / not attached (never natal data);
  houses 1 + running / next AD lords' houses + the year's Jupiter / Saturn
  transit houses, with their basis; ingress / station dates only.
- Timing: Rahu / Ketu with ingress dates, Jupiter / Saturn SAV and BAV,
  Double Transit conclusions (no degrees); vargas follow the turn's other
  domain, D9 when alone.
- Chara Dasha leaves every card; KP is never on a card. The lookup enum adds
  karakamsha, dispositor_chains, inter_chart_linkage, argala, moon_transit;
  a KP lookup travels with its blocked note.
- System prompt and tool descriptions name the D9 summary, the
  parameter_sensitive / observation_only tags, 年盘未接入 and the lookups.
- Telemetry card version / agentVersion bumped to v2 (three-column notes in
  the touched tests).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
2026-09-27 16:44:19 +08:00

94 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// TASK-consult-evidence-card-20260927 T6 (D7): per-turn evidence-card record
// in [agent-observability], six fields only, no user text.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import {
createAgentObservabilityLogger,
EVIDENCE_CARD_TELEMETRY_FIELDS,
evidenceCardTelemetrySchema,
type AgentObservabilityEvent,
} from "../src/lib/agent-observability.ts";
import {
consultationEvidenceCardTelemetry,
createConsultationRuntimeState,
createConsultationTools,
} from "../src/mastra/consultation-tools.ts";
type Json = Record<string, unknown>;
const golden = JSON.parse(readFileSync(
new URL("./fixtures/consult-evidence-card-golden.json", import.meta.url),
"utf8",
)) as { charts: Array<{ id: string; workflow: Json }> };
const workflow = golden.charts[0]!.workflow;
const route = readFileSync(new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8");
async function stateAfterCalculation() {
const state = createConsultationRuntimeState();
const tools = createConsultationTools({
userId: "user-telemetry", sessionId: "session-telemetry", requestId: "req-telemetry", consultationMode: "verified_chart",
serverChart: { name: "public", toolInput: { year: 2000, month: 1, day: 1, hour: 0, minute: 0, city: "fixture", lat: 0, lon: 0, tz: 0, ayanamsa: "raman" }, truth: { birthTimeSource: "reported" } } as never,
state,
runWorkflow: async () => structuredClone(workflow) as never,
});
await tools["run-jyotish-consultation"].execute!({ question: "q", domains: ["parents"] } as never, { writer: { custom: async () => {} } } as never);
return state;
}
test("the record has exactly the six approved fields", async () => {
assert.deepEqual([...EVIDENCE_CARD_TELEMETRY_FIELDS].sort(), Object.keys(evidenceCardTelemetrySchema.unwrap().shape).sort());
assert.deepEqual([...EVIDENCE_CARD_TELEMETRY_FIELDS], ["domains", "cardVersion", "cardChars", "cardTokenEstimate", "citedFieldIds", "feedback"]);
const state = await stateAfterCalculation();
const record = consultationEvidenceCardTelemetry(state, "");
assert.ok(record);
assert.deepEqual(Object.keys(record).sort(), [...EVIDENCE_CARD_TELEMETRY_FIELDS].sort());
assert.deepEqual(record.domains, ["parents"]);
// 原值: "evidence-card-v1" / 新值: "evidence-card-v2" / 原因: TASK-consult-evidence-card-v2-20260927 T3 卡版本升级
assert.equal(record.cardVersion, "evidence-card-v2");
assert.equal(record.cardChars, state.evidenceCardChars);
assert.equal(record.cardTokenEstimate, Math.round(record.cardChars / 3.5));
assert.deepEqual(record.citedFieldIds, []);
assert.equal(record.feedback, "none");
});
test("the schema is strict: no extra field, no free text in field ids", () => {
const valid = { domains: ["parents"], cardVersion: "evidence-card-v2", cardChars: 10, cardTokenEstimate: 3, citedFieldIds: ["base.natal.ascendant.sign"], feedback: "up" };
assert.equal(evidenceCardTelemetrySchema.safeParse(valid).success, true);
for (const extra of ["question", "answer", "sessionId", "userId", "birthDate", "matched"]) {
assert.equal(evidenceCardTelemetrySchema.safeParse({ ...valid, [extra]: "x" }).success, false, extra);
}
assert.equal(evidenceCardTelemetrySchema.safeParse({ ...valid, citedFieldIds: ["我和父母关系如何"] }).success, false);
assert.equal(evidenceCardTelemetrySchema.safeParse({ ...valid, feedback: "great" }).success, false);
});
test("the logged line keeps field ids and drops every word of the user's text", async () => {
const state = await stateAfterCalculation();
const card = state.evidenceCard!;
const pdStart = String((card.base.timing.vimshottari.pratyantardasha as Json).start);
// Fictional sensitive values, not a real person.
const secrets = ["虚构名字王小明", "fictional.person@example.invalid", "1990-01-01", "我和父母关系如何"];
const answer = `${secrets[0]}(${secrets[1]},生日 ${secrets[2]})问:${secrets[3]}。子运从 ${pdStart} 开始。`;
const lines: AgentObservabilityEvent[] = [];
const log = createAgentObservabilityLogger((event) => lines.push(event));
log({ runId: "req-telemetry", agentVersion: "consultation-evidence-card-v2", evidenceCard: consultationEvidenceCardTelemetry(state, answer) });
const text = JSON.stringify(lines);
for (const secret of secrets) assert.equal(text.includes(secret), false, secret);
assert.equal(text.includes(pdStart), false, "the matched date is not kept");
assert.equal(text.includes("session-telemetry"), false);
assert.equal(text.includes("user-telemetry"), false);
assert.ok(lines[0]?.evidenceCard?.citedFieldIds.includes("base.timing.vimshottari.pratyantardasha.start"));
});
test("the consult route logs the record once per natal turn, without session or user id", () => {
assert.match(route, /const evidenceCard = consultationEvidenceCardTelemetry\(state, answerForTelemetry\);/);
const block = route.slice(route.indexOf("const evidenceCard = consultationEvidenceCardTelemetry"), route.indexOf("const settleRun = async"));
// 原值: consultation-evidence-card-v1 / 新值: v2 / 原因: 卡 v2(TASK-consult-evidence-card-v2-20260927 T3)
assert.match(block, /agentVersion: "consultation-evidence-card-v2"/);
assert.doesNotMatch(block, /sessionId|userId/);
const natal = route.slice(route.indexOf("const agent = getJyotishAgent(selectedModel, agentContext);"));
assert.match(natal, /answerForTelemetry = output;[\s\S]*return settleRun\(\(\) => completeResponse\(/);
// Only a finished answer counts citations; a failed run logs the card size with no citations.
assert.doesNotMatch(natal, /onError: \(error, _emitted, output\)/);
});