guidance.test.ts 5.1 KB

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