system-prompt.test.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import { describe, expect, test } from "bun:test"
  2. import { SystemPart } from "@opencode-ai/ai"
  3. import { Agent } from "@opencode-ai/core/agent"
  4. import { Catalog } from "@opencode-ai/core/catalog"
  5. import { Plugin } from "@opencode-ai/core/plugin"
  6. import { PluginHooks } from "@opencode-ai/core/plugin/hooks"
  7. import { PluginHost } from "@opencode-ai/core/plugin/host"
  8. import { SystemPromptPlugin } from "@opencode-ai/core/plugin/system-prompt"
  9. import { Session } from "@opencode-ai/core/session"
  10. import type { SessionHooks } from "@opencode-ai/plugin/effect/session"
  11. import { Model } from "@opencode-ai/schema/model"
  12. import { Provider } from "@opencode-ai/schema/provider"
  13. import { Effect } from "effect"
  14. import { testEffect } from "../lib/effect"
  15. import { PluginTestLayer } from "./fixture"
  16. import PROMPT_META from "../../src/plugin/system-prompt/meta.txt"
  17. import PROMPT_DEFAULT from "../../src/session/runner/prompt/base.txt"
  18. const it = testEffect(PluginTestLayer)
  19. const fallback = PROMPT_DEFAULT
  20. const makeHost = Effect.gen(function* () {
  21. const agents = yield* Agent.Service
  22. const plugins = yield* Plugin.Service
  23. yield* agents.transform((draft) => draft.update(Agent.ID.make("build"), () => {}))
  24. return yield* PluginHost.make(plugins)
  25. })
  26. const context = (id: string, system = fallback): SessionHooks["context"] => ({
  27. sessionID: Session.ID.make("ses_system_prompt"),
  28. agent: Agent.ID.make("build"),
  29. model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make(id) }),
  30. system: [SystemPart.make(system)],
  31. messages: [],
  32. tools: {},
  33. })
  34. describe("SystemPromptPlugin", () => {
  35. test("uses current vocabulary in the Meta prompt", () => {
  36. expect(PROMPT_META).toContain("`webfetch` tool")
  37. expect(PROMPT_META).toContain("`subagent` tool")
  38. expect(PROMPT_META).toContain("Reserve `shell`")
  39. expect(PROMPT_META).toContain("`read` for reading files")
  40. expect(PROMPT_META).toContain("`edit` for editing")
  41. expect(PROMPT_META).toContain("`write` for creating files")
  42. expect(PROMPT_META).toContain("https://opencode.ai/v2/docs/")
  43. expect(PROMPT_META).not.toMatch(
  44. /TodoWrite|Task tool|WebFetch|\bBash\b|https:\/\/opencode\.ai\/docs/,
  45. )
  46. })
  47. test("uses granular IDs with a common prefix", () => {
  48. expect(SystemPromptPlugin.Plugins.map((plugin) => plugin.id)).toEqual([
  49. "opencode.system-prompt.openai",
  50. "opencode.system-prompt.google",
  51. "opencode.system-prompt.anthropic",
  52. "opencode.system-prompt.kimi",
  53. "opencode.system-prompt.arcee",
  54. "opencode.system-prompt.meta",
  55. ])
  56. })
  57. it.effect("selects model-lab prompts through session context hooks", () =>
  58. Effect.gen(function* () {
  59. const hooks = yield* PluginHooks.Service
  60. const pluginHost = yield* makeHost
  61. yield* Effect.forEach(SystemPromptPlugin.Plugins, (plugin) => plugin.effect(pluginHost), {
  62. discard: true,
  63. })
  64. const cases = [
  65. ["gpt-5", "You are OpenCode, You and the user share the same workspace"],
  66. ["gpt-4.1", "You are OpenCode, You and the user share the same workspace"],
  67. ["o3", "You are OpenCode, You and the user share the same workspace"],
  68. ["gpt-5-codex", "## Editing constraints"],
  69. ["gemini-2.5-pro", "# Core Mandates"],
  70. ["claude-sonnet-4", "# Professional objectivity"],
  71. ["kimi-k2", "# Prompt and Tool Use"],
  72. ["trinity", "what command should I run to list files"],
  73. ["meta/muse-spark-1.1", "powered by Muse Spark"],
  74. ["llama-3.3", "You are opencode, an interactive CLI tool"],
  75. ] as const
  76. yield* Effect.forEach(
  77. cases,
  78. ([id, expected]) => {
  79. const event = context(id)
  80. return hooks
  81. .trigger("session", "context", event)
  82. .pipe(Effect.tap(() => Effect.sync(() => expect(event.system[0]?.text).toContain(expected))))
  83. },
  84. { discard: true },
  85. )
  86. }),
  87. )
  88. it.effect("preserves an explicit agent system prompt", () =>
  89. Effect.gen(function* () {
  90. const agents = yield* Agent.Service
  91. const hooks = yield* PluginHooks.Service
  92. yield* agents.transform((draft) =>
  93. draft.update(Agent.ID.make("build"), (agent) => {
  94. agent.system = "Custom agent prompt"
  95. }),
  96. )
  97. const pluginHost = yield* makeHost
  98. yield* Effect.forEach(SystemPromptPlugin.Plugins, (plugin) => plugin.effect(pluginHost), {
  99. discard: true,
  100. })
  101. const event = context("gpt-5", "Custom agent prompt")
  102. yield* hooks.trigger("session", "context", event)
  103. expect(event.system.map((part) => part.text)).toEqual(["Custom agent prompt"])
  104. }),
  105. )
  106. it.effect("skips the hook when agent lookup fails", () =>
  107. Effect.gen(function* () {
  108. const agents = yield* Agent.Service
  109. const hooks = yield* PluginHooks.Service
  110. const pluginHost = yield* makeHost
  111. yield* SystemPromptPlugin.OpenAIPlugin.effect(pluginHost)
  112. yield* agents.transform((draft) => draft.remove(Agent.ID.make("build")))
  113. const event = context("gpt-5")
  114. yield* hooks.trigger("session", "context", event)
  115. expect(event.system[0]?.text).toBe(fallback)
  116. }),
  117. )
  118. it.effect("allows one model-lab prompt plugin to be enabled independently", () =>
  119. Effect.gen(function* () {
  120. const hooks = yield* PluginHooks.Service
  121. const pluginHost = yield* makeHost
  122. yield* SystemPromptPlugin.GooglePlugin.effect(pluginHost)
  123. const gemini = context("gemini-2.5-pro")
  124. const claude = context("claude-sonnet-4")
  125. yield* hooks.trigger("session", "context", gemini)
  126. yield* hooks.trigger("session", "context", claude)
  127. expect(gemini.system[0]?.text).toContain("# Core Mandates")
  128. expect(claude.system[0]?.text).toBe(fallback)
  129. }),
  130. )
  131. it.effect("selects against the catalog model ID instead of its alias", () =>
  132. Effect.gen(function* () {
  133. const catalog = yield* Catalog.Service
  134. const hooks = yield* PluginHooks.Service
  135. const pluginHost = yield* makeHost
  136. yield* catalog.transform((draft) => {
  137. draft.model.update(Provider.ID.make("test"), Model.ID.make("openai-alias"), (model) => {
  138. model.modelID = Model.ID.make("gpt-5")
  139. })
  140. draft.model.update(Provider.ID.make("test"), Model.ID.make("gpt-5-alias"), (model) => {
  141. model.modelID = Model.ID.make("custom-model")
  142. })
  143. draft.model.update(Provider.ID.make("test"), Model.ID.make("codex-family-alias"), (model) => {
  144. model.modelID = Model.ID.make("custom-deployment")
  145. model.family = Model.Family.make("gpt-codex")
  146. })
  147. })
  148. yield* SystemPromptPlugin.OpenAIPlugin.effect(pluginHost)
  149. const physicalOpenAI = context("openai-alias")
  150. const physicalCustom = context("gpt-5-alias")
  151. const familyOpenAI = context("codex-family-alias")
  152. yield* hooks.trigger("session", "context", physicalOpenAI)
  153. yield* hooks.trigger("session", "context", physicalCustom)
  154. yield* hooks.trigger("session", "context", familyOpenAI)
  155. expect(physicalOpenAI.system[0]?.text).toContain("You are OpenCode, You and the user share the same workspace")
  156. expect(physicalCustom.system[0]?.text).toBe(fallback)
  157. expect(familyOpenAI.system[0]?.text).toContain("## Editing constraints")
  158. }),
  159. )
  160. })