import assert from "node:assert/strict"; import { existsSync, readFileSync, realpathSync } from "node:fs"; import { join, resolve } from "node:path"; import test from "node:test"; import { Agent } from "@mastra/core/agent"; import { sharedConsultationMethodMarkdown } from "../src/lib/consultation-methodology.ts"; import { BOUND_METHOD_MARKER, jyotishSkillBinding, jyotishSkillBoundProcessor, jyotishSkillMethodBlock, jyotishSkillMethodCoreBlock, jyotishSkillPackage, jyotishSkillRuntimePath, } from "../src/mastra/skill-binding.ts"; const model = { specificationVersion: "v2", provider: "probe", modelId: "probe" } as never; function probeAgent(options: Record) { return new Agent({ id: "skill-binding-probe", name: "Probe", model, instructions: "x", ...options } as never); } function liveSkillBody() { const live = readFileSync(resolve(jyotishSkillPackage.resolvedPath, "SKILL.md"), "utf8"); return live.slice(live.indexOf("---", 3) + 4).trim(); } function boundMethodText() { const opened = jyotishSkillMethodBlock.indexOf(`"); assert.ok(opened >= 0 && closed > opened); return jyotishSkillMethodBlock.slice(jyotishSkillMethodBlock.indexOf("\n", opened) + 1, closed).trim(); } test("the method the model follows is the live skill, not a hashed snapshot", () => { const body = liveSkillBody(); const bound = boundMethodText(); assert.match(jyotishSkillMethodBlock, new RegExp(`name="${jyotishSkillPackage.name}"`)); assert.doesNotMatch(jyotishSkillMethodBlock, / { // Independent source: the commercial SKILL.md still contains both the // answering contract and the repo-construction notes. Binding the whole // file is what pushed Mastra past its 500-line warning; the excerpt keeps // the contract and drops the notes. const body = liveSkillBody(); const bound = boundMethodText(); for (const heading of ["商业运行时路由", "关联技法完整调取", "强制工作流", "五层硬约束", "核心方法论", "注意事项", "commercial_skill_truth_overlay"]) { assert.ok(body.includes(heading), heading); assert.ok(bound.includes(heading), heading); } for (const phrase of ["全谱系真实调用", "P0/P1 观察层", "网页对话的技法审计表由界面折叠展示", "静态分析10步"]) { assert.ok(bound.includes(phrase), phrase); } for (const heading of ["施工判断原则", "冲顶路线", "37大子命令", "验证与错题体系", "开源复用边界冻结"]) { assert.ok(body.includes(heading), heading); assert.equal(bound.includes(heading), false, heading); } assert.ok(body.includes("~/.workbuddy/skills")); assert.equal(bound.includes("~/.workbuddy/skills"), false); assert.ok(bound.split("\n").length < 500, `${bound.split("\n").length} lines`); assert.ok(bound.length < body.length * 0.7, `${bound.length} of ${body.length}`); }); test("binding the method costs a fraction of activating the skill", async () => { // What activation would have sent, measured rather than assumed: Mastra answers // it with the entrypoint plus a flat listing of every file in the package, and // that listing is the majority of the bytes. Nothing carries over between // requests, so it was resent on every turn of every conversation. const activation = probeAgent({ skills: [jyotishSkillRuntimePath] }); const skillTool = (await activation.getToolsForExecution({ runId: "probe" })).skill as { execute: (input: unknown, context: unknown) => Promise; }; const activated = await skillTool.execute( { name: jyotishSkillPackage.name }, { runId: "probe" } as never, ); const activationBytes = Buffer.byteLength( typeof activated === "string" ? activated : JSON.stringify(activated), "utf8", ); const boundBytes = Buffer.byteLength(jyotishSkillMethodBlock, "utf8"); assert.ok(activationBytes > boundBytes * 2, `${activationBytes} vs ${boundBytes}`); assert.ok(boundBytes < activationBytes * 0.45, `${boundBytes} of ${activationBytes}`); }); test("agents that bind the method are not given a tool to load it", async () => { const bound = probeAgent(jyotishSkillBinding()); const toolNames = Object.keys(await bound.getToolsForExecution({ runId: "probe" })); assert.deepEqual(toolNames, ["skill_read"]); // The skill is still declared, so the references the method names by path stay // reachable for the one question the delivered sections do not cover. assert.equal( (await bound.listSkills()).some((skill) => skill.name === jyotishSkillPackage.name), true, ); }); test("a prompt assembled without the method stops the run instead of answering", () => { const abortReasons: string[] = []; const abort = ((reason?: string) => { abortReasons.push(reason ?? ""); throw new Error("aborted"); }) as never; const step = (system: unknown[]) => jyotishSkillBoundProcessor.processInputStep({ messageList: { getAllSystemMessages: () => system }, abort, } as never); assert.throws(() => step([{ role: "system", content: "you are a guide" }]), /aborted/); assert.match(abortReasons[0], /not bound into the system prompt/); // A prompt that does carry the method, and a prompt that cannot be read at all, // both pass: this guard exists to catch a missing method, not to become a new // way for a run to fail. assert.doesNotThrow(() => step([{ role: "system", content: jyotishSkillMethodBlock }])); assert.doesNotThrow(() => step([{ role: "system", content: jyotishSkillMethodCoreBlock }])); assert.doesNotThrow(() => step([])); }); function agentConstructorBodies(source: string): string[] { const bodies: string[] = []; const needle = "new Agent("; let search = 0; while (true) { const start = source.indexOf(needle, search); if (start < 0) break; const openParen = start + needle.length - 1; let depth = 0; let inStr: string | null = null; let escaped = false; let i = openParen; for (; i < source.length; i += 1) { const ch = source[i]; if (inStr) { if (escaped) { escaped = false; continue; } if (ch === "\\") { escaped = true; continue; } if (ch === inStr) inStr = null; continue; } if (ch === "'" || ch === '"' || ch === "`") { inStr = ch; continue; } if (ch === "(") depth += 1; else if (ch === ")") { depth -= 1; if (depth === 0) { i += 1; break; } } } bodies.push(source.slice(start, i)); search = i; } return bodies; } function constTemplateBody(file: string, name: string): string { return new RegExp(`(?:const|let) ${name} = \`([\\s\\S]*?)\`;`).exec(file)?.[1] ?? ""; } function instructionSource(agentBody: string, file: string): string { const named = /instructions:\s*([A-Za-z_][A-Za-z0-9_]*)/.exec(agentBody); let text = named ? `${agentBody}\n${constTemplateBody(file, named[1])}` : (/instructions:\s*`([\s\S]*?)`/.exec(agentBody)?.[1] ?? agentBody); for (const match of text.matchAll(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g)) { text += `\n${constTemplateBody(file, match[1])}`; } return text; } function carriesBoundMethodMarker(text: string): boolean { return text.includes("jyotishSkillMethodBlock") || text.includes("jyotishSkillMethodCoreBlock") || text.includes(BOUND_METHOD_MARKER) || text.includes(" { const mastra = readFileSync(new URL("../src/mastra/index.ts", import.meta.url), "utf8"); const boundAgents = agentConstructorBodies(mastra).filter((body) => body.includes("...jyotishSkillBinding()")); assert.ok(boundAgents.length >= 3, `expected natal, legacy, and window agents, got ${boundAgents.length}`); for (const body of boundAgents) { assert.equal(carriesBoundMethodMarker(instructionSource(body, mastra)), true, body.slice(0, 160)); } assert.match(jyotishSkillMethodCoreBlock, new RegExp(BOUND_METHOD_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); assert.doesNotMatch(jyotishSkillMethodCoreBlock, /Level 2 report template/); assert.match(jyotishSkillMethodBlock, /Level 2 report template/); }); test("window agent input processing does not abort (BUG-954)", async () => { const { getWindowJyotishAgent } = await import("../src/mastra/index.ts"); const { createConsultationRuntimeState, createWindowConsultationAgentContext } = await import("../src/mastra/consultation-tools.ts"); const model = { id: "window-binding-probe", label: "Window probe", description: "", creditCost: 1, isDefault: false, mode: "openai", model: "openai/gpt-5-mini", } as never; const agent = getWindowJyotishAgent(model, createWindowConsultationAgentContext({ userId: "user", sessionId: "session", requestId: "req", consultationMode: "declared_birth_window", declaredWindow: { name: "探针", toolInput: { year: 1990, month: 1, day: 2, city: "台北", lat: 25.03, lon: 121.56, tz: 8, ayanamsa: "raman", rangeStart: "04:00", rangeEnd: "06:00", }, truth: { birthDate: "1990-01-02", birthTimeSource: "family_period", birthTimePeriod: "morning", birthTimeStatus: "window", wrapsMidnight: false, placeLabel: "台北", placeCodes: { countryCode: "TW", provinceCode: null, cityCode: null, districtCode: null }, placeId: null, placeType: "city", placeProvider: "profile", timezoneId: "Asia/Taipei", timezoneSource: "profile", latitude: 25.03, longitude: 121.56, timezoneOffset: 8, }, }, state: createConsultationRuntimeState(), })); const instructions = await agent.getInstructions(); const text = typeof instructions === "string" ? instructions : JSON.stringify(instructions); assert.match(text, / { abortReasons.push(reason ?? ""); throw new Error("aborted"); }) as never, } as never)); assert.equal(abortReasons.length, 0); }); test("only chart-answering agents bind the method", () => { const mastra = readFileSync(new URL("../src/mastra/index.ts", import.meta.url), "utf8"); const chart = mastra.slice( mastra.indexOf("const jyotishInstructions"), mastra.indexOf("const generalJyotishInstructions"), ); const general = mastra.slice( mastra.indexOf("const generalJyotishInstructions"), mastra.indexOf("const dailyStarlanguageInstructions"), ); const daily = mastra.slice( mastra.indexOf("const dailyStarlanguageInstructions"), mastra.indexOf("const birthTimeGuideInstructions"), ); assert.match(chart, /\.\.\.jyotishSkillBinding\(\)/); assert.match(chart, /\$\{jyotishSkillMethodBlock\}/); // 原值: 另切 onboardingInstructions,断言它也不绑方法。 // 新值: onboarding agent 已删除;general 与 daily 仍不绑。 // 原因: 建议问题生成接口下线。 for (const [name, source] of [["general", general], ["daily", daily]] as const) { assert.doesNotMatch(source, /jyotishSkillBinding|jyotishSkillMethodBlock/, name); } }); test("the system block binds the shared method sections verbatim from the package", () => { const shared = sharedConsultationMethodMarkdown(); const opened = jyotishSkillMethodBlock.indexOf(""); const closed = jyotishSkillMethodBlock.indexOf(""); assert.ok(opened >= 0 && closed > opened); const boundShared = jyotishSkillMethodBlock.slice( opened + "".length, closed, ).trim(); assert.equal(boundShared, shared.trim()); assert.match(shared, /Full-Spectrum Invocation Contract/); assert.ok(shared.includes(readFileSync(resolve(jyotishSkillPackage.resolvedPath, "references/event_judgment_skeleton.md"), "utf8").trim())); });