Files
Jyotisha/frontend/tests/rectification-grounding-prompt-20260927.test.ts
T
66f087b588 feat(rectification): submit varga-resolution implementation for review
Review-only snapshot for BUG-1115 through BUG-1117; not merge-ready. New opening and append-turn PostgreSQL permission failures remain blocked. Persisted joint replay has zero completed questions; segment ordering remains off by default. The existing offline replay JSON is retained stale and unchanged after a denied overwrite, including its CRLF line endings. Browser/provider validation and final serial gates remain pending. No deployment, role permission changes, or staging/main push.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-01 08:04:04 +08:00

226 lines
10 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-rectification-grounding-20260927 T6 (R4 + Skill slicing): what a real
* rectification Agent sends the model on an evidence turn. Real
* `runV9AgentTurn` + real `getRectificationV9Agent` (real bound Skill, real
* tools) + a prompt-recording scripted model; case data is a real local
* engine response for a public AA chart (9 candidates).
*
* Token estimate (for the size table): one token per CJK character plus one
* per four other characters (`estimateTokens`). "Fixed overhead" is what every
* call of the turn re-sends regardless of tool results: the system messages
* plus the tool schemas.
*/
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import {
CASE_ID,
SESSION_ID,
TURN_ID,
USER_ID,
dossierFixture,
fakeAccounting,
receiptHandlers,
} from "./rectification-v9-test-support.ts";
import {
AA_EVIDENCE_ROWS,
AA_RANGE,
AA_TURNS,
buildGoldenLatest,
contentText,
estimateTokens,
scriptedModel,
setFocusHandler,
stubGoldenFetch,
type RecordedCall,
} from "./rectification-grounding-support.ts";
import { getRectificationV9Agent } from "../src/mastra/agentic-rectification.ts";
import { runV9AgentTurn } from "../src/lib/rectification-agentic/v9/agent-run.ts";
import {
RECTIFICATION_SKILL_SECTIONS_BY_ACTION,
sliceRectificationSkill,
} from "../src/lib/rectification-agentic/v9/skill-slice.ts";
import {
resolveActiveSkillPackage,
resolveExactSkillPackage,
} from "../src/lib/skill-package-registry.ts";
import { resolve } from "node:path";
import { productDeliveryForDomain } from "../src/lib/rectification-agentic/v9/product-delivery.ts";
export type CallSize = Readonly<{
call: number;
systemChars: number;
systemTokens: number;
toolsBytes: number;
toolsTokens: number;
toolNames: string[];
fixedTokens: number;
totalTokens: number;
}>;
export function callSizes(calls: readonly RecordedCall[]): CallSize[] {
return calls.map((call, index) => {
const system = call.prompt.filter((message) => message.role === "system").map((message) => contentText(message.content)).join("\n");
const tools = JSON.stringify(call.tools ?? []);
const all = call.prompt.map((message) => contentText(message.content)).join("\n");
return {
call: index + 1,
systemChars: system.length,
systemTokens: estimateTokens(system),
toolsBytes: Buffer.byteLength(tools, "utf8"),
toolsTokens: estimateTokens(tools),
toolNames: Array.isArray(call.tools) ? (call.tools as Array<{ name: string }>).map((tool) => tool.name) : [],
fixedTokens: estimateTokens(system) + estimateTokens(tools),
totalTokens: estimateTokens(all) + estimateTokens(tools),
};
});
}
/** One evidence turn: read-case, then one short body (no write claimed). */
export async function recordEvidenceTurn(productDomain?: string) {
const { latest, compute } = await buildGoldenLatest();
const { model, calls } = scriptedModel([
{ parts: [{ tool: { name: "rectification-read-case", input: { caseId: CASE_ID } } }], finish: "tool-calls" },
{ parts: [{ text: "2014 年 8 月结婚这件事拿去和星盘对照了。" }], finish: "stop" },
]);
const active = resolveActiveSkillPackage("jyotish-birth-time-rectification");
const accounting = fakeAccounting({
...receiptHandlers,
// The case is bound to the active Skill (10.0.x layout), as a new case is.
get_agentic_rectification_skill_identity: () => ({
skill_name: active.name,
skill_version: active.version,
skill_sha256: active.sha256,
skill_source_commit: active.sourceCommit,
}),
insert_agentic_rectification_skill_run_receipt: () => ({
receipt_id: "88888888-8888-4888-8888-888888888888",
skill_name: active.name,
skill_version: active.version,
skill_sha256: active.sha256,
source_commit: active.sourceCommit,
}),
get_agentic_rectification_case_dossier: () => {
const dossier = dossierFixture({
evidence: AA_EVIDENCE_ROWS,
latestResult: latest,
turns: AA_TURNS as never,
candidateRange: AA_RANGE as never,
}) as { case: Record<string, unknown> };
return { ...dossier, case: { ...dossier.case, skill_version: active.version,
...(productDomain ? { rectification_domain: productDomain } : {}),
} };
},
get_agentic_rectification_case_compute: () => compute,
set_agentic_rectification_conversation_focus: setFocusHandler,
append_agentic_rectification_turn: () => ({ turn_id: TURN_ID, should_execute: true, status: "pending" }),
finalize_agentic_rectification_turn: () => ({ turn_id: TURN_ID, status: "completed", idempotent: false }),
}, { fallback: () => null });
const restore = stubGoldenFetch();
try {
await runV9AgentTurn({
userId: USER_ID,
caseId: CASE_ID,
sessionId: SESSION_ID,
requestId: "req-grounding-prompt",
action: "evidence",
message: "2014 年 8 月结婚。",
modelName: "fake",
accounting: accounting.client as never,
billing: { reserve: async () => ({ success: true, status: 200 }), complete: async () => true, release: async () => true },
emit: () => {},
expectedWrite: "none",
buildAgent: async (turnId, skillPackage, attemptId) => getRectificationV9Agent(
{ id: "fake", label: "fake", description: "", creditCost: 1, isDefault: true, mode: "compatible", model: model as never } as never,
{ userId: USER_ID, caseId: CASE_ID, turnId, attemptId, userMessage: "2014 年 8 月结婚。", accounting: accounting.client as never,
...(productDomain ? { productDelivery: productDeliveryForDomain(productDomain) } : {}),
},
skillPackage,
),
});
} finally {
restore();
}
return calls;
}
function activeSkillBody(): string {
const skill = resolveActiveSkillPackage("jyotish-birth-time-rectification");
const raw = readFileSync(resolve(skill.resolvedPath, "SKILL.md"), "utf8");
return raw.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "").trim();
}
test("R4: an evidence-turn call carries no <available_skills> block and no skill tools", async () => {
const calls = await recordEvidenceTurn();
assert.ok(calls.length >= 2);
for (const call of calls) {
const system = call.prompt.filter((message) => message.role === "system").map((message) => contentText(message.content)).join("\n");
assert.doesNotMatch(system, /<available_skills>/);
assert.doesNotMatch(system, /Skills are NOT tools|call the `skill` tool/);
const names = Array.isArray(call.tools) ? (call.tools as Array<{ name: string }>).map((tool) => tool.name) : [];
assert.equal(names.includes("skill"), false);
assert.equal(names.includes("skill_search"), false);
}
});
test("T6: the evidence-turn bootstrap carries Skill §5 and §7 only", async () => {
const calls = await recordEvidenceTurn();
const system = calls[0]!.prompt.filter((message) => message.role === "system").map((message) => contentText(message.content)).join("\n");
assert.match(system, /## 5\. ConversationFocus 与意图承接/);
assert.match(system, /## 7\. 批量证据与日期真实性/);
for (const heading of ["## 1. ", "## 2. ", "## 3. ", "## 4. ", "## 6. ", "## 8. ", "## 9. ", "## 10. ", "## 11. "]) {
assert.equal(system.includes(heading), false, `${heading} in ${system.slice(system.indexOf(heading) - 200, system.indexOf(heading) + 200)}`);
}
// full_diagnostics-only fields described in §6 are not in the evidence turn.
assert.doesNotMatch(system, /guided_collect_windows|method_followup_plan/);
assert.match(system, /本轮是证据轮/);
assert.match(system, /# Jyotish 生时校正(V10)/);
});
test("T6: fixed overhead per evidence-turn call is under 8K estimated tokens", async () => {
const sizes = callSizes(await recordEvidenceTurn());
console.log(JSON.stringify({ scope: "T6 evidence turn sizes", sizes }));
for (const size of sizes) assert.ok(size.fixedTokens < 8_000, JSON.stringify(size));
});
test("segment-product real model calls keep target identity in the evidence slice within the original 8K budget", async () => {
const calls = await recordEvidenceTurn("career");
assert.ok(calls.length >= 2);
for (const call of calls) {
const system = call.prompt.filter(message => message.role === "system").map(message => contentText(message.content)).join("\n");
assert.match(system, /服务端持久产品身份/);
assert.match(system, /segment-product-v1/);
assert.match(system, /"targets":\["D1","D10"\]/);
assert.match(system, /没有摘要仍是新产品/);
assert.match(system, /代表候选签名只是诊断/);
assert.match(system, /## 5\. ConversationFocus 与意图承接/);
assert.match(system, /## 7\. 批量证据与日期真实性/);
for (const heading of ["## 1. ", "## 2. ", "## 3. ", "## 4. ", "## 6. ", "## 8. ", "## 9. ", "## 10. ", "## 11. "]) assert.equal(system.includes(heading), false);
assert.doesNotMatch(system, /<available_skills>/);
}
for (const size of callSizes(calls)) assert.ok(size.fixedTokens < 8_000, JSON.stringify(size));
});
test("T6: the slice rule is a code constant; other actions and unknown layouts get the whole Skill", () => {
assert.deepEqual(RECTIFICATION_SKILL_SECTIONS_BY_ACTION.evidence, ["ConversationFocus 与意图承接", "批量证据与日期真实性"]);
assert.equal(RECTIFICATION_SKILL_SECTIONS_BY_ACTION.opening, "all");
assert.equal(RECTIFICATION_SKILL_SECTIONS_BY_ACTION.read_only, "all");
const body = activeSkillBody();
assert.equal(sliceRectificationSkill(body, "opening").text, body);
const evidence = sliceRectificationSkill(body, "evidence");
assert.deepEqual(evidence.sections, ["## 5. ConversationFocus 与意图承接", "## 7. 批量证据与日期真实性"]);
assert.ok(evidence.text.length < body.length / 2);
const oldLayout = "# 旧版\n\n## 概述\n\n没有编号的章节。";
assert.equal(sliceRectificationSkill(oldLayout, "evidence").text, oldLayout);
// BUG-621: the oldest deprecated snapshot still resolves and still slices safely.
const oldest = resolveExactSkillPackage(
"jyotish-birth-time-rectification",
"9.0.0",
"5acb3103e80993ea611b93d2c1746e70b74aff8f8636bcffc80a837b954b470d",
);
const oldestBody = readFileSync(resolve(oldest.resolvedPath, "SKILL.md"), "utf8");
// 9.0.0 numbers its sections differently (its §5 is the candidate language):
// no title match, so the whole bound body goes out unchanged.
assert.equal(sliceRectificationSkill(oldestBody, "evidence").text, oldestBody);
});