guidance.test.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. import path from "path"
  2. import { describe, expect } from "bun:test"
  3. import { Effect, Layer } from "effect"
  4. import { AgentV2 } from "@opencode-ai/core/agent"
  5. import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
  6. import { AbsolutePath } from "@opencode-ai/core/schema"
  7. import { SkillV2 } from "@opencode-ai/core/skill"
  8. import { SystemContext } from "@opencode-ai/core/system-context"
  9. import { SkillGuidance } from "@opencode-ai/core/skill/guidance"
  10. import { it } from "../lib/effect"
  11. const build = AgentV2.ID.make("build")
  12. const effect = SkillV2.Info.make({
  13. name: "effect",
  14. description: "Build applications with Effect",
  15. location: AbsolutePath.make(path.resolve("/skills/effect/SKILL.md")),
  16. content: "Effect guidance",
  17. })
  18. const hidden = SkillV2.Info.make({
  19. name: "hidden",
  20. location: AbsolutePath.make(path.resolve("/skills/hidden/SKILL.md")),
  21. content: "Undescribed guidance",
  22. })
  23. const denied = SkillV2.Info.make({
  24. name: "denied",
  25. description: "Must not be advertised",
  26. location: AbsolutePath.make(path.resolve("/skills/denied/SKILL.md")),
  27. content: "Denied guidance",
  28. })
  29. const layer = (list: () => SkillV2.Info[]) =>
  30. AppNodeBuilder.build(SkillGuidance.node, [
  31. [SkillV2.node, Layer.mock(SkillV2.Service, { list: () => Effect.succeed(list()) })],
  32. ])
  33. describe("SkillGuidance", () => {
  34. it.effect("renders described agent skills and reconciles the complete available list", () => {
  35. const agent = AgentV2.Info.make({
  36. ...AgentV2.Info.empty(build),
  37. permissions: [{ action: "skill", resource: "denied", effect: "deny" }],
  38. })
  39. let skills = [hidden, denied, effect]
  40. return Effect.gen(function* () {
  41. const guidance = yield* SkillGuidance.Service
  42. const initialized = yield* guidance
  43. .load({ id: agent.id, info: agent })
  44. .pipe(Effect.flatMap(SystemContext.initialize))
  45. expect(initialized.baseline).toBe(
  46. [
  47. "Skills provide specialized instructions and workflows for specific tasks.",
  48. "Use the skill tool to load a skill when a task matches its description.",
  49. "<available_skills>",
  50. " <skill>",
  51. " <name>effect</name>",
  52. " <description>Build applications with Effect</description>",
  53. " </skill>",
  54. "</available_skills>",
  55. ].join("\n"),
  56. )
  57. skills = []
  58. expect(
  59. yield* guidance
  60. .load({ id: agent.id, info: agent })
  61. .pipe(Effect.flatMap((context) => SystemContext.reconcile(context, initialized.snapshot))),
  62. ).toMatchObject({
  63. _tag: "Updated",
  64. text: expect.stringContaining("No skills are currently available."),
  65. })
  66. }).pipe(Effect.provide(layer(() => skills)))
  67. })
  68. it.effect("omits guidance when the selected agent denies all skills", () => {
  69. const agent = AgentV2.Info.make({
  70. ...AgentV2.Info.empty(build),
  71. permissions: [{ action: "skill", resource: "*", effect: "deny" }],
  72. })
  73. return Effect.gen(function* () {
  74. const guidance = yield* SkillGuidance.Service
  75. expect(
  76. yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(SystemContext.initialize)),
  77. ).toEqual({
  78. baseline: "",
  79. snapshot: {},
  80. })
  81. }).pipe(Effect.provide(layer(() => [effect])))
  82. })
  83. it.effect("omits guidance when a resource-specific denial follows the global denial", () => {
  84. const agent = AgentV2.Info.make({
  85. ...AgentV2.Info.empty(build),
  86. permissions: [
  87. { action: "skill", resource: "*", effect: "deny" },
  88. { action: "skill", resource: "hidden", effect: "deny" },
  89. ],
  90. })
  91. return Effect.gen(function* () {
  92. const guidance = yield* SkillGuidance.Service
  93. expect(
  94. yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(SystemContext.initialize)),
  95. ).toEqual({
  96. baseline: "",
  97. snapshot: {},
  98. })
  99. }).pipe(Effect.provide(layer(() => [effect])))
  100. })
  101. it.effect("retains specifically allowed skills after a global denial", () => {
  102. const agent = AgentV2.Info.make({
  103. ...AgentV2.Info.empty(build),
  104. permissions: [
  105. { action: "skill", resource: "*", effect: "deny" },
  106. { action: "skill", resource: "effect", effect: "allow" },
  107. ],
  108. })
  109. return Effect.gen(function* () {
  110. const guidance = yield* SkillGuidance.Service
  111. expect(
  112. (yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(SystemContext.initialize))).baseline,
  113. ).toContain("<name>effect</name>")
  114. }).pipe(Effect.provide(layer(() => [effect])))
  115. })
  116. it.effect("omits guidance when a specifically allowed skill is denied again", () => {
  117. const agent = AgentV2.Info.make({
  118. ...AgentV2.Info.empty(build),
  119. permissions: [
  120. { action: "skill", resource: "*", effect: "deny" },
  121. { action: "skill", resource: "effect", effect: "allow" },
  122. { action: "skill", resource: "effect", effect: "deny" },
  123. ],
  124. })
  125. return Effect.gen(function* () {
  126. const guidance = yield* SkillGuidance.Service
  127. expect(
  128. yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(SystemContext.initialize)),
  129. ).toEqual({
  130. baseline: "",
  131. snapshot: {},
  132. })
  133. }).pipe(Effect.provide(layer(() => [effect])))
  134. })
  135. })