feat(consultation): let the jyotish agent drive skills and tools
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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\)/);
|
||||
|
||||
@@ -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/);
|
||||
|
||||
Reference in New Issue
Block a user