instructions.test.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import { describe, expect } from "bun:test"
  2. import { CodeModeCatalog } from "@opencode-ai/core/codemode/catalog"
  3. import { CodeModeInstructions } from "@opencode-ai/core/codemode/instructions"
  4. import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
  5. import { Location } from "@opencode-ai/core/location"
  6. import { AbsolutePath } from "@opencode-ai/core/schema"
  7. import { Tool } from "@opencode-ai/core/tool"
  8. import { Effect, Schema } from "effect"
  9. import { it } from "../lib/effect"
  10. import { readInitial, readUpdate } from "../lib/instructions"
  11. const echo: CodeModeCatalog.Entry = {
  12. path: "notes.echo",
  13. description: "Echo text",
  14. signature: "tools.notes.echo(input: {\n text: string,\n}): Promise<string>",
  15. }
  16. const lookup: CodeModeCatalog.Entry = {
  17. path: "orders.lookup",
  18. description: "Look up an order",
  19. signature: "tools.orders.lookup(input: {\n id: string,\n}): Promise<unknown>",
  20. }
  21. describe("CodeModeInstructions", () => {
  22. it.effect("instructs the model not to call execute while the catalog is empty", () =>
  23. Effect.gen(function* () {
  24. const initialized = yield* readInitial(CodeModeInstructions.make([]))
  25. expect(initialized.text).toBe(
  26. "No Code Mode tools are currently available. Later Code Mode catalog updates may add or remove tools. Do not call `execute` unless there is at least one available Code Mode tool.",
  27. )
  28. const added = yield* readUpdate(CodeModeInstructions.make([echo]), initialized)
  29. expect(added.text).toContain("New tools are available in addition to those previously listed:")
  30. expect(added.text).toContain(echo.signature)
  31. expect(yield* readUpdate(CodeModeInstructions.make([]), { values: added.values })).toMatchObject({
  32. text:
  33. "The Code Mode tool catalog has changed. This catalog supersedes the previous Code Mode tool catalog.\n\n" +
  34. "No Code Mode tools are currently available. Later Code Mode catalog updates may add or remove tools. Do not call `execute` unless there is at least one available Code Mode tool.",
  35. })
  36. }),
  37. )
  38. it.effect("renders the initial catalog, semantic deltas, and removal", () =>
  39. Effect.gen(function* () {
  40. const initialized = yield* readInitial(CodeModeInstructions.make([echo]))
  41. expect(initialized.text).toContain(
  42. "This catalog is the complete set of tools available within Code Mode. Tools presented elsewhere are not available in this runtime.",
  43. )
  44. expect(initialized.text).toContain("## Available tools")
  45. expect(initialized.text).not.toContain("## Search")
  46. expect(initialized.text).toContain(` - ${echo.signature} // Echo text`)
  47. const added = yield* readUpdate(CodeModeInstructions.make([echo, lookup]), initialized)
  48. expect(added.text).toContain("The Code Mode tool catalog has changed.")
  49. expect(added.text).toContain("New tools are available in addition to those previously listed:")
  50. expect(added.text).toContain(` - ${lookup.signature} // Look up an order`)
  51. expect(added.text).not.toContain("## Available tools")
  52. const removed = yield* readUpdate(CodeModeInstructions.make([echo]), { values: added.values })
  53. expect(removed.text).toBe(
  54. "The Code Mode tool catalog has changed.\n\n" +
  55. "The following tools are no longer available and must not be called: tools.orders.lookup.",
  56. )
  57. expect(yield* readUpdate(CodeModeInstructions.make(), initialized)).toMatchObject({
  58. text: "Code Mode tools are no longer available. Do not use any previously listed Code Mode tools.",
  59. })
  60. }),
  61. )
  62. it.effect("stores a canonical sorted snapshot so registration order does not churn history", () => {
  63. const alpha = ({
  64. name: "alpha",
  65. description: "Alpha tool",
  66. input: Schema.Struct({}),
  67. output: Schema.String,
  68. execute: () => Effect.succeed({ output: "alpha" }),
  69. })
  70. const zeta = ({
  71. name: "zeta",
  72. description: "Zeta tool",
  73. input: Schema.Struct({}),
  74. output: Schema.String,
  75. execute: () => Effect.succeed({ output: "zeta" }),
  76. })
  77. const layer = AppNodeBuilder.build(Tool.node, [
  78. [Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
  79. ])
  80. return Effect.gen(function* () {
  81. const tools = yield* Tool.Service
  82. const initialized = yield* Effect.scoped(
  83. Effect.gen(function* () {
  84. yield* tools.transform((draft) => {
  85. draft.add({ ...zeta, options: { namespace: "tools" } })
  86. draft.add({ ...alpha, options: { namespace: "tools" } })
  87. })
  88. return yield* readInitial(
  89. CodeModeInstructions.make((yield* tools.snapshot()).codeModeCatalog),
  90. )
  91. }),
  92. )
  93. const reordered = yield* Effect.scoped(
  94. Effect.gen(function* () {
  95. yield* tools.transform((draft) => {
  96. draft.add({ ...alpha, options: { namespace: "tools" } })
  97. draft.add({ ...zeta, options: { namespace: "tools" } })
  98. })
  99. return yield* readUpdate(
  100. CodeModeInstructions.make((yield* tools.snapshot()).codeModeCatalog),
  101. initialized,
  102. )
  103. }),
  104. )
  105. expect(reordered.changed).toBe(false)
  106. expect(reordered.text).toBe("")
  107. }).pipe(Effect.provide(layer))
  108. })
  109. })