skill.test.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. import { PermissionV1 } from "@kirincode-ai/core/v1/permission"
  2. import { CrossSpawnSpawner } from "@kirincode-ai/core/cross-spawn-spawner"
  3. import { LayerNode } from "@kirincode-ai/core/effect/layer-node"
  4. import { Ripgrep } from "@kirincode-ai/core/ripgrep"
  5. import { Cause, Effect, Exit, Layer } from "effect"
  6. import { afterEach, describe, expect } from "bun:test"
  7. import path from "path"
  8. import type { Permission } from "../../src/permission"
  9. import type { Tool } from "@/tool/tool"
  10. import { SkillTool } from "../../src/tool/skill"
  11. import { ToolRegistry } from "@/tool/registry"
  12. import { disposeAllInstances, TestInstance } from "../fixture/fixture"
  13. import { SessionID, MessageID } from "../../src/session/schema"
  14. import { testEffect } from "../lib/effect"
  15. const baseCtx: Omit<Tool.Context, "ask"> = {
  16. sessionID: SessionID.make("ses_test"),
  17. messageID: MessageID.make("msg_test"),
  18. callID: "",
  19. agent: "build",
  20. abort: AbortSignal.any([]),
  21. messages: [],
  22. metadata: () => Effect.void,
  23. }
  24. afterEach(async () => {
  25. await disposeAllInstances()
  26. })
  27. const it = testEffect(LayerNode.compile(LayerNode.group([ToolRegistry.node, CrossSpawnSpawner.node, Ripgrep.node])))
  28. describe("tool.skill", () => {
  29. it.instance("execute returns skill content block with files", () =>
  30. Effect.gen(function* () {
  31. const dir = (yield* TestInstance).directory
  32. const skill = path.join(dir, ".kirincode", "skill", "tool-skill")
  33. yield* Effect.promise(() =>
  34. Bun.write(
  35. path.join(skill, "SKILL.md"),
  36. `---
  37. name: tool-skill
  38. description: Skill for tool tests.
  39. ---
  40. # Tool Skill
  41. Use this skill.
  42. `,
  43. ),
  44. )
  45. yield* Effect.promise(() => Bun.write(path.join(skill, "scripts", "demo.txt"), "demo"))
  46. const home = process.env.KIRINCODE_TEST_HOME
  47. process.env.KIRINCODE_TEST_HOME = dir
  48. yield* Effect.addFinalizer(() =>
  49. Effect.sync(() => {
  50. process.env.KIRINCODE_TEST_HOME = home
  51. }),
  52. )
  53. const registry = yield* ToolRegistry.Service
  54. const agent = { name: "build", mode: "primary" as const, permission: [], options: {} }
  55. const tool = (yield* registry.tools({
  56. providerID: "kirincode" as any,
  57. modelID: "gpt-5" as any,
  58. agent,
  59. })).find((tool) => tool.id === SkillTool.id)
  60. if (!tool) throw new Error("Skill tool not found")
  61. expect(tool.description).not.toContain("tool-skill")
  62. expect(tool.description).not.toContain("Skill for tool tests.")
  63. const requests: Array<Omit<PermissionV1.Request, "id" | "sessionID" | "tool">> = []
  64. const ctx: Tool.Context = {
  65. ...baseCtx,
  66. ask: (req) =>
  67. Effect.sync(() => {
  68. requests.push(req)
  69. }),
  70. }
  71. const result = yield* tool.execute({ name: "tool-skill" }, ctx)
  72. const file = path.resolve(skill, "scripts", "demo.txt")
  73. expect(requests.length).toBe(1)
  74. expect(requests[0].permission).toBe("skill")
  75. expect(requests[0].patterns).toContain("tool-skill")
  76. expect(requests[0].always).toContain("tool-skill")
  77. expect(result.metadata.dir).toBe(skill)
  78. expect(result.output).toContain(`<skill_content name="tool-skill">`)
  79. expect(result.output).toContain(`Base directory for this skill: ${skill}`)
  80. expect(result.output).toContain(`<file>${file}</file>`)
  81. }),
  82. )
  83. it.instance("execute preserves not found message", () =>
  84. Effect.gen(function* () {
  85. const dir = (yield* TestInstance).directory
  86. const home = process.env.KIRINCODE_TEST_HOME
  87. process.env.KIRINCODE_TEST_HOME = dir
  88. yield* Effect.addFinalizer(() =>
  89. Effect.sync(() => {
  90. process.env.KIRINCODE_TEST_HOME = home
  91. }),
  92. )
  93. const registry = yield* ToolRegistry.Service
  94. const agent = { name: "build", mode: "primary" as const, permission: [], options: {} }
  95. const tool = (yield* registry.tools({
  96. providerID: "kirincode" as any,
  97. modelID: "gpt-5" as any,
  98. agent,
  99. })).find((tool) => tool.id === SkillTool.id)
  100. if (!tool) throw new Error("Skill tool not found")
  101. const exit = yield* tool
  102. .execute(
  103. { name: "missing-skill" },
  104. {
  105. ...baseCtx,
  106. ask: () => Effect.void,
  107. },
  108. )
  109. .pipe(Effect.exit)
  110. expect(Exit.isFailure(exit)).toBe(true)
  111. if (Exit.isFailure(exit)) {
  112. const error = Cause.squash(exit.cause)
  113. expect(error).toBeInstanceOf(Error)
  114. if (error instanceof Error) expect(error.message).toContain('Skill "missing-skill" not found.')
  115. }
  116. }),
  117. )
  118. })