summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAiden Cline <[email protected]>2025-12-20 19:36:22 -0800
committerGitHub <[email protected]>2025-12-20 21:36:22 -0600
commitd3922f0965a17491dc7fdf24af5b3ac0c9a72510 (patch)
treec12d80644cb2fccbd0120c7c04955a92f2054409
parentcfaac9f2e12acdfcf95459f450452d049d514513 (diff)
downloadopencode-d3922f0965a17491dc7fdf24af5b3ac0c9a72510.tar.gz
opencode-d3922f0965a17491dc7fdf24af5b3ac0c9a72510.zip
core: add verification that at least 1 primary agent is enabled, add regression tests (#5881)
-rw-r--r--packages/opencode/src/agent/agent.ts8
-rw-r--r--packages/opencode/test/agent/agent.test.ts146
-rw-r--r--packages/opencode/test/config/config.test.ts32
3 files changed, 186 insertions, 0 deletions
diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts
index 26f241fab..90c8594cd 100644
--- a/packages/opencode/src/agent/agent.ts
+++ b/packages/opencode/src/agent/agent.ts
@@ -262,6 +262,14 @@ export namespace Agent {
}
}
+ const hasPrimaryAgents = Object.values(result).filter((a) => a.mode !== "subagent" && !a.hidden).length > 0
+ if (!hasPrimaryAgents) {
+ throw new Config.InvalidError({
+ path: "config",
+ message: "No primary agents are available. Please configure at least one agent with mode 'primary' or 'all'.",
+ })
+ }
+
return result
})
diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts
new file mode 100644
index 000000000..222bf8367
--- /dev/null
+++ b/packages/opencode/test/agent/agent.test.ts
@@ -0,0 +1,146 @@
+import { test, expect } from "bun:test"
+import path from "path"
+import fs from "fs/promises"
+import { tmpdir } from "../fixture/fixture"
+import { Instance } from "../../src/project/instance"
+import { Agent } from "../../src/agent/agent"
+
+test("loads built-in agents when no custom agents configured", async () => {
+ await using tmp = await tmpdir()
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const agents = await Agent.list()
+ const names = agents.map((a) => a.name)
+ expect(names).toContain("build")
+ expect(names).toContain("plan")
+ },
+ })
+})
+
+test("custom subagent works alongside built-in primary agents", async () => {
+ await using tmp = await tmpdir({
+ init: async (dir) => {
+ const opencodeDir = path.join(dir, ".opencode")
+ await fs.mkdir(opencodeDir, { recursive: true })
+ const agentDir = path.join(opencodeDir, "agent")
+ await fs.mkdir(agentDir, { recursive: true })
+
+ await Bun.write(
+ path.join(agentDir, "helper.md"),
+ `---
+model: test/model
+mode: subagent
+---
+Helper subagent prompt`,
+ )
+ },
+ })
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const agents = await Agent.list()
+ const helper = agents.find((a) => a.name === "helper")
+ expect(helper).toBeDefined()
+ expect(helper?.mode).toBe("subagent")
+
+ // Built-in primary agents should still exist
+ const build = agents.find((a) => a.name === "build")
+ expect(build).toBeDefined()
+ expect(build?.mode).toBe("primary")
+ },
+ })
+})
+
+test("throws error when all primary agents are disabled", async () => {
+ await using tmp = await tmpdir({
+ init: async (dir) => {
+ await Bun.write(
+ path.join(dir, "opencode.json"),
+ JSON.stringify({
+ $schema: "https://opencode.ai/config.json",
+ agent: {
+ build: { disable: true },
+ plan: { disable: true },
+ },
+ }),
+ )
+ },
+ })
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ try {
+ await Agent.list()
+ expect(true).toBe(false) // should not reach here
+ } catch (e: any) {
+ expect(e.data?.message).toContain("No primary agents are available")
+ }
+ },
+ })
+})
+
+test("does not throw when at least one primary agent remains", async () => {
+ await using tmp = await tmpdir({
+ init: async (dir) => {
+ await Bun.write(
+ path.join(dir, "opencode.json"),
+ JSON.stringify({
+ $schema: "https://opencode.ai/config.json",
+ agent: {
+ build: { disable: true },
+ },
+ }),
+ )
+ },
+ })
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const agents = await Agent.list()
+ const plan = agents.find((a) => a.name === "plan")
+ expect(plan).toBeDefined()
+ expect(plan?.mode).toBe("primary")
+ },
+ })
+})
+
+test("custom primary agent satisfies requirement when built-ins disabled", async () => {
+ await using tmp = await tmpdir({
+ init: async (dir) => {
+ const opencodeDir = path.join(dir, ".opencode")
+ await fs.mkdir(opencodeDir, { recursive: true })
+ const agentDir = path.join(opencodeDir, "agent")
+ await fs.mkdir(agentDir, { recursive: true })
+
+ await Bun.write(
+ path.join(agentDir, "custom.md"),
+ `---
+model: test/model
+mode: primary
+---
+Custom primary agent`,
+ )
+
+ await Bun.write(
+ path.join(dir, "opencode.json"),
+ JSON.stringify({
+ $schema: "https://opencode.ai/config.json",
+ agent: {
+ build: { disable: true },
+ plan: { disable: true },
+ },
+ }),
+ )
+ },
+ })
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const agents = await Agent.list()
+ const custom = agents.find((a) => a.name === "custom")
+ expect(custom).toBeDefined()
+ expect(custom?.mode).toBe("primary")
+ },
+ })
+})
diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts
index 2ff8c01cd..6f43cab61 100644
--- a/packages/opencode/test/config/config.test.ts
+++ b/packages/opencode/test/config/config.test.ts
@@ -450,6 +450,38 @@ test("merges plugin arrays from global and local configs", async () => {
})
})
+test("does not error when only custom agent is a subagent", async () => {
+ await using tmp = await tmpdir({
+ init: async (dir) => {
+ const opencodeDir = path.join(dir, ".opencode")
+ await fs.mkdir(opencodeDir, { recursive: true })
+ const agentDir = path.join(opencodeDir, "agent")
+ await fs.mkdir(agentDir, { recursive: true })
+
+ await Bun.write(
+ path.join(agentDir, "helper.md"),
+ `---
+model: test/model
+mode: subagent
+---
+Helper subagent prompt`,
+ )
+ },
+ })
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const config = await Config.get()
+ expect(config.agent?.["helper"]).toEqual({
+ name: "helper",
+ model: "test/model",
+ mode: "subagent",
+ prompt: "Helper subagent prompt",
+ })
+ },
+ })
+})
+
test("deduplicates duplicate plugins from global and local configs", async () => {
await using tmp = await tmpdir({
init: async (dir) => {