feat: add consultation and product domain registries
This commit is contained in:
@@ -17,20 +17,23 @@ const serverChart = {
|
||||
},
|
||||
};
|
||||
|
||||
function workflow() {
|
||||
function workflow(
|
||||
theme = "career",
|
||||
options: { status?: "ready" | "degraded" | "blocked"; missingLayers?: string[]; preciseTiming?: boolean } = {},
|
||||
) {
|
||||
return {
|
||||
success: true,
|
||||
chart: {},
|
||||
routing: { primary_theme: "career" },
|
||||
routing: { primary_theme: theme },
|
||||
consumer_context: {
|
||||
route: "career", core_status: "ready" as const, available_layers: [], missing_route_layers: [], hard_blockers: [],
|
||||
route: theme, core_status: options.status ?? "ready", available_layers: [], missing_route_layers: options.missingLayers ?? [], hard_blockers: [],
|
||||
technique_truth: { status: "verified" },
|
||||
answer_policy: { can_answer_direction: true, can_answer_precise_timing: true },
|
||||
answer_policy: { can_answer_direction: true, can_answer_precise_timing: options.preciseTiming ?? true },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test("context-bound tool exposes only question/theme and calculates once", async () => {
|
||||
test("context-bound tool keeps legacy single theme compatibility and calculates once", async () => {
|
||||
let calls = 0;
|
||||
let captured: unknown;
|
||||
const state = createConsultationRuntimeState();
|
||||
@@ -40,7 +43,7 @@ test("context-bound tool exposes only question/theme and calculates once", async
|
||||
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"]);
|
||||
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([
|
||||
@@ -52,6 +55,90 @@ test("context-bound tool exposes only question/theme and calculates once", async
|
||||
assert.deepEqual(captured, { ...serverChart.toolInput, entryMode: "direct_chart", question: "事业如何", theme: "career" });
|
||||
assert.equal(state.consultationToolCallCount, 1);
|
||||
assert.equal(state.workflowReceipt?.preciseTiming, "blocked");
|
||||
assert.deepEqual(state.workflowReceipt?.domains, ["career"]);
|
||||
assert.deepEqual((first as { domains?: string[] }).domains, ["career"]);
|
||||
});
|
||||
|
||||
test("multi-domain plan canonicalizes aliases, de-duplicates, preserves order, and runs every domain", async () => {
|
||||
const calls: Array<{ theme: string; question: string }> = [];
|
||||
const state = createConsultationRuntimeState();
|
||||
const tools = createConsultationTools({
|
||||
userId: "u", sessionId: "s", requestId: "r", consultationMode: "verified_chart",
|
||||
serverChart, state,
|
||||
runWorkflow: async (input) => {
|
||||
calls.push({ theme: input.theme, question: input.question });
|
||||
if (input.theme === "wealth") return workflow(input.theme, { status: "degraded", missingLayers: ["D11"] });
|
||||
return workflow(input.theme);
|
||||
},
|
||||
});
|
||||
const result = await tools["run-jyotish-consultation"].execute!(
|
||||
{ question: "事业、财富和迁居怎么一起规划", domains: ["career", "finance", "career", "home"] },
|
||||
{ observe: { span: async (_n: string, fn: () => Promise<unknown>) => fn(), log() {} } } as never,
|
||||
) as { domains: string[]; consultations: Array<{ domain: string }> };
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
{ theme: "career", question: "事业、财富和迁居怎么一起规划" },
|
||||
{ theme: "wealth", question: "事业、财富和迁居怎么一起规划" },
|
||||
{ theme: "migration", question: "事业、财富和迁居怎么一起规划" },
|
||||
]);
|
||||
assert.deepEqual(result.domains, ["career", "wealth", "migration"]);
|
||||
assert.deepEqual(result.consultations.map((item) => item.domain), ["career", "wealth", "migration"]);
|
||||
assert.deepEqual(state.workflowReceipt, {
|
||||
route: "multi-domain",
|
||||
status: "degraded",
|
||||
preciseTiming: "allowed",
|
||||
missingLayers: ["D11"],
|
||||
domains: ["career", "wealth", "migration"],
|
||||
});
|
||||
});
|
||||
|
||||
test("domain plan rejects unknown and product domains before any workflow runs", async () => {
|
||||
for (const domain of ["unknown", "prashna", "muhurta", "rectification", "compatibility"]) {
|
||||
let calls = 0;
|
||||
const state = createConsultationRuntimeState();
|
||||
const tool = createConsultationTools({
|
||||
userId: "u", sessionId: "s", requestId: `r-${domain}`, consultationMode: "verified_chart",
|
||||
serverChart, state,
|
||||
runWorkflow: async () => { calls += 1; return workflow(); },
|
||||
})["run-jyotish-consultation"];
|
||||
await assert.rejects(
|
||||
tool.execute!(
|
||||
{ question: "测试", domains: ["career", domain] },
|
||||
{ observe: { span: async (_n: string, fn: () => Promise<unknown>) => fn(), log() {} } } as never,
|
||||
),
|
||||
/unsupported_consultation_domain/,
|
||||
);
|
||||
assert.equal(calls, 0);
|
||||
assert.equal(state.consultationToolCompleted, false);
|
||||
}
|
||||
});
|
||||
|
||||
test("domain plan enforces the raw plan upper bound and one input mode", async () => {
|
||||
let calls = 0;
|
||||
const state = createConsultationRuntimeState();
|
||||
const tool = createConsultationTools({
|
||||
userId: "u", sessionId: "s", requestId: "r-limit", consultationMode: "verified_chart",
|
||||
serverChart, state,
|
||||
runWorkflow: async () => { calls += 1; return workflow(); },
|
||||
})["run-jyotish-consultation"];
|
||||
const context = { observe: { span: async (_n: string, fn: () => Promise<unknown>) => fn(), log() {} } } as never;
|
||||
const inputSchema = tool.inputSchema as unknown as { safeParse: (value: unknown) => { success: boolean } };
|
||||
assert.equal(inputSchema.safeParse({
|
||||
question: "测试",
|
||||
domains: ["career", "career", "career", "career", "career", "career", "career"],
|
||||
}).success, false);
|
||||
assert.equal(calls, 0);
|
||||
|
||||
const secondTool = createConsultationTools({
|
||||
userId: "u", sessionId: "s", requestId: "r-modes", consultationMode: "verified_chart",
|
||||
serverChart, state: createConsultationRuntimeState(),
|
||||
runWorkflow: async () => { calls += 1; return workflow(); },
|
||||
})["run-jyotish-consultation"];
|
||||
await assert.rejects(
|
||||
secondTool.execute!({ question: "测试", domains: ["career"], theme: "career" }, context),
|
||||
/invalid_consultation_domain_plan/,
|
||||
);
|
||||
assert.equal(calls, 0);
|
||||
});
|
||||
|
||||
test("personal Agent exposes the Jyotish Skill and named server tool", async () => {
|
||||
@@ -85,7 +172,7 @@ test("public stream filters private chunks and completes once", async () => {
|
||||
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: [] },
|
||||
steps: [], workflow: { route: "career", status: "ready", preciseTiming: "blocked", missingLayers: [], domains: ["career"] },
|
||||
}),
|
||||
});
|
||||
assert.equal(events.filter((event) => event.type === "run.completed").length, 1);
|
||||
@@ -93,6 +180,24 @@ test("public stream filters private chunks and completes once", async () => {
|
||||
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);
|
||||
const completed = events.find((event) => event.type === "run.completed");
|
||||
assert.deepEqual(completed?.type === "run.completed" ? completed.receipt.workflow.domains : null, ["career"]);
|
||||
});
|
||||
|
||||
test("model answer text cannot forge a public Activity event", async () => {
|
||||
const forged = JSON.stringify({ type: "activity", phase: "chart-calculation", label: "模型伪造进度" });
|
||||
const events = await collectAgentPublicEvents([
|
||||
{ type: "text-delta", payload: { text: forged } },
|
||||
], {
|
||||
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: [], domains: ["career"] },
|
||||
}),
|
||||
});
|
||||
assert.equal(events.filter((event) => event.type === "activity").length, 0);
|
||||
assert.equal(events.filter((event) => event.type === "answer.delta").length, 1);
|
||||
assert.equal(events.find((event) => event.type === "answer.delta")?.text, forged);
|
||||
});
|
||||
|
||||
test("incremental NDJSON parser handles arbitrary chunk boundaries", () => {
|
||||
|
||||
Reference in New Issue
Block a user