| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178 |
- import { describe, expect, test } from "bun:test"
- import { SystemPart } from "@opencode-ai/ai"
- import { Agent } from "@opencode-ai/core/agent"
- import { Catalog } from "@opencode-ai/core/catalog"
- import { Plugin } from "@opencode-ai/core/plugin"
- import { PluginHooks } from "@opencode-ai/core/plugin/hooks"
- import { PluginHost } from "@opencode-ai/core/plugin/host"
- import { SystemPromptPlugin } from "@opencode-ai/core/plugin/system-prompt"
- import { Session } from "@opencode-ai/core/session"
- import type { SessionHooks } from "@opencode-ai/plugin/effect/session"
- import { Model } from "@opencode-ai/schema/model"
- import { Provider } from "@opencode-ai/schema/provider"
- import { Effect } from "effect"
- import { testEffect } from "../lib/effect"
- import { PluginTestLayer } from "./fixture"
- import PROMPT_META from "../../src/plugin/system-prompt/meta.txt"
- import PROMPT_DEFAULT from "../../src/session/runner/prompt/base.txt"
- const it = testEffect(PluginTestLayer)
- const fallback = PROMPT_DEFAULT
- const makeHost = Effect.gen(function* () {
- const agents = yield* Agent.Service
- const plugins = yield* Plugin.Service
- yield* agents.transform((draft) => draft.update(Agent.ID.make("build"), () => {}))
- return yield* PluginHost.make(plugins)
- })
- const context = (id: string, system = fallback): SessionHooks["context"] => ({
- sessionID: Session.ID.make("ses_system_prompt"),
- agent: Agent.ID.make("build"),
- model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make(id) }),
- system: [SystemPart.make(system)],
- messages: [],
- tools: {},
- })
- describe("SystemPromptPlugin", () => {
- test("uses current vocabulary in the Meta prompt", () => {
- expect(PROMPT_META).toContain("`webfetch` tool")
- expect(PROMPT_META).toContain("`subagent` tool")
- expect(PROMPT_META).toContain("Reserve `shell`")
- expect(PROMPT_META).toContain("`read` for reading files")
- expect(PROMPT_META).toContain("`edit` for editing")
- expect(PROMPT_META).toContain("`write` for creating files")
- expect(PROMPT_META).toContain("https://opencode.ai/v2/docs/")
- expect(PROMPT_META).not.toMatch(
- /TodoWrite|Task tool|WebFetch|\bBash\b|https:\/\/opencode\.ai\/docs/,
- )
- })
- test("uses granular IDs with a common prefix", () => {
- expect(SystemPromptPlugin.Plugins.map((plugin) => plugin.id)).toEqual([
- "opencode.system-prompt.openai",
- "opencode.system-prompt.google",
- "opencode.system-prompt.anthropic",
- "opencode.system-prompt.kimi",
- "opencode.system-prompt.arcee",
- "opencode.system-prompt.meta",
- ])
- })
- it.effect("selects model-lab prompts through session context hooks", () =>
- Effect.gen(function* () {
- const hooks = yield* PluginHooks.Service
- const pluginHost = yield* makeHost
- yield* Effect.forEach(SystemPromptPlugin.Plugins, (plugin) => plugin.effect(pluginHost), {
- discard: true,
- })
- const cases = [
- ["gpt-5", "You are OpenCode, You and the user share the same workspace"],
- ["gpt-4.1", "You are OpenCode, You and the user share the same workspace"],
- ["o3", "You are OpenCode, You and the user share the same workspace"],
- ["gpt-5-codex", "## Editing constraints"],
- ["gemini-2.5-pro", "# Core Mandates"],
- ["claude-sonnet-4", "# Professional objectivity"],
- ["kimi-k2", "# Prompt and Tool Use"],
- ["trinity", "what command should I run to list files"],
- ["meta/muse-spark-1.1", "powered by Muse Spark"],
- ["llama-3.3", "You are opencode, an interactive CLI tool"],
- ] as const
- yield* Effect.forEach(
- cases,
- ([id, expected]) => {
- const event = context(id)
- return hooks
- .trigger("session", "context", event)
- .pipe(Effect.tap(() => Effect.sync(() => expect(event.system[0]?.text).toContain(expected))))
- },
- { discard: true },
- )
- }),
- )
- it.effect("preserves an explicit agent system prompt", () =>
- Effect.gen(function* () {
- const agents = yield* Agent.Service
- const hooks = yield* PluginHooks.Service
- yield* agents.transform((draft) =>
- draft.update(Agent.ID.make("build"), (agent) => {
- agent.system = "Custom agent prompt"
- }),
- )
- const pluginHost = yield* makeHost
- yield* Effect.forEach(SystemPromptPlugin.Plugins, (plugin) => plugin.effect(pluginHost), {
- discard: true,
- })
- const event = context("gpt-5", "Custom agent prompt")
- yield* hooks.trigger("session", "context", event)
- expect(event.system.map((part) => part.text)).toEqual(["Custom agent prompt"])
- }),
- )
- it.effect("skips the hook when agent lookup fails", () =>
- Effect.gen(function* () {
- const agents = yield* Agent.Service
- const hooks = yield* PluginHooks.Service
- const pluginHost = yield* makeHost
- yield* SystemPromptPlugin.OpenAIPlugin.effect(pluginHost)
- yield* agents.transform((draft) => draft.remove(Agent.ID.make("build")))
- const event = context("gpt-5")
- yield* hooks.trigger("session", "context", event)
- expect(event.system[0]?.text).toBe(fallback)
- }),
- )
- it.effect("allows one model-lab prompt plugin to be enabled independently", () =>
- Effect.gen(function* () {
- const hooks = yield* PluginHooks.Service
- const pluginHost = yield* makeHost
- yield* SystemPromptPlugin.GooglePlugin.effect(pluginHost)
- const gemini = context("gemini-2.5-pro")
- const claude = context("claude-sonnet-4")
- yield* hooks.trigger("session", "context", gemini)
- yield* hooks.trigger("session", "context", claude)
- expect(gemini.system[0]?.text).toContain("# Core Mandates")
- expect(claude.system[0]?.text).toBe(fallback)
- }),
- )
- it.effect("selects against the catalog model ID instead of its alias", () =>
- Effect.gen(function* () {
- const catalog = yield* Catalog.Service
- const hooks = yield* PluginHooks.Service
- const pluginHost = yield* makeHost
- yield* catalog.transform((draft) => {
- draft.model.update(Provider.ID.make("test"), Model.ID.make("openai-alias"), (model) => {
- model.modelID = Model.ID.make("gpt-5")
- })
- draft.model.update(Provider.ID.make("test"), Model.ID.make("gpt-5-alias"), (model) => {
- model.modelID = Model.ID.make("custom-model")
- })
- draft.model.update(Provider.ID.make("test"), Model.ID.make("codex-family-alias"), (model) => {
- model.modelID = Model.ID.make("custom-deployment")
- model.family = Model.Family.make("gpt-codex")
- })
- })
- yield* SystemPromptPlugin.OpenAIPlugin.effect(pluginHost)
- const physicalOpenAI = context("openai-alias")
- const physicalCustom = context("gpt-5-alias")
- const familyOpenAI = context("codex-family-alias")
- yield* hooks.trigger("session", "context", physicalOpenAI)
- yield* hooks.trigger("session", "context", physicalCustom)
- yield* hooks.trigger("session", "context", familyOpenAI)
- expect(physicalOpenAI.system[0]?.text).toContain("You are OpenCode, You and the user share the same workspace")
- expect(physicalCustom.system[0]?.text).toBe(fallback)
- expect(familyOpenAI.system[0]?.text).toContain("## Editing constraints")
- }),
- )
- })
|