guidance.test.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  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 { PluginBoot } from "@opencode-ai/core/plugin/boot"
  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 = new SkillV2.Info({
  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 = new SkillV2.Info({
  19. name: "hidden",
  20. location: AbsolutePath.make(path.resolve("/skills/hidden/SKILL.md")),
  21. content: "Undescribed guidance",
  22. })
  23. const denied = new SkillV2.Info({
  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[], wait: () => void = () => {}) =>
  30. SkillGuidance.layer.pipe(
  31. Layer.provide(Layer.mock(SkillV2.Service, { list: () => Effect.succeed(list()) })),
  32. Layer.provide(Layer.mock(PluginBoot.Service, { wait: () => Effect.sync(wait) })),
  33. )
  34. describe("SkillGuidance", () => {
  35. it.effect("renders described agent skills and reconciles the complete available list", () => {
  36. const agent = new AgentV2.Info({
  37. ...AgentV2.Info.empty(build),
  38. permissions: [{ action: "skill", resource: "denied", effect: "deny" }],
  39. })
  40. let skills = [hidden, denied, effect]
  41. let waited = 0
  42. return Effect.gen(function* () {
  43. const guidance = yield* SkillGuidance.Service
  44. const initialized = yield* guidance
  45. .load({ id: agent.id, info: agent })
  46. .pipe(Effect.flatMap(SystemContext.initialize))
  47. expect(waited).toBe(1)
  48. expect(initialized.baseline).toBe(
  49. [
  50. "Skills provide specialized instructions and workflows for specific tasks.",
  51. "Use the skill tool to load a skill when a task matches its description.",
  52. "<available_skills>",
  53. " <skill>",
  54. " <name>effect</name>",
  55. " <description>Build applications with Effect</description>",
  56. " </skill>",
  57. "</available_skills>",
  58. ].join("\n"),
  59. )
  60. skills = []
  61. expect(
  62. yield* guidance
  63. .load({ id: agent.id, info: agent })
  64. .pipe(Effect.flatMap((context) => SystemContext.reconcile(context, initialized.snapshot))),
  65. ).toMatchObject({
  66. _tag: "Updated",
  67. text: expect.stringContaining("No skills are currently available."),
  68. })
  69. }).pipe(
  70. Effect.provide(
  71. layer(
  72. () => skills,
  73. () => waited++,
  74. ),
  75. ),
  76. )
  77. })
  78. it.effect("omits guidance when the selected agent denies all skills", () => {
  79. const agent = new AgentV2.Info({
  80. ...AgentV2.Info.empty(build),
  81. permissions: [{ action: "skill", resource: "*", effect: "deny" }],
  82. })
  83. return Effect.gen(function* () {
  84. const guidance = yield* SkillGuidance.Service
  85. expect(
  86. yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(SystemContext.initialize)),
  87. ).toEqual({
  88. baseline: "",
  89. snapshot: {},
  90. })
  91. }).pipe(Effect.provide(layer(() => [effect])))
  92. })
  93. it.effect("omits guidance when a resource-specific denial follows the global denial", () => {
  94. const agent = new AgentV2.Info({
  95. ...AgentV2.Info.empty(build),
  96. permissions: [
  97. { action: "skill", resource: "*", effect: "deny" },
  98. { action: "skill", resource: "hidden", effect: "deny" },
  99. ],
  100. })
  101. return Effect.gen(function* () {
  102. const guidance = yield* SkillGuidance.Service
  103. expect(
  104. yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(SystemContext.initialize)),
  105. ).toEqual({
  106. baseline: "",
  107. snapshot: {},
  108. })
  109. }).pipe(Effect.provide(layer(() => [effect])))
  110. })
  111. it.effect("retains specifically allowed skills after a global denial", () => {
  112. const agent = new AgentV2.Info({
  113. ...AgentV2.Info.empty(build),
  114. permissions: [
  115. { action: "skill", resource: "*", effect: "deny" },
  116. { action: "skill", resource: "effect", effect: "allow" },
  117. ],
  118. })
  119. return Effect.gen(function* () {
  120. const guidance = yield* SkillGuidance.Service
  121. expect(
  122. (yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(SystemContext.initialize))).baseline,
  123. ).toContain("<name>effect</name>")
  124. }).pipe(Effect.provide(layer(() => [effect])))
  125. })
  126. it.effect("omits guidance when a specifically allowed skill is denied again", () => {
  127. const agent = new AgentV2.Info({
  128. ...AgentV2.Info.empty(build),
  129. permissions: [
  130. { action: "skill", resource: "*", effect: "deny" },
  131. { action: "skill", resource: "effect", effect: "allow" },
  132. { action: "skill", resource: "effect", effect: "deny" },
  133. ],
  134. })
  135. return Effect.gen(function* () {
  136. const guidance = yield* SkillGuidance.Service
  137. expect(
  138. yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(SystemContext.initialize)),
  139. ).toEqual({
  140. baseline: "",
  141. snapshot: {},
  142. })
  143. }).pipe(Effect.provide(layer(() => [effect])))
  144. })
  145. })