Merge codex/pr2-final-response into staging integration
This commit is contained in:
@@ -1,9 +1,15 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { createConsultationTools, createConsultationRuntimeState } from "../src/mastra/consultation-tools.ts";
|
||||
import {
|
||||
appendConsultationRuntimeStep,
|
||||
consultationStepBudgetReceipt,
|
||||
createConsultationTools,
|
||||
createConsultationRuntimeState,
|
||||
} from "../src/mastra/consultation-tools.ts";
|
||||
import { getJyotishAgent } from "../src/mastra/index.ts";
|
||||
import { consultationAgentPublicEventSchema, createNdjsonParser } from "../src/lib/consultation-agent-events.ts";
|
||||
import { collectAgentPublicEvents, streamAgentResponse } from "../src/lib/stream-agent-response.ts";
|
||||
import { createConsultationPlan } from "../src/lib/consultation-plan.ts";
|
||||
import { collectAgentPublicEvents, ensureFinalResponseText, streamAgentResponse } from "../src/lib/stream-agent-response.ts";
|
||||
|
||||
const serverChart = {
|
||||
name: "测试",
|
||||
@@ -36,23 +42,35 @@ function workflow(
|
||||
test("context-bound tool keeps legacy single theme compatibility and calculates once", async () => {
|
||||
let calls = 0;
|
||||
let captured: unknown;
|
||||
let capturedPlan: unknown;
|
||||
const state = createConsultationRuntimeState();
|
||||
const plan = createConsultationPlan({
|
||||
userIntent: "事业如何", theme: "career", consultationMode: "unverified_birth_time", modelCreditCost: 1,
|
||||
});
|
||||
assert.throws(
|
||||
() => (plan.requestedDomains as unknown as string[]).push("timing"),
|
||||
TypeError,
|
||||
);
|
||||
const tools = createConsultationTools({
|
||||
userId: "u", sessionId: "s", requestId: "r", consultationMode: "unverified_birth_time",
|
||||
serverChart, state,
|
||||
runWorkflow: async (input) => { calls += 1; captured = input; return workflow(); },
|
||||
plan,
|
||||
theme: "career", serverChart, state,
|
||||
runWorkflow: async (input, options) => {
|
||||
calls += 1; captured = input; capturedPlan = options?.plan; return workflow();
|
||||
},
|
||||
});
|
||||
const tool = tools["run-jyotish-consultation"];
|
||||
assert.deepEqual(Object.keys((tool.inputSchema as unknown as { shape: object }).shape), ["question", "domains", "theme"]);
|
||||
const execute = tool.execute!;
|
||||
const context = { observe: { span: async (_n: string, fn: () => Promise<unknown>) => fn(), log() {} } } as never;
|
||||
const [first, second] = await Promise.all([
|
||||
execute({ question: "事业如何", theme: "career" }, context),
|
||||
execute({ question: "事业如何", theme: "career" }, context),
|
||||
execute({ question: "尝试改成精确应期", theme: "timing" }, context),
|
||||
execute({ question: "尝试改成婚恋", theme: "marriage" }, context),
|
||||
]);
|
||||
assert.equal(calls, 1);
|
||||
assert.deepEqual(first, second);
|
||||
assert.deepEqual(captured, { ...serverChart.toolInput, entryMode: "direct_chart", question: "事业如何", theme: "career" });
|
||||
assert.strictEqual(capturedPlan, plan);
|
||||
assert.equal(state.consultationToolCallCount, 1);
|
||||
assert.equal(state.workflowReceipt?.preciseTiming, "blocked");
|
||||
assert.deepEqual(state.workflowReceipt?.domains, ["career"]);
|
||||
@@ -211,12 +229,25 @@ test("incremental NDJSON parser handles arbitrary chunk boundaries", () => {
|
||||
});
|
||||
|
||||
|
||||
|
||||
test("uses a bounded dynamic step budget and reports truncation", () => {
|
||||
const state = createConsultationRuntimeState({ plannedSteps: 1, reservedValidationSteps: 1 });
|
||||
assert.deepEqual(state.stepBudget, { planned: 1, reservedValidation: 1, total: 2 });
|
||||
assert.equal(appendConsultationRuntimeStep(state, { kind: "skill", name: "jyotish-vedic-astrology", status: "completed" }), true);
|
||||
assert.equal(appendConsultationRuntimeStep(state, { kind: "validation", name: "ensure-final-response", status: "completed" }), true);
|
||||
assert.equal(appendConsultationRuntimeStep(state, { kind: "tool", name: "unexpected-extra-step", status: "completed" }), false);
|
||||
assert.equal(state.steps.length, 2);
|
||||
assert.equal(state.stepsTruncated, true);
|
||||
assert.deepEqual(consultationStepBudgetReceipt(state), { planned: 2, used: 2, remaining: 0, truncated: true });
|
||||
});
|
||||
|
||||
function receipt(state: ReturnType<typeof createConsultationRuntimeState>) {
|
||||
return {
|
||||
runId: "run",
|
||||
runtime: "mastra-agentic" as const,
|
||||
skill: { name: "jyotish-vedic-astrology" as const, loaded: state.jyotishSkillLoaded },
|
||||
steps: state.steps,
|
||||
stepBudget: consultationStepBudgetReceipt(state),
|
||||
workflow: state.workflowReceipt ?? { route: "career", status: "ready", preciseTiming: "blocked", missingLayers: [] },
|
||||
};
|
||||
}
|
||||
@@ -271,6 +302,31 @@ test("incomplete runtime contract fails without saving a successful answer", asy
|
||||
assert.equal(events.filter((event) => (event as { type?: string }).type === "run.failed").length, 1);
|
||||
});
|
||||
|
||||
test("ensures a controlled final response after a successful tool-only run", async () => {
|
||||
const state = createConsultationRuntimeState();
|
||||
state.jyotishSkillLoaded = true;
|
||||
state.consultationToolCallCount = 1;
|
||||
state.consultationToolCompleted = true;
|
||||
state.workflowReceipt = { route: "career", status: "ready", preciseTiming: "blocked", missingLayers: [] };
|
||||
let completedOutput = "";
|
||||
async function* chunks() {
|
||||
yield { type: "tool-result", payload: { toolCallId: "tool-1", toolName: "run-jyotish-consultation", result: {} } };
|
||||
}
|
||||
const response = streamAgentResponse({
|
||||
runId: "run", requestId: "req", state, stream: chunks(), requireTool: true,
|
||||
toolStatus: () => "ready", receipt: () => receipt(state),
|
||||
onComplete: (output) => { completedOutput = output; },
|
||||
});
|
||||
const events: unknown[] = [];
|
||||
const parser = createNdjsonParser((event) => events.push(event));
|
||||
parser.finish(await response.text());
|
||||
assert.equal(ensureFinalResponseText("", true), completedOutput);
|
||||
assert.match(completedOutput, /计算已完成/);
|
||||
assert.equal(events.filter((event) => (event as { type?: string }).type === "answer.delta").length, 1);
|
||||
assert.equal(events.filter((event) => (event as { type?: string }).type === "run.completed").length, 1);
|
||||
assert.equal(state.steps.at(-1)?.name, "ensure-final-response");
|
||||
});
|
||||
|
||||
|
||||
test("persistence failure emits run.failed instead of run.completed", async () => {
|
||||
const state = createConsultationRuntimeState();
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import { createConsultationPlan } from "../src/lib/consultation-plan.ts";
|
||||
import { toAgentConsultationContext, toModelOutput } from "../src/mastra/consultation-workflow.ts";
|
||||
|
||||
|
||||
test("passes transparent public-case references into the agent context", () => {
|
||||
const workflowSource = readFileSync(new URL("../src/mastra/consultation-workflow.ts", import.meta.url), "utf8");
|
||||
@@ -49,3 +52,100 @@ test("keeps strength, Ashtakavarga, and timing evidence available to the answer
|
||||
assert.match(workflowSource, /numerical_parity: record\(data\.external_parity_gate\)/);
|
||||
assert.match(workflowSource, /real_case_calibration: record\(data\.real_case_calibration\)/);
|
||||
});
|
||||
|
||||
test("projects only bounded server-selected evidence to the model", () => {
|
||||
const context = toAgentConsultationContext({
|
||||
success: true,
|
||||
question: "事业如何",
|
||||
chart: {
|
||||
birth: { date: "1990-01-02", time: "03:04", latitude: 25.03, longitude: 121.56 },
|
||||
ascendant: { sign: "Leo", degree: 12.5, reported_time: "private-reported-time" },
|
||||
planets: [{ name: "Sun", sign: "Capricorn", degree: 4.2, lng: "private-lng" }],
|
||||
houses: { first: { sign: "Leo" } },
|
||||
dasha: { current: "Mars", next: "Rahu", utc_offset: "private-utc-offset" },
|
||||
shadbala: { planets: { Sun: { total_rupa: 7.1 } } },
|
||||
ashtakavarga: { house_scores: { "10": 31 } },
|
||||
modules: {
|
||||
shadbala: { total: 412 },
|
||||
ashtakavarga: { total: 28 },
|
||||
dasha_boundaries: { next: "2027-03", utc_offset: "private-module-utc-offset" },
|
||||
narayana_dasha: { current: "Aries" },
|
||||
},
|
||||
},
|
||||
routing: { primary_theme: "career" },
|
||||
thematic_report: {
|
||||
themes: {
|
||||
career: {
|
||||
theme: "career",
|
||||
summary: "保留这条服务端证据摘要",
|
||||
narrative: "事业证据叙事",
|
||||
strength: "strong",
|
||||
account_balance: "private-account-balance",
|
||||
recommendations: ["保持稳健推进"],
|
||||
evidence: [{
|
||||
technique: "D10-Dashamsha-local",
|
||||
chart: "D10",
|
||||
conclusion: "D10 事业分盘已进入证据链",
|
||||
sentiment: "positive",
|
||||
strength: "strong",
|
||||
details: {
|
||||
source: "chart",
|
||||
derived: true,
|
||||
entitlements: "private-entitlements",
|
||||
access_scopes: "private-access-scopes",
|
||||
},
|
||||
}],
|
||||
},
|
||||
},
|
||||
},
|
||||
consumer_context: {
|
||||
route: "career", core_status: "ready", available_layers: ["natal", "timing"],
|
||||
missing_route_layers: [], hard_blockers: [],
|
||||
technique_truth: { status: "verified", access_scopes: "private-validation-scopes" },
|
||||
answer_policy: {
|
||||
can_answer_direction: true,
|
||||
can_answer_precise_timing: false,
|
||||
entitlements: "private-policy-entitlements",
|
||||
},
|
||||
user_facing_limitation: "精确月份暂不可用",
|
||||
},
|
||||
reference_transparency: { similar_public_cases: { status: "public_context_only" } },
|
||||
rectification: { summary: "none" },
|
||||
});
|
||||
const output = toModelOutput(context);
|
||||
const careerPlan = createConsultationPlan({ userIntent: "事业如何", theme: "career" });
|
||||
const careerOutput = toModelOutput(context, careerPlan);
|
||||
const serialized = JSON.stringify(output);
|
||||
|
||||
assert.equal(output.packet_version, "consultation-evidence-packet-v2");
|
||||
assert.equal("birth" in output, false);
|
||||
assert.equal(serialized.includes("1990-01-02"), false);
|
||||
assert.equal(serialized.includes("03:04"), false);
|
||||
assert.equal(serialized.includes("25.03"), false);
|
||||
assert.equal(serialized.includes("121.56"), false);
|
||||
for (const privateValue of [
|
||||
"private-reported-time", "private-lng", "private-utc-offset", "private-module-utc-offset",
|
||||
"private-account-balance", "private-entitlements", "private-access-scopes",
|
||||
"private-validation-scopes", "private-policy-entitlements",
|
||||
]) {
|
||||
assert.equal(serialized.includes(privateValue), false);
|
||||
}
|
||||
for (const privateKey of [
|
||||
"reported_time", "lng", "utc_offset", "account_balance", "entitlements", "access_scopes",
|
||||
]) {
|
||||
assert.equal(serialized.includes(`"${privateKey}"`), false);
|
||||
}
|
||||
assert.equal(serialized.includes("保留这条服务端证据摘要"), true);
|
||||
assert.equal(serialized.includes("D10-Dashamsha-local"), true);
|
||||
assert.equal(serialized.includes("total_rupa"), true);
|
||||
assert.equal(serialized.includes("house_scores"), true);
|
||||
assert.equal(serialized.includes('"dasha"'), true);
|
||||
assert.equal(serialized.includes("narayana_dasha"), true);
|
||||
assert.equal(serialized.includes('"status":"verified"'), true);
|
||||
const answerPolicy = output.evidence_contract.answer_policy as Record<string, unknown>;
|
||||
assert.equal(answerPolicy.can_answer_precise_timing, false);
|
||||
assert.deepEqual(output.claim_cards.map((card) => card.category), ["natal_foundation", "domain", "timing", "validation"]);
|
||||
assert.deepEqual(careerOutput.claim_cards.map((card) => card.category), ["natal_foundation", "domain"]);
|
||||
assert.equal(JSON.stringify(careerOutput).includes('"dasha"'), false);
|
||||
assert.equal(serialized.includes("consultation-evidence-packet-v2"), true);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
ConsultationPlanValidationError,
|
||||
consultationPlanMaxCreditCost,
|
||||
createConsultationPlan,
|
||||
validateConsultationPlan,
|
||||
} from "../src/lib/consultation-plan.ts";
|
||||
import { consultationDomainIds } from "../src/lib/consultation-domain-registry.ts";
|
||||
|
||||
for (const theme of consultationDomainIds) {
|
||||
test(`creates a bounded server-owned ${theme} plan`, () => {
|
||||
const plan = createConsultationPlan({
|
||||
userIntent: "请回答我的问题",
|
||||
theme,
|
||||
consultationMode: "verified_chart",
|
||||
modelCreditCost: 1,
|
||||
});
|
||||
assert.equal(plan.plan_version, "consultation-plan-v2");
|
||||
assert.equal(plan.birthTimeMode, "verified_chart");
|
||||
assert.equal(plan.precisionBoundary, "server_evidence_required");
|
||||
assert.equal(plan.maxCreditCost, consultationPlanMaxCreditCost);
|
||||
});
|
||||
}
|
||||
|
||||
test("rejects a theme outside the consultation allowlist", () => {
|
||||
assert.throws(
|
||||
() => createConsultationPlan({
|
||||
userIntent: "请回答我的问题",
|
||||
theme: "medical" as "career",
|
||||
consultationMode: "verified_chart",
|
||||
}),
|
||||
(error: unknown) => error instanceof z.ZodError,
|
||||
);
|
||||
});
|
||||
|
||||
test("server birth-time mode fixes the precision boundary", () => {
|
||||
assert.equal(createConsultationPlan({
|
||||
userIntent: "未来什么时候适合行动",
|
||||
theme: "timing",
|
||||
consultationMode: "unverified_birth_time",
|
||||
}).precisionBoundary, "precise_timing_blocked");
|
||||
assert.equal(createConsultationPlan({
|
||||
userIntent: "什么是上升星座",
|
||||
theme: "general",
|
||||
consultationMode: "general_no_birth_time",
|
||||
}).precisionBoundary, "precise_timing_blocked");
|
||||
});
|
||||
|
||||
test("rejects mode or precision tampering", () => {
|
||||
const plan = createConsultationPlan({
|
||||
userIntent: "未来什么时候适合行动",
|
||||
theme: "timing",
|
||||
consultationMode: "unverified_birth_time",
|
||||
});
|
||||
assert.throws(
|
||||
() => validateConsultationPlan({ ...plan, precisionBoundary: "server_evidence_required" }, {
|
||||
consultationMode: "unverified_birth_time",
|
||||
modelCreditCost: 1,
|
||||
}),
|
||||
(error: unknown) => error instanceof ConsultationPlanValidationError
|
||||
&& error.code === "plan_precision_mismatch",
|
||||
);
|
||||
assert.throws(
|
||||
() => validateConsultationPlan(plan, {
|
||||
consultationMode: "verified_chart",
|
||||
modelCreditCost: 1,
|
||||
}),
|
||||
(error: unknown) => error instanceof ConsultationPlanValidationError
|
||||
&& error.code === "plan_mode_mismatch",
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects model cost above the release consultation ceiling", () => {
|
||||
assert.throws(
|
||||
() => createConsultationPlan({
|
||||
userIntent: "事业如何",
|
||||
theme: "career",
|
||||
consultationMode: "verified_chart",
|
||||
modelCreditCost: consultationPlanMaxCreditCost + 1,
|
||||
}),
|
||||
(error: unknown) => error instanceof ConsultationPlanValidationError
|
||||
&& error.code === "plan_cost_ceiling_exceeded",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
test("rejects non-positive or fractional model costs", () => {
|
||||
for (const modelCreditCost of [0, -1, 0.5, Number.NaN, Number.POSITIVE_INFINITY]) {
|
||||
assert.throws(
|
||||
() => createConsultationPlan({
|
||||
userIntent: "事业如何",
|
||||
theme: "career",
|
||||
consultationMode: "verified_chart",
|
||||
modelCreditCost,
|
||||
}),
|
||||
(error: unknown) => error instanceof ConsultationPlanValidationError
|
||||
&& error.code === "plan_cost_ceiling_exceeded",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("deep-freezes the server-owned plan and nested evidence lists", () => {
|
||||
const plan = createConsultationPlan({
|
||||
userIntent: "事业如何",
|
||||
theme: "career",
|
||||
consultationMode: "verified_chart",
|
||||
modelCreditCost: 1,
|
||||
});
|
||||
const mutable = plan as unknown as {
|
||||
precisionBoundary: string;
|
||||
requestedDomains: string[];
|
||||
requiredEvidenceCategories: string[];
|
||||
};
|
||||
|
||||
assert.equal(Object.isFrozen(plan), true);
|
||||
assert.equal(Object.isFrozen(plan.requestedDomains), true);
|
||||
assert.equal(Object.isFrozen(plan.requiredEvidenceCategories), true);
|
||||
assert.throws(() => { mutable.precisionBoundary = "precise_timing_blocked"; }, TypeError);
|
||||
assert.throws(() => mutable.requestedDomains.push("timing"), TypeError);
|
||||
assert.throws(() => mutable.requiredEvidenceCategories.splice(0, 1), TypeError);
|
||||
assert.equal(plan.precisionBoundary, "server_evidence_required");
|
||||
assert.deepEqual(plan.requestedDomains, ["career"]);
|
||||
assert.deepEqual(plan.requiredEvidenceCategories, ["natal_foundation", "domain"]);
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { createConsultationReplyMetadata, consultationReplyMetadataSchema } from "../src/lib/consultation-reply-metadata.ts";
|
||||
import { parseAgentReply } from "../src/lib/agent-reply.ts";
|
||||
|
||||
for (const theme of ["career", "marriage", "wealth", "timing", "general"] as const) {
|
||||
test(`server-owned metadata is bounded for ${theme}`, () => {
|
||||
const metadata = createConsultationReplyMetadata({ theme, question: "未来的重点是什么?" });
|
||||
const parsed = consultationReplyMetadataSchema.parse(metadata);
|
||||
assert.equal(parsed.suggestions.length, 3);
|
||||
assert.ok(parsed.suggestions.every((item) => item.length > 0 && item.length <= 80));
|
||||
assert.ok(parsed.title.length >= 6 && parsed.title.length <= 14);
|
||||
assert.doesNotMatch(parsed.title, /[\d\p{P}\p{S}]/u);
|
||||
});
|
||||
}
|
||||
|
||||
test("server metadata wins without blocking legacy hidden-block parsing", () => {
|
||||
const reply = parseAgentReply(
|
||||
[
|
||||
"回答正文",
|
||||
'<!--AYANAM_SUGGESTIONS:["模型问题一","模型问题二","模型问题三"]-->',
|
||||
"<!--AYANAM_TITLE:模型标题-->",
|
||||
].join("\n"),
|
||||
"career",
|
||||
createConsultationReplyMetadata({ theme: "career", question: "工作变化的重点是什么?" }),
|
||||
);
|
||||
|
||||
assert.equal(reply.text, "回答正文");
|
||||
assert.deepEqual(reply.suggestions, ["我更适合怎样的职业路径?", "未来一年事业上要避开什么?", "我该如何发挥自己的优势?"]);
|
||||
assert.equal(reply.title, "工作变化的重点是什么");
|
||||
});
|
||||
@@ -33,14 +33,21 @@ test("route service loads complete server truth before billing and uses only rep
|
||||
order.push("profile");
|
||||
return profile;
|
||||
},
|
||||
beforeReserve({ consultationMode, serverChart }) {
|
||||
order.push("plan");
|
||||
assert.equal(consultationMode, "unverified_birth_time");
|
||||
assert.equal(serverChart?.truth.selectedTimeKind, "reported");
|
||||
return Object.freeze({ version: "consultation-plan-v2" });
|
||||
},
|
||||
async reserve() {
|
||||
order.push("reserve");
|
||||
return { reservation: "ok" };
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(order, ["profile", "reserve"]);
|
||||
assert.deepEqual(order, ["profile", "plan", "reserve"]);
|
||||
assert.deepEqual(prepared.reservation, { reservation: "ok" });
|
||||
assert.deepEqual(prepared.preReserveResult, { version: "consultation-plan-v2" });
|
||||
assert.deepEqual(prepared.serverChart?.toolInput, {
|
||||
year: 1997,
|
||||
month: 8,
|
||||
@@ -63,6 +70,32 @@ test("route service loads complete server truth before billing and uses only rep
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
test("pre-reserve plan guard sees resolved server mode and blocks billing on failure", async () => {
|
||||
const order: string[] = [];
|
||||
await assert.rejects(prepareConsultationRoute({
|
||||
userId: "user-1",
|
||||
mode: "general_no_birth_time",
|
||||
loadProfile: async () => ({
|
||||
...profile,
|
||||
reported_birth_time: "14:49:00",
|
||||
birth_time_source: "family_exact",
|
||||
birth_time_status: "reported",
|
||||
}),
|
||||
beforeReserve({ consultationMode, serverChart }) {
|
||||
order.push("plan");
|
||||
assert.equal(consultationMode, "unverified_birth_time");
|
||||
assert.equal(serverChart?.toolInput.minute, 49);
|
||||
throw new Error("plan rejected");
|
||||
},
|
||||
reserve: async () => {
|
||||
order.push("reserve");
|
||||
return "reserved";
|
||||
},
|
||||
}), /plan rejected/);
|
||||
assert.deepEqual(order, ["plan"]);
|
||||
});
|
||||
|
||||
test("verified route uses only server active time", async () => {
|
||||
const prepared = await prepareConsultationRoute({
|
||||
userId: "user-1",
|
||||
|
||||
@@ -6,9 +6,59 @@ const route = readFileSync(new URL("../src/app/api/consult/route.ts", import.met
|
||||
const reportsRoute = readFileSync(new URL("../src/app/api/reports/route.ts", import.meta.url), "utf8");
|
||||
const mastra = readFileSync(new URL("../src/mastra/index.ts", import.meta.url), "utf8");
|
||||
const tools = readFileSync(new URL("../src/mastra/consultation-tools.ts", import.meta.url), "utf8");
|
||||
const stream = readFileSync(new URL("../src/lib/stream-agent-response.ts", import.meta.url), "utf8");
|
||||
const workflow = readFileSync(new URL("../src/mastra/consultation-workflow.ts", import.meta.url), "utf8");
|
||||
const plan = readFileSync(new URL("../src/lib/consultation-plan.ts", import.meta.url), "utf8");
|
||||
const routeService = readFileSync(new URL("../src/lib/consultation-route-service.ts", import.meta.url), "utf8");
|
||||
const pythonPlanContract = readFileSync(new URL("../../scripts/consultation_plan_contract.py", import.meta.url), "utf8");
|
||||
const stagingCompose = readFileSync(new URL("../../deploy/docker-compose.staging.yml", import.meta.url), "utf8");
|
||||
|
||||
|
||||
|
||||
test("consultation plans are server-owned and bounded", () => {
|
||||
assert.match(plan, /consultationPlanSchema/);
|
||||
assert.match(plan, /requestedDomains/);
|
||||
assert.match(plan, /requiredEvidenceCategories/);
|
||||
assert.match(plan, /createConsultationPlan/);
|
||||
assert.match(tools, /const userIntent = ctx\.plan\?\.userIntent \?\? input\.question/);
|
||||
assert.match(tools, /input\.domains === undefined && context\.plan && context\.theme/);
|
||||
assert.match(tools, /return \[context\.theme\]/);
|
||||
assert.match(tools, /const domainPlan = ctx\.plan[\s\S]*createConsultationPlan\(\{/);
|
||||
assert.match(tools, /userIntent,[\s\S]*theme: domain/);
|
||||
assert.match(tools, /question: userIntent,[\s\S]*theme: domain/);
|
||||
assert.match(tools, /plan: domainPlan/);
|
||||
assert.match(workflow, /packet_version: "consultation-evidence-packet-v2"/);
|
||||
assert.match(workflow, /claim_cards/);
|
||||
assert.match(tools, /modelOutput: toModelOutput\(agentContext, domainPlan\)/);
|
||||
assert.match(tools, /modelOutput: toModelOutput[\s\S]*ctx\.state\.consultationToolCompleted = true/);
|
||||
assert.match(plan, /precisionBoundary/);
|
||||
assert.match(plan, /consultationPlanMaxCreditCost = 1/);
|
||||
const modelToolInput = tools.slice(tools.indexOf("const consultationToolInputSchema"), tools.indexOf("const MAX_RECORDED_STEPS"));
|
||||
assert.doesNotMatch(modelToolInput, /birth|latitude|longitude|engine|script|credit/i);
|
||||
const prepareCall = route.slice(route.indexOf("prepared = await prepareConsultationRoute"), route.indexOf("const modelSelection"));
|
||||
assert.match(prepareCall, /beforeReserve: \(\{ consultationMode \}\) => createConsultationPlan\(\{/);
|
||||
assert.ok(prepareCall.indexOf("beforeReserve:") < prepareCall.indexOf("reserve: () => reserveConsultationModel("));
|
||||
assert.ok(routeService.indexOf("const preReserveResult = input.beforeReserve") < routeService.indexOf("const reservation = await input.reserve()"));
|
||||
assert.match(route, /plan: prepared\.preReserveResult,[\s\S]*theme: consultationTheme/);
|
||||
assert.match(route, /runConsultationWorkflow\(toolInput, \{[\s\S]*plan: prepared\.preReserveResult/);
|
||||
assert.match(workflow, /plan_version: workflowRequest\.plan_version/);
|
||||
assert.match(workflow, /strict_workflow_route: workflowRequest\.strictWorkflowRoute/);
|
||||
assert.match(workflow, /required_layers: workflowRequest\.requiredLayers/);
|
||||
assert.match(workflow, /claim_boundary: workflowRequest\.claimBoundary/);
|
||||
assert.match(workflow, /precision_boundary: workflowRequest\.precision_boundary/);
|
||||
assert.match(workflow, /required_evidence_categories: workflowRequest\.required_evidence_categories/);
|
||||
assert.match(pythonPlanContract, /PLAN_VERSION = "consultation-plan-v2"/);
|
||||
assert.match(pythonPlanContract, /server_allowlist_validated/);
|
||||
assert.match(pythonPlanContract, /can_answer_precise_timing": False/);
|
||||
});
|
||||
|
||||
test("uses one runtime step append entry and no scattered hard-coded step cap", () => {
|
||||
assert.match(tools, /export function appendConsultationRuntimeStep/);
|
||||
assert.doesNotMatch(tools, /steps\.length\s*>=\s*32/);
|
||||
assert.doesNotMatch(stream, /state\.steps\.push/);
|
||||
assert.doesNotMatch(stream, /steps\.length\s*<\s*32/);
|
||||
});
|
||||
|
||||
test("personal consultation lets the Agent invoke the server-bound workflow tool", () => {
|
||||
const agenticStart = route.indexOf("async function runAgenticConsultation");
|
||||
const agenticBranch = route.slice(
|
||||
@@ -22,6 +72,7 @@ test("personal consultation lets the Agent invoke the server-bound workflow tool
|
||||
assert.match(tools, /\(ctx\.runWorkflow \?\? runConsultationWorkflow\)\(toolInput, \{/);
|
||||
assert.match(tools, /return \{ "run-jyotish-consultation": consultationTool \};/);
|
||||
assert.match(agenticBranch, /state\.workflowReceipt\?\.preciseTiming === "allowed"/);
|
||||
assert.match(route, /createConsultationReplyMetadata/);
|
||||
});
|
||||
|
||||
test("defers optional external evidence only for foreground chat", () => {
|
||||
@@ -52,6 +103,8 @@ test("validates and emits non-sensitive workflow and execution receipts", () =>
|
||||
test("carries commercial technique truth into the model contract", () => {
|
||||
assert.match(workflow, /technique_truth/);
|
||||
assert.match(mastra, /deterministic_claims_forbidden_for/);
|
||||
assert.doesNotMatch(mastra, /AYANAM_SUGGESTIONS|AYANAM_TITLE/);
|
||||
assert.doesNotMatch(mastra, /2-5 short paragraphs/);
|
||||
assert.match(mastra, /reference_only/);
|
||||
assert.match(mastra, /Do not use a restricted technique/);
|
||||
});
|
||||
@@ -67,9 +120,9 @@ test("projects consultation themes through explicit strict workflow taxonomy", (
|
||||
});
|
||||
|
||||
|
||||
test("staging enables the agentic consultation runtime without changing production compose", () => {
|
||||
test("agentic consultation is the safe default and legacy is explicit rollback", () => {
|
||||
assert.match(stagingCompose, /CONSULTATION_AGENTIC_RUNTIME: enabled/);
|
||||
assert.match(route, /CONSULTATION_AGENTIC_RUNTIME/);
|
||||
assert.match(route, /legacy/);
|
||||
assert.match(route, /canary/);
|
||||
assert.match(route, /CONSULTATION_AGENTIC_RUNTIME\?\.trim\(\)\.toLowerCase\(\) \?\? "enabled"/);
|
||||
assert.match(route, /mode === "legacy"/);
|
||||
assert.match(route, /mode !== "canary"/);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { createConsultationPlan } from "../src/lib/consultation-plan.ts";
|
||||
import { projectConsultationWorkflowRequest } from "../src/lib/consultation-workflow-request.ts";
|
||||
import { runConsultationWorkflow } from "../src/mastra/index.ts";
|
||||
import type { ConsultationInput } from "../src/mastra/index.ts";
|
||||
@@ -9,7 +10,12 @@ test("timing questions use a legal report theme and preserve a timing route hint
|
||||
const question = "未来哪些阶段值得把握?";
|
||||
|
||||
// When: its workflow request is projected for the Python service.
|
||||
const request = projectConsultationWorkflowRequest(question, "timing");
|
||||
const plan = createConsultationPlan({
|
||||
userIntent: question,
|
||||
theme: "timing",
|
||||
consultationMode: "unverified_birth_time",
|
||||
});
|
||||
const request = projectConsultationWorkflowRequest(question, "timing", plan);
|
||||
|
||||
// Then: the public theme remains a canonical allowlisted domain with strict route metadata.
|
||||
assert.equal(request.question, "应期与阶段问题:未来哪些阶段值得把握?");
|
||||
@@ -18,6 +24,9 @@ test("timing questions use a legal report theme and preserve a timing route hint
|
||||
assert.ok(request.requiredLayers.includes("Narayana"));
|
||||
assert.ok(request.requiredLayers.includes("negative holdout gate"));
|
||||
assert.equal(request.claimBoundary, "candidate_day_month_window_only_until_holdout_passes");
|
||||
assert.equal(request.plan_version, "consultation-plan-v2");
|
||||
assert.equal(request.precision_boundary, "precise_timing_blocked");
|
||||
assert.equal(request.timing_horizon, "next_12_months");
|
||||
});
|
||||
|
||||
test("consultation workflow allows a cold engine run to finish", async () => {
|
||||
@@ -68,7 +77,13 @@ test("timing input projects only legal private workflow fields", async () => {
|
||||
|
||||
try {
|
||||
// When: the adapter invokes the Python workflow.
|
||||
await runConsultationWorkflow(input);
|
||||
await runConsultationWorkflow(input, {
|
||||
plan: createConsultationPlan({
|
||||
userIntent: input.question,
|
||||
theme: input.theme,
|
||||
consultationMode: "unverified_birth_time",
|
||||
}),
|
||||
});
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
@@ -79,6 +94,13 @@ test("timing input projects only legal private workflow fields", async () => {
|
||||
assert.equal(body.question_text, "应期与阶段问题:未来哪些阶段值得把握?");
|
||||
assert.deepEqual(body.theme, ["timing"]);
|
||||
assert.match(requestBody, /"timing"/);
|
||||
assert.equal(body.plan_version, "consultation-plan-v2");
|
||||
assert.equal(body.strict_workflow_route, "timing");
|
||||
assert.ok(body.required_layers.includes("negative holdout gate"));
|
||||
assert.equal(body.claim_boundary, "candidate_day_month_window_only_until_holdout_passes");
|
||||
assert.equal(body.precision_boundary, "precise_timing_blocked");
|
||||
assert.deepEqual(body.requested_domains, ["timing"]);
|
||||
assert.deepEqual(body.required_evidence_categories, ["natal_foundation", "timing", "validation"]);
|
||||
assert.equal(input.question, "未来哪些阶段值得把握?");
|
||||
assert.equal(input.theme, "timing");
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { readFileSync, realpathSync } from "node:fs";
|
||||
import { basename } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
RECTIFICATION_AGENT_HARD_STEP_LIMIT,
|
||||
RECTIFICATION_AGENT_MAX_STEPS,
|
||||
RECTIFICATION_AGENT_STEP_BUDGETS,
|
||||
RECTIFICATION_V9_PACKAGE_PATH,
|
||||
RECTIFICATION_V9_SKILL_PATH,
|
||||
RECTIFICATION_V9_SKILL_NAME,
|
||||
getRectificationV9Agent,
|
||||
@@ -60,11 +62,10 @@ test("system prompt carries only high-priority boundaries, never the method copy
|
||||
|
||||
test("agent pins the dedicated rectification skill and its fixed version", () => {
|
||||
assert.equal(RECTIFICATION_V9_SKILL_NAME, "jyotish-birth-time-rectification");
|
||||
assert.ok(
|
||||
RECTIFICATION_V9_SKILL_PATH.endsWith(
|
||||
"skills/jyotish-birth-time-rectification/versions/10.0.0",
|
||||
),
|
||||
);
|
||||
assert.equal(basename(RECTIFICATION_V9_SKILL_PATH), RECTIFICATION_V9_SKILL_NAME);
|
||||
assert.ok(RECTIFICATION_V9_PACKAGE_PATH.endsWith("skills/jyotish-birth-time-rectification/versions/10.0.0"));
|
||||
assert.notEqual(RECTIFICATION_V9_SKILL_PATH, RECTIFICATION_V9_PACKAGE_PATH);
|
||||
assert.equal(realpathSync(RECTIFICATION_V9_SKILL_PATH), RECTIFICATION_V9_PACKAGE_PATH);
|
||||
assert.equal(RECTIFICATION_SKILL_NAME, "jyotish-birth-time-rectification");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.0");
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, isAbsolute, join, resolve } from "node:path";
|
||||
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { test, type TestContext } from "node:test";
|
||||
import {
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
createSkillPackageRegistry,
|
||||
resolveActiveSkillPackage,
|
||||
resolveExactSkillPackage,
|
||||
resolveSkillPackageRuntimePath,
|
||||
resolveSkillPackageVersion,
|
||||
verifyAllActiveSkillPackages,
|
||||
type SkillPackageIdentity,
|
||||
@@ -111,6 +112,10 @@ test("checked-in registry verifies all three active packages and returns absolut
|
||||
projectRoot,
|
||||
});
|
||||
assert.equal(active.version, "6.9.14");
|
||||
const runtimePath = resolveSkillPackageRuntimePath(active);
|
||||
assert.equal(basename(runtimePath), active.name);
|
||||
assert.equal(realpathSync(runtimePath), active.resolvedPath);
|
||||
assert.notEqual(runtimePath, active.resolvedPath);
|
||||
assert.deepEqual(
|
||||
resolveExactSkillPackage(active.name, active.version, active.sha256, {
|
||||
projectRoot,
|
||||
|
||||
Reference in New Issue
Block a user