From d3922f0965a17491dc7fdf24af5b3ac0c9a72510 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 20 Dec 2025 19:36:22 -0800 Subject: [PATCH] core: add verification that at least 1 primary agent is enabled, add regression tests (#5881) --- packages/opencode/src/agent/agent.ts | 8 + packages/opencode/test/agent/agent.test.ts | 146 +++++++++++++++++++ packages/opencode/test/config/config.test.ts | 32 ++++ 3 files changed, 186 insertions(+) create mode 100644 packages/opencode/test/agent/agent.test.ts 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) => {