feat(consultation): let the jyotish agent drive skills and tools

This commit is contained in:
Jesse_Chen
2026-08-11 16:22:02 +08:00
parent 6d7a9a97be
commit 46cdc3bbf4
24 changed files with 1336 additions and 306 deletions
@@ -36,7 +36,9 @@ test("standard consultation awaits real usage before durable response settlement
assert.match(consultRoute, /async function usagePayload\(usage: Promise<\{ inputTokens\?: number; outputTokens\?: number \}>\)/);
assert.match(consultRoute, /const resolved = await usage;/);
assert.match(consultRoute, /const actualUsage = await usagePayload\(usage\);[\s\S]*p_actual_usage: actualUsage/);
assert.equal(consultRoute.match(/result\.totalUsage/g)?.length, 4);
assert.match(consultRoute, /function mergeUsage\(usages: Promise<Usage>\[\]\): Promise<Usage> \{[\s\S]*Promise\.all\(usages\)/);
assert.equal(consultRoute.match(/usages\.push\(result\.totalUsage\)/g)?.length, 2);
assert.equal(consultRoute.match(/usages\.push\(retried\.totalUsage\)/g)?.length, 2);
});
test("standard consultation forwards its stable reservation request as the usage event key", async () => {
+18 -1
View File
@@ -1,7 +1,7 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { writeChatSession, type ChatSessionWrite } from "../src/lib/chat-session-write-contract.ts";
import { chatSessionWriteSchema, writeChatSession, type ChatSessionWrite } from "../src/lib/chat-session-write-contract.ts";
const sessionId = "11111111-1111-4111-8111-111111111111";
const values = {
@@ -14,6 +14,23 @@ const values = {
updated_at: "2026-07-22T00:00:00.000Z",
} satisfies ChatSessionWrite;
test("chat session schema preserves the safe agent execution receipt", () => {
const receipt = {
runId: "run-1",
runtime: "mastra-agentic" as const,
skill: { name: "jyotish-vedic-astrology" as const, loaded: true },
steps: [{ sequence: 1, kind: "skill" as const, name: "jyotish-vedic-astrology", status: "completed" as const }],
workflow: { route: "career", status: "ready", preciseTiming: "blocked", missingLayers: [] },
techniqueTruth: "verified",
};
const parsed = chatSessionWriteSchema.parse({
...values,
messages: [{ role: "assistant", text: "回答", agentExecutionReceipt: receipt }],
});
assert.deepEqual(parsed.messages[0]?.agentExecutionReceipt, receipt);
});
test("chat session writes use same-origin API instead of browser-to-Supabase requests", async () => {
const calls: Array<{ url: string; init?: RequestInit }> = [];
await writeChatSession(sessionId, values, "update", async (url, init) => {
+13 -3
View File
@@ -45,15 +45,25 @@ test("does not duplicate a completed assistant answer while loading state settle
});
test("shows honest agent activity states before and during streamed text", () => {
assert.match(messageRowSource, /message\.state === "thinking"[\s\S]*?\? "working"/);
assert.match(messageRowSource, /message\.state === "thinking" \? "正在核对星盘信息…"/);
const activity = { phase: "chart-calculation", label: "正在计算本命盘…" } as const;
const view = chatMessageViews(previousMessages, true, "", activity).at(-1);
assert.deepEqual(view?.activity, activity);
assert.match(messageRowSource, /"loading-method": "searching"/);
assert.match(messageRowSource, /"chart-calculation": "solving"/);
assert.match(messageRowSource, /"evidence-validation": "working"/);
assert.match(messageRowSource, /"answer-composition": "composing"/);
assert.match(messageRowSource, /message\.activity\?\.label/);
assert.match(messageRowSource, /message\.state !== "settled"/);
assert.match(messageRowSource, /message\.state === "thinking" \? "working" : "composing"/);
assert.match(messageRowSource, /message\.text && <ChatMessageContent text=\{message\.text\}/);
assert.match(activitySource, /<ThinkingOrb aria-hidden="true" state=\{state\} size=\{20\}/);
assert.doesNotMatch(activitySource, /CircleCheck|回答已完成|completed/);
assert.doesNotMatch(messageRowSource, /: "completed"/);
assert.doesNotMatch(globalStyles, /\.thinking\b/);
assert.match(pageSource, /application\/x-ndjson/);
assert.match(pageSource, /createNdjsonParser/);
assert.match(pageSource, /if \(event\.type === "run\.failed"\) throw new ConsultationResponseError/);
assert.match(pageSource, /if \(!runCompleted\) throw new ConsultationResponseError/);
assert.match(pageSource, /agentExecutionReceipt = event\.receipt/);
});
test("keeps the suggestion row height stable while an answer streams", () => {
@@ -0,0 +1,192 @@
import assert from "node:assert/strict";
import test from "node:test";
import { 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";
const serverChart = {
name: "测试",
toolInput: { year: 1990, month: 1, day: 2, hour: 3, minute: 4, city: "台北", lat: 25.03, lon: 121.56, tz: 8 },
truth: {
birthDate: "1990-01-02", reportedBirthTime: "03:04", activeBirthTime: null,
selectedTimeKind: "reported" as const, birthTimeSource: "reported", birthTimeStatus: "reported",
placeLabel: "台北", placeCodes: { countryCode: "TW", provinceCode: null, cityCode: null, districtCode: null },
placeId: null, placeType: "city", placeProvider: "profile", latitude: 25.03, longitude: 121.56,
timezoneId: "Asia/Taipei", timezoneSource: "profile", timezoneOffset: 8,
},
};
function workflow() {
return {
success: true,
chart: {},
routing: { primary_theme: "career" },
consumer_context: {
route: "career", core_status: "ready" as const, available_layers: [], missing_route_layers: [], hard_blockers: [],
technique_truth: { status: "verified" },
answer_policy: { can_answer_direction: true, can_answer_precise_timing: true },
},
};
}
test("context-bound tool exposes only question/theme and calculates once", async () => {
let calls = 0;
let captured: unknown;
const state = createConsultationRuntimeState();
const tools = createConsultationTools({
userId: "u", sessionId: "s", requestId: "r", consultationMode: "unverified_birth_time",
serverChart, state,
runWorkflow: async (input) => { calls += 1; captured = input; return workflow(); },
});
const tool = tools["run-jyotish-consultation"];
assert.deepEqual(Object.keys((tool.inputSchema as unknown as { shape: object }).shape), ["question", "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),
]);
assert.equal(calls, 1);
assert.deepEqual(first, second);
assert.deepEqual(captured, { ...serverChart.toolInput, entryMode: "direct_chart", question: "事业如何", theme: "career" });
assert.equal(state.consultationToolCallCount, 1);
assert.equal(state.workflowReceipt?.preciseTiming, "blocked");
});
test("personal Agent exposes the Jyotish Skill and named server tool", async () => {
const state = createConsultationRuntimeState();
const agent = getJyotishAgent({
id: "personal-agent-probe", label: "Probe", description: "", creditCost: 1, isDefault: false,
mode: "openai", model: "openai/gpt-5-mini",
} as never, {
userId: "u", sessionId: "s", requestId: "r", consultationMode: "verified_chart", serverChart, state,
} as never);
const skills = await agent.listSkills();
const toolNames = Object.keys(await agent.getToolsForExecution({ runId: "r" }));
assert.equal(skills.some((skill) => skill.name === "jyotish-vedic-astrology"), true);
assert.equal(toolNames.includes("skill"), true);
assert.equal(toolNames.includes("run-jyotish-consultation"), true);
assert.equal(toolNames.includes("consultationTool"), false);
});
test("public stream filters private chunks and completes once", async () => {
const chunks = [
{ type: "reasoning-delta", payload: { text: "secret" } },
{ type: "tool-call", payload: { toolCallId: "c1", toolName: "skill", args: { name: "jyotish-vedic-astrology", secret: "x" } } },
{ type: "tool-result", payload: { toolCallId: "other", toolName: "skill", result: { private: true } } },
{ type: "tool-result", payload: { toolCallId: "c1", toolName: "skill", result: { private: true } } },
{ type: "tool-call", payload: { toolCallId: "c2", toolName: "run-jyotish-consultation", args: { year: 1990 } } },
{ type: "data-jyotish-activity", data: { phase: "chart-calculation", label: "正在计算本命盘", private: "x" } },
{ type: "tool-result", payload: { toolCallId: "c2", toolName: "run-jyotish-consultation", result: { birth: "private" } } },
{ type: "text-delta", payload: { text: "可以先看方向。", providerMetadata: { secret: true } } },
];
const events = await collectAgentPublicEvents(chunks as never, {
runId: "run", requestId: "req", toolStatus: () => "ready",
receipt: () => ({
runId: "run", runtime: "mastra-agentic", skill: { name: "jyotish-vedic-astrology", loaded: true },
steps: [], workflow: { route: "career", status: "ready", preciseTiming: "blocked", missingLayers: [] },
}),
});
assert.equal(events.filter((event) => event.type === "run.completed").length, 1);
assert.equal(events.some((event) => JSON.stringify(event).includes("secret") || JSON.stringify(event).includes("private") || JSON.stringify(event).includes("1990")), false);
assert.equal(events.filter((event) => event.type === "skill.completed").length, 1);
assert.equal(events.some((event) => event.type === "answer.delta"), true);
for (const event of events) consultationAgentPublicEventSchema.parse(event);
});
test("incremental NDJSON parser handles arbitrary chunk boundaries", () => {
const parsed: unknown[] = [];
const parser = createNdjsonParser((event) => parsed.push(event));
const line = `${JSON.stringify({ type: "run.started", runId: "r", requestId: "q" })}\n`;
parser.push(line.slice(0, 7));
parser.push(line.slice(7, 21));
parser.finish(line.slice(21));
assert.deepEqual(parsed, [{ type: "run.started", runId: "r", requestId: "q" }]);
});
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,
workflow: state.workflowReceipt ?? { route: "career", status: "ready", preciseTiming: "blocked", missingLayers: [] },
};
}
test("holds answer text until the Skill and server tool contract completes", async () => {
const state = createConsultationRuntimeState();
let completed = 0;
async function* chunks() {
yield { type: "text-delta", payload: { text: "只在合同完成后显示。" } };
yield { type: "tool-call", payload: { toolCallId: "skill-1", toolName: "skill", args: { name: "jyotish-vedic-astrology" } } };
state.jyotishSkillLoaded = true;
yield { type: "tool-result", payload: { toolCallId: "skill-1", toolName: "skill", result: {} } };
yield { type: "tool-call", payload: { toolCallId: "tool-1", toolName: "run-jyotish-consultation", args: {} } };
state.consultationToolCallCount = 1;
state.consultationToolCompleted = true;
state.workflowReceipt = { route: "career", status: "ready", preciseTiming: "blocked", missingLayers: [] };
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: () => { completed += 1; },
});
const events: unknown[] = [];
const parser = createNdjsonParser((event) => events.push(event));
parser.finish(await response.text());
assert.equal(completed, 1);
assert.equal(events.filter((event) => (event as { type?: string }).type === "run.completed").length, 1);
assert.equal(events.filter((event) => (event as { type?: string }).type === "answer.delta").length, 1);
assert.equal((events.find((event) => (event as { type?: string }).type === "answer.delta") as { text?: string }).text, "只在合同完成后显示。");
});
test("incomplete runtime contract fails without saving a successful answer", async () => {
const state = createConsultationRuntimeState();
let completed = 0;
let failed = 0;
async function* chunks() {
yield { type: "text-delta", payload: { text: "不能保存" } };
}
const response = streamAgentResponse({
runId: "run", requestId: "req", state, stream: chunks(), requireTool: true,
toolStatus: () => "blocked", receipt: () => receipt(state),
onComplete: () => { completed += 1; },
onError: () => { failed += 1; },
});
const events: unknown[] = [];
const parser = createNdjsonParser((event) => events.push(event));
parser.finish(await response.text());
assert.equal(completed, 0);
assert.equal(failed, 1);
assert.equal(events.some((event) => (event as { type?: string }).type === "answer.delta"), false);
assert.equal(events.filter((event) => (event as { type?: string }).type === "run.failed").length, 1);
});
test("persistence failure emits run.failed instead of run.completed", async () => {
const state = createConsultationRuntimeState();
state.jyotishSkillLoaded = true;
state.consultationToolCallCount = 1;
state.consultationToolCompleted = true;
state.workflowReceipt = { route: "career", status: "ready", preciseTiming: "blocked", missingLayers: [] };
let failed = 0;
async function* chunks() {
yield { type: "text-delta", payload: { text: "不能在持久化失败后标记完成。" } };
}
const response = streamAgentResponse({
runId: "run", requestId: "req", state, stream: chunks(), requireTool: true,
toolStatus: () => "ready", receipt: () => receipt(state),
onComplete: () => { throw new Error("persistence failed"); },
onError: () => { failed += 1; },
});
const events: unknown[] = [];
const parser = createNdjsonParser((event) => events.push(event));
parser.finish(await response.text());
assert.equal(failed, 1);
assert.equal(events.filter((event) => (event as { type?: string }).type === "run.completed").length, 0);
assert.equal(events.filter((event) => (event as { type?: string }).type === "run.failed").length, 1);
});
@@ -69,7 +69,7 @@ test("general mode deterministically rejects personal chart claims while preserv
assert.doesNotMatch(guarded, /。。/);
});
test("general agent runtime has no skill, skill search, skill read, or chart tool", async () => {
test("general agent runtime has the Jyotish skill but no personal chart tool", async () => {
const model: ResolvedLanguageModel = {
id: "general-zero-tool-probe",
label: "General probe",
@@ -81,18 +81,19 @@ test("general agent runtime has no skill, skill search, skill read, or chart too
};
const agent = getGeneralJyotishAgent(model);
const skills = await agent.listSkills();
const toolNames = Object.keys(await agent.listTools());
const toolNames = Object.keys(await agent.getToolsForExecution({ runId: "general-zero-tool-probe" }));
assert.deepEqual(skills, []);
assert.deepEqual(toolNames, []);
assert.equal(toolNames.some((name) => ["skill", "skill_search", "skill_read"].includes(name)), false);
assert.equal(skills.some((skill) => skill.name === "jyotish-vedic-astrology"), true);
assert.equal(toolNames.includes("skill"), true);
assert.equal(toolNames.includes("run-jyotish-consultation"), false);
const mastra = readFileSync(new URL("../src/mastra/index.ts", import.meta.url), "utf8");
const generalFactory = mastra.slice(
mastra.indexOf("export function getGeneralJyotishAgent"),
mastra.indexOf("const onboardingInstructions"),
);
assert.doesNotMatch(generalFactory, /\bskills\s*:|\btools\s*:/);
assert.match(generalFactory, /skills: \[jyotishSkillPath\]/);
assert.doesNotMatch(generalFactory, /run-jyotish-consultation|createConsultationTools/);
});
test("consult route validates mode before billing and general mode uses no chart agent or workflow", () => {
@@ -112,7 +113,8 @@ test("consult route validates mode before billing and general mode uses no chart
mastra.indexOf("export function getGeneralJyotishAgent"),
mastra.indexOf("const onboardingInstructions"),
);
assert.doesNotMatch(generalFactory, /\bskills\s*:|\btools\s*:/);
assert.match(generalFactory, /skills: \[jyotishSkillPath\]/);
assert.doesNotMatch(generalFactory, /run-jyotish-consultation|createConsultationTools/);
});
test("homepage sends explicit modes and never routes an unverified minute through the retired questionnaire", () => {
+41 -38
View File
@@ -3,46 +3,49 @@ import { readFileSync } from "node:fs";
import test from "node:test";
test("passes transparent public-case references into the agent context", () => {
const source = readFileSync(new URL("../src/mastra/index.ts", import.meta.url), "utf8");
const workflowSource = readFileSync(new URL("../src/mastra/consultation-workflow.ts", import.meta.url), "utf8");
const agentSource = readFileSync(new URL("../src/mastra/index.ts", import.meta.url), "utf8");
assert.match(source, /reference_transparency:\s*record\(data\.reference_transparency\)/);
assert.match(source, /vedastro_gateway:\s*record\(data\.vedastro_gateway\)/);
assert.match(source, /ashtakavarga:\s*chart\.ashtakavarga/);
assert.match(source, /high_similarity_public_references_available/);
assert.match(source, /requested_uncovered_domains/);
assert.match(source, /public_context_only/);
assert.match(source, /timing_state/);
assert.match(source, /partial_match/);
assert.match(source, /narayana_status/);
assert.match(source, /transit_status/);
assert.match(source, /Jupiter and Saturn relative houses/);
assert.match(source, /exact_triggers as technical trigger points/);
assert.match(source, /production_tuning_allowed=false/);
assert.match(source, /no_majority_vote/);
assert.match(source, /method_variant_not_majority_vote/);
assert.match(source, /Shadbala\/Ashtakavarga component differences/);
assert.match(source, /D2, D11/);
assert.match(source, /gender-specific spouse significators are supplements/);
assert.match(source, /male.*Venus/);
assert.match(source, /female.*Jupiter\/Mars/);
assert.match(workflowSource, /reference_transparency:\s*record\(data\.reference_transparency\)/);
assert.match(workflowSource, /vedastro_gateway:\s*record\(data\.vedastro_gateway\)/);
assert.match(workflowSource, /ashtakavarga:\s*chart\.ashtakavarga/);
assert.match(agentSource, /high_similarity_public_references_available/);
assert.match(agentSource, /requested_uncovered_domains/);
assert.match(agentSource, /public_context_only/);
assert.match(agentSource, /timing_state/);
assert.match(agentSource, /partial_match/);
assert.match(agentSource, /narayana_status/);
assert.match(agentSource, /transit_status/);
assert.match(agentSource, /Jupiter and Saturn relative houses/);
assert.match(agentSource, /exact_triggers as technical trigger points/);
assert.match(agentSource, /production_tuning_allowed=false/);
assert.match(agentSource, /no_majority_vote/);
assert.match(agentSource, /method_variant_not_majority_vote/);
assert.match(agentSource, /Shadbala\/Ashtakavarga component differences/);
assert.match(agentSource, /D2, D11/);
assert.match(agentSource, /gender-specific spouse significators are supplements/);
assert.match(agentSource, /male.*Venus/);
assert.match(agentSource, /female.*Jupiter\/Mars/);
});
test("keeps strength, Ashtakavarga, and timing evidence available to the answer model", () => {
const source = readFileSync(new URL("../src/mastra/index.ts", import.meta.url), "utf8");
assert.match(source, /shadbala: chart\.shadbala/);
assert.match(source, /shadbala_boundary:/);
assert.match(source, /ashtakavarga: modules\.ashtakavarga/);
assert.match(source, /dasha_boundaries: modules\.dasha_boundaries/);
assert.match(source, /narayana_dasha: modules\.narayana_dasha/);
assert.match(source, /evidence_contract:/);
assert.match(source, /missing_route_layers: consumerContext\.missing_route_layers/);
assert.match(source, /answer_policy: consumerContext\.answer_policy/);
assert.match(source, /evidence_contract\.answer_policy/);
assert.match(source, /can_answer_precise_timing/);
assert.match(source, /boundary: "not_auto_rectified"/);
assert.match(source, /rectification\.boundary=not_auto_rectified/);
assert.match(source, /external_engine_evidence:/);
assert.match(source, /runtime_truth: record\(data\.runtime_truth\)/);
assert.match(source, /numerical_parity: record\(data\.external_parity_gate\)/);
assert.match(source, /real_case_calibration: record\(data\.real_case_calibration\)/);
const workflowSource = readFileSync(new URL("../src/mastra/consultation-workflow.ts", import.meta.url), "utf8");
const agentSource = readFileSync(new URL("../src/mastra/index.ts", import.meta.url), "utf8");
assert.match(workflowSource, /shadbala: chart\.shadbala/);
assert.match(workflowSource, /shadbala_boundary:/);
assert.match(workflowSource, /ashtakavarga: modules\.ashtakavarga/);
assert.match(workflowSource, /dasha_boundaries: modules\.dasha_boundaries/);
assert.match(workflowSource, /narayana_dasha: modules\.narayana_dasha/);
assert.match(workflowSource, /evidence_contract:/);
assert.match(workflowSource, /missing_route_layers: consumerContext\.missing_route_layers/);
assert.match(workflowSource, /answer_policy: consumerContext\.answer_policy/);
assert.match(agentSource, /evidence_contract\.answer_policy/);
assert.match(agentSource, /can_answer_precise_timing/);
assert.match(workflowSource, /boundary: "not_auto_rectified"/);
assert.match(agentSource, /rectification\.boundary=not_auto_rectified/);
assert.match(workflowSource, /external_engine_evidence:/);
assert.match(workflowSource, /runtime_truth: record\(data\.runtime_truth\)/);
assert.match(workflowSource, /numerical_parity: record\(data\.external_parity_gate\)/);
assert.match(workflowSource, /real_case_calibration: record\(data\.real_case_calibration\)/);
});
@@ -18,7 +18,7 @@ test("reserves usage and binds the owned consultation session atomically", () =>
});
test("persists transformed assistant metadata before atomically settling usage", () => {
assert.equal(consultRoute.match(/continueAfterDisconnect: true/g)?.length, 2);
assert.equal(consultRoute.match(/continueAfterDisconnect: true/g)?.length, 4);
assert.equal(consultRoute.match(/onComplete: \(rawTransformedText\) => settle\(\(\) => completeResponse\(/g)?.length, 2);
assert.match(consultRoute, /parseAgentReply\(rawTransformedText, consultationTheme\)/);
assert.match(consultRoute, /role: "assistant" as const,[\s\S]*suggestions: reply\.suggestions,[\s\S]*techniqueTruth,[\s\S]*workflowReceipt/);
@@ -39,6 +39,27 @@ test("persists partial transformed output when the upstream stream errors", () =
assert.equal(consultRoute.match(/const settleErrored = \(emitted: boolean, output: string\) => settle\(/g)?.length, 2);
assert.equal(consultRoute.match(/emitted[\s\S]*?\? \(\) => completeResponse\([\s\S]*?output,[\s\S]*?result\.totalUsage,[\s\S]*?: cancel,/g)?.length, 2);
assert.equal(consultRoute.match(/onCancel: \(\) => settle\(cancel\)/g)?.length, 2);
assert.equal(
consultRoute.match(/onCancel: \(\) => settleRun\(cancel, "cancelled", "cancelled"\)/g)?.length,
2,
);
});
test("Agentic failures always refund and detached execution uses a server-owned timeout", () => {
const agentic = consultRoute.slice(
consultRoute.indexOf("async function runAgenticConsultation("),
consultRoute.indexOf(" try {\n const { history } = parsed.data;"),
);
assert.match(agentic, /const agentAbortSignal = AbortSignal\.timeout\(110_000\)/);
assert.equal(agentic.match(/abortSignal: agentAbortSignal/g)?.length, 2);
assert.doesNotMatch(agentic, /abortSignal: request\.signal/);
assert.equal(
agentic.match(/onError: \(error\) => settleRun\(\s*cancel,[\s\S]*?"cancelled",\s*\)/g)?.length,
2,
);
const onErrorBlocks = agentic.match(/onError:[\s\S]*?onCancel:/g) ?? [];
assert.equal(onErrorBlocks.length, 2);
for (const block of onErrorBlocks) assert.doesNotMatch(block, /completeResponse|completed_partial/);
});
test("best-effort cancels a failed or uncertain durable completion before rethrowing", () => {
@@ -5,46 +5,55 @@ import test from "node:test";
const route = readFileSync(new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8");
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 workflow = readFileSync(new URL("../src/mastra/consultation-workflow.ts", import.meta.url), "utf8");
const stagingCompose = readFileSync(new URL("../../deploy/docker-compose.staging.yml", import.meta.url), "utf8");
test("runs the Jyotish workflow before streaming a commercial consultation", () => {
const chartBranch = route.slice(route.indexOf("const toolInput = consultationInputSchema.parse"));
assert.match(route, /runConsultationWorkflow/);
assert.match(chartBranch, /await runConsultationWorkflow\(toolInput, \{ foreground: true \}\)/);
assert.match(chartBranch, /getJyotishAgent\(selectedModel, workflowContext\)\.stream/);
assert.ok(
chartBranch.indexOf("await runConsultationWorkflow(toolInput, { foreground: true })")
< chartBranch.indexOf("getJyotishAgent(selectedModel, workflowContext).stream"),
test("personal consultation lets the Agent invoke the server-bound workflow tool", () => {
const agenticStart = route.indexOf("async function runAgenticConsultation");
const agenticBranch = route.slice(
agenticStart,
route.indexOf(" const { history } = parsed.data;", agenticStart),
);
assert.match(agenticBranch, /createConsultationAgentContext/);
assert.match(agenticBranch, /getJyotishAgent\(selectedModel, agentContext\)/);
assert.doesNotMatch(agenticBranch, /await runConsultationWorkflow/);
assert.doesNotMatch(agenticBranch, /JSON\.stringify\(toolInput\)/);
assert.match(tools, /\(ctx\.runWorkflow \?\? runConsultationWorkflow\)\(toolInput, \{/);
assert.match(tools, /return \{ "run-jyotish-consultation": consultationTool \};/);
assert.match(agenticBranch, /state\.workflowReceipt\?\.preciseTiming === "allowed"/);
});
test("defers optional external evidence only for foreground chat", () => {
assert.match(mastra, /defer_optional_external_evidence: options\?\.foreground === true/);
assert.match(workflow, /defer_optional_external_evidence: options\?\.foreground === true/);
assert.match(reportsRoute, /runWorkflow: \(input\) => runConsultationWorkflow\(input\)/);
assert.doesNotMatch(reportsRoute, /foreground:\s*true/);
});
test("grounds the answer in the server-computed workflow without a second tool run", () => {
assert.match(mastra, /function getJyotishAgent\(model: ResolvedLanguageModel, workflowContext\?/);
assert.match(mastra, /workflowContext \? \{\} : \{ consultationTool \}/);
assert.match(mastra, /server-computed Jyotish workflow/);
test("personal Agent owns the Skill and context-bound calculation tool", () => {
const personalFactory = mastra.slice(
mastra.indexOf("export function getJyotishAgent"),
mastra.indexOf("export function getLegacyJyotishAgent"),
);
assert.match(personalFactory, /getJyotishAgent\(model: ResolvedLanguageModel, context: ConsultationAgentContext\)/);
assert.match(personalFactory, /skills: \[jyotishSkillPath\]/);
assert.match(personalFactory, /tools: createConsultationTools\(context\)/);
assert.doesNotMatch(personalFactory, /server-computed-jyotish-workflow/);
});
test("validates and emits a non-sensitive workflow receipt", () => {
assert.match(mastra, /consultationWorkflowResponseSchema/);
assert.match(mastra, /safeParse\(data\)/);
assert.match(mastra, /consultationWorkflowReceipt/);
assert.match(route, /workflowReceipt/);
assert.match(route, /x-jyotish-workflow-route/);
assert.match(route, /x-jyotish-workflow-status/);
test("validates and emits non-sensitive workflow and execution receipts", () => {
assert.match(workflow, /consultationWorkflowResponseSchema/);
assert.match(workflow, /safeParse\(data\)/);
assert.match(workflow, /consultationWorkflowReceipt/);
assert.match(route, /agentExecutionReceipt/);
assert.match(route, /streamAgentResponse/);
});
test("carries commercial technique truth into the model contract", () => {
assert.match(mastra, /technique_truth/);
assert.match(workflow, /technique_truth/);
assert.match(mastra, /deterministic_claims_forbidden_for/);
assert.match(mastra, /reference_only/);
assert.match(mastra, /Do not use a restricted technique/);
assert.match(route, /x-jyotish-technique-truth/);
});
test("projects consultation themes through explicit strict workflow taxonomy", () => {
@@ -54,3 +63,11 @@ test("projects consultation themes through explicit strict workflow taxonomy", (
assert.match(projection, /requiredLayers/);
assert.match(projection, /negative holdout gate/);
});
test("staging enables the agentic consultation runtime without changing production compose", () => {
assert.match(stagingCompose, /CONSULTATION_AGENTIC_RUNTIME: enabled/);
assert.match(route, /CONSULTATION_AGENTIC_RUNTIME/);
assert.match(route, /legacy/);
assert.match(route, /canary/);
});
@@ -22,7 +22,7 @@ test("timing questions use a legal report theme and preserve a timing route hint
test("consultation workflow allows a cold engine run to finish", async () => {
const source = await import("node:fs/promises").then(({ readFile }) =>
readFile(new URL("../src/mastra/index.ts", import.meta.url), "utf8")
readFile(new URL("../src/mastra/consultation-workflow.ts", import.meta.url), "utf8")
);
assert.match(source, /AbortSignal\.timeout\(90_000\)/);
+1 -1
View File
@@ -9,7 +9,7 @@ test("staging postgres is private and CI binds loopback only", () => {
const ci = readFileSync("../deploy/docker-compose.postgres-ci.yml", "utf8");
assert.match(staging, /image:\s*postgres:17-alpine/);
assert.doesNotMatch(staging, /^\s+ports:/m);
assert.match(application, /web:\s*\n\s+networks:\s*\n\s+- default\s*\n\s+- app/);
assert.match(application, /web:\s*\n[\s\S]*?\n\s+networks:\s*\n\s+- default\s*\n\s+- app/);
assert.match(ci, /127\.0\.0\.1:\$\{POSTGRES_HOST_PORT:-55432\}:5432/);
});
@@ -4,7 +4,7 @@ import test from "node:test";
const root = new URL("../../", import.meta.url);
const readRoot = (path: string) => readFileSync(new URL(path, root), "utf8");
const mastra = readRoot("frontend/src/mastra/index.ts");
const consultationWorkflow = readRoot("frontend/src/mastra/consultation-workflow.ts");
const rectification = readRoot("frontend/src/lib/birth-time-journey-engine-model.ts");
const synastry = readRoot("frontend/src/app/api/synastry/route.ts");
const apiServer = readRoot("scripts/jyotish_api_server.py");
@@ -20,7 +20,7 @@ test("commercial Jyotish paths resolve to a registered Python handler", () => {
]) {
assert.match(apiServer, new RegExp(`['\"]${path.replaceAll("/", "\\/")}['\"]`));
}
assert.match(mastra, /\/api\/consultation_workflow/);
assert.match(consultationWorkflow, /\/api\/consultation_workflow/);
assert.match(rectification, /\/api\/active_rectification_questions/);
assert.match(rectification, /\/api\/active_rectification_score/);
assert.match(rectification, /\/api\/active_rectification_events/);