4438 lines
152 KiB
TypeScript
4438 lines
152 KiB
TypeScript
declare const require: NodeJS.Require
|
|
const { describe, test, expect, beforeEach, afterEach, spyOn, mock } = require("bun:test")
|
|
import { DEFAULT_CATEGORIES, CATEGORY_PROMPT_APPENDS, CATEGORY_DESCRIPTIONS, isPlanAgent, PLAN_AGENT_NAMES, isPlanFamily, PLAN_FAMILY_NAMES } from "./constants"
|
|
import { getAgentDisplayName, getAgentListDisplayName } from "../../shared/agent-display-names"
|
|
import type { CategoryConfig } from "../../config/schema"
|
|
import type { DelegateTaskArgs } from "./types"
|
|
import { __resetModelCache } from "../../shared/model-availability"
|
|
import { clearSkillCache } from "../../features/opencode-skill-loader/skill-content"
|
|
import { __setTimingConfig, __resetTimingConfig } from "./timing"
|
|
import * as connectedProvidersCache from "../../shared/connected-providers-cache"
|
|
import * as executor from "./executor"
|
|
import { releaseAllPromptAsyncReservationsForTesting } from "../../shared/prompt-async-gate"
|
|
|
|
const runtimeRequire = require as NodeJS.Require & { cache?: Record<string, unknown> }
|
|
|
|
function clearRequireCache(modulePath: string): void {
|
|
const resolvedPath = runtimeRequire.resolve(modulePath)
|
|
if (runtimeRequire.cache?.[resolvedPath]) {
|
|
delete runtimeRequire.cache[resolvedPath]
|
|
}
|
|
}
|
|
|
|
function resolveCategoryConfig(...args: Parameters<typeof import("./tools").resolveCategoryConfig>): ReturnType<typeof import("./tools").resolveCategoryConfig> {
|
|
clearRequireCache("./tools")
|
|
return require("./tools").resolveCategoryConfig(...args)
|
|
}
|
|
|
|
const SYSTEM_DEFAULT_MODEL = "anthropic/claude-sonnet-4-6"
|
|
|
|
const TEST_CONNECTED_PROVIDERS = ["anthropic", "google", "openai"]
|
|
const TEST_AVAILABLE_MODELS = new Set([
|
|
"anthropic/claude-opus-4-7",
|
|
"anthropic/claude-sonnet-4-6",
|
|
"anthropic/claude-haiku-4-5",
|
|
"google/gemini-3.1-pro",
|
|
"google/gemini-3-flash",
|
|
"openai/gpt-5.4-mini",
|
|
"openai/gpt-5.5",
|
|
"openai/gpt-5.3-codex",
|
|
])
|
|
|
|
type DelegateTaskArgsWithSerializedSkills = Omit<DelegateTaskArgs, "load_skills"> & {
|
|
load_skills: string
|
|
}
|
|
|
|
function createTestAvailableModels(): Set<string> {
|
|
return new Set(TEST_AVAILABLE_MODELS)
|
|
}
|
|
|
|
describe("sisyphus-task", () => {
|
|
let cacheSpy: ReturnType<typeof spyOn>
|
|
let providerModelsSpy: ReturnType<typeof spyOn>
|
|
|
|
beforeEach(() => {
|
|
mock.restore()
|
|
clearRequireCache("./tools")
|
|
__resetModelCache()
|
|
clearSkillCache()
|
|
__setTimingConfig({
|
|
POLL_INTERVAL_MS: 10,
|
|
MIN_STABILITY_TIME_MS: 50,
|
|
STABILITY_POLLS_REQUIRED: 1,
|
|
WAIT_FOR_SESSION_INTERVAL_MS: 10,
|
|
WAIT_FOR_SESSION_TIMEOUT_MS: 1000,
|
|
MAX_POLL_TIME_MS: 2000,
|
|
SESSION_CONTINUATION_STABILITY_MS: 50,
|
|
})
|
|
cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["anthropic", "google", "openai"])
|
|
providerModelsSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({
|
|
models: {
|
|
anthropic: ["claude-opus-4-7", "claude-sonnet-4-6", "claude-haiku-4-5"],
|
|
google: ["gemini-3.1-pro", "gemini-3-flash"],
|
|
openai: ["gpt-5.5", "gpt-5.4-mini", "gpt-5.3-codex"],
|
|
},
|
|
connected: ["anthropic", "google", "openai"],
|
|
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
})
|
|
})
|
|
|
|
afterEach(() => {
|
|
__resetTimingConfig()
|
|
releaseAllPromptAsyncReservationsForTesting()
|
|
cacheSpy?.mockRestore()
|
|
providerModelsSpy?.mockRestore()
|
|
})
|
|
|
|
describe("DEFAULT_CATEGORIES", () => {
|
|
test("visual-engineering category has model and variant config", () => {
|
|
// given
|
|
const category = DEFAULT_CATEGORIES["visual-engineering"]
|
|
|
|
// when / #then
|
|
expect(category).toBeDefined()
|
|
expect(category.model).toBe("google/gemini-3.1-pro")
|
|
expect(category.variant).toBe("high")
|
|
})
|
|
|
|
test("ultrabrain category has model and variant config", () => {
|
|
// given
|
|
const category = DEFAULT_CATEGORIES["ultrabrain"]
|
|
|
|
// when / #then
|
|
expect(category).toBeDefined()
|
|
expect(category.model).toBe("openai/gpt-5.5")
|
|
expect(category.variant).toBe("xhigh")
|
|
})
|
|
|
|
test("deep category has model and variant config", () => {
|
|
// given
|
|
const category = DEFAULT_CATEGORIES["deep"]
|
|
|
|
// when / #then
|
|
expect(category).toBeDefined()
|
|
expect(category.model).toBe("openai/gpt-5.5")
|
|
expect(category.variant).toBe("medium")
|
|
})
|
|
|
|
test("unspecified-high category uses claude-opus-4-7 max as primary", () => {
|
|
// given
|
|
const category = DEFAULT_CATEGORIES["unspecified-high"]
|
|
|
|
// when / #then
|
|
expect(category).toBeDefined()
|
|
expect(category.model).toBe("anthropic/claude-opus-4-7")
|
|
expect(category.variant).toBe("max")
|
|
})
|
|
})
|
|
|
|
describe("CATEGORY_PROMPT_APPENDS", () => {
|
|
test("visual-engineering category has design-focused prompt", () => {
|
|
// given
|
|
const promptAppend = CATEGORY_PROMPT_APPENDS["visual-engineering"]
|
|
|
|
// when / #then
|
|
expect(promptAppend).toContain("VISUAL/UI")
|
|
expect(promptAppend).toContain("Design-first")
|
|
})
|
|
|
|
test("ultrabrain category has deep logical reasoning prompt", () => {
|
|
// given
|
|
const promptAppend = CATEGORY_PROMPT_APPENDS["ultrabrain"]
|
|
|
|
// when / #then
|
|
expect(promptAppend).toContain("DEEP LOGICAL REASONING")
|
|
expect(promptAppend).toContain("Strategic advisor")
|
|
})
|
|
|
|
test("deep category has goal-oriented autonomous prompt", () => {
|
|
// given
|
|
const promptAppend = CATEGORY_PROMPT_APPENDS["deep"]
|
|
|
|
// when / #then
|
|
expect(promptAppend).toContain("GOAL-ORIENTED")
|
|
expect(promptAppend).toContain("autonomous")
|
|
})
|
|
})
|
|
|
|
describe("CATEGORY_DESCRIPTIONS", () => {
|
|
test("has description for all default categories", () => {
|
|
// given
|
|
const defaultCategoryNames = Object.keys(DEFAULT_CATEGORIES)
|
|
|
|
// when / #then
|
|
for (const name of defaultCategoryNames) {
|
|
expect(CATEGORY_DESCRIPTIONS[name]).toBeDefined()
|
|
expect(CATEGORY_DESCRIPTIONS[name].length).toBeGreaterThan(0)
|
|
}
|
|
})
|
|
|
|
test("unspecified-high category exists and has description", () => {
|
|
// given / #when
|
|
const description = CATEGORY_DESCRIPTIONS["unspecified-high"]
|
|
|
|
// then
|
|
expect(description).toBeDefined()
|
|
expect(description).toContain("high effort")
|
|
})
|
|
})
|
|
|
|
describe("isPlanAgent", () => {
|
|
test("returns true for 'plan'", () => {
|
|
// given / #when
|
|
const result = isPlanAgent("plan")
|
|
|
|
// then
|
|
expect(result).toBe(true)
|
|
})
|
|
|
|
test("returns false for 'prometheus' (decoupled from plan)", () => {
|
|
//#given / #when
|
|
const result = isPlanAgent("prometheus")
|
|
|
|
//#then - prometheus is NOT a plan agent
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
test("returns false for 'planner' (no longer matches via substring)", () => {
|
|
//#given / #when
|
|
const result = isPlanAgent("planner")
|
|
|
|
//#then - "planner" is NOT an exact match for "plan" (T37 exact match fix)
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
test("returns true for case-insensitive match 'PLAN'", () => {
|
|
// given / #when
|
|
const result = isPlanAgent("PLAN")
|
|
|
|
// then
|
|
expect(result).toBe(true)
|
|
})
|
|
|
|
test("returns false for case-insensitive match 'Prometheus' (decoupled from plan)", () => {
|
|
//#given / #when
|
|
const result = isPlanAgent("Prometheus")
|
|
|
|
//#then - Prometheus is NOT a plan agent
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
test("returns false for 'oracle'", () => {
|
|
// given / #when
|
|
const result = isPlanAgent("oracle")
|
|
|
|
// then
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
test("returns false for 'explore'", () => {
|
|
// given / #when
|
|
const result = isPlanAgent("explore")
|
|
|
|
// then
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
test("returns false for undefined", () => {
|
|
// given / #when
|
|
const result = isPlanAgent(undefined)
|
|
|
|
// then
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
test("returns false for empty string", () => {
|
|
// given / #when
|
|
const result = isPlanAgent("")
|
|
|
|
// then
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
test("PLAN_AGENT_NAMES contains only plan", () => {
|
|
//#given / #when / #then
|
|
expect(PLAN_AGENT_NAMES).toEqual(["plan"])
|
|
})
|
|
|
|
test("returns false for non-plan agent display names (regression: isPlanAgent display-name false-positive)", () => {
|
|
//#given / #when / #then
|
|
expect(isPlanAgent(getAgentDisplayName("metis"))).toBe(false)
|
|
expect(isPlanAgent(getAgentDisplayName("momus"))).toBe(false)
|
|
expect(isPlanAgent(getAgentDisplayName("atlas"))).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("isPlanFamily", () => {
|
|
test("returns true for 'plan'", () => {
|
|
//#given / #when
|
|
const result = isPlanFamily("plan")
|
|
//#then
|
|
expect(result).toBe(true)
|
|
})
|
|
|
|
test("returns true for 'prometheus'", () => {
|
|
//#given / #when
|
|
const result = isPlanFamily("prometheus")
|
|
//#then
|
|
expect(result).toBe(true)
|
|
})
|
|
|
|
test("returns true for prometheus display name", () => {
|
|
//#given / #when
|
|
const result = isPlanFamily(getAgentDisplayName("prometheus"))
|
|
//#then
|
|
expect(result).toBe(true)
|
|
})
|
|
|
|
test("returns true for prometheus list display name with zwsp prefix", () => {
|
|
//#given / #when
|
|
const result = isPlanFamily(getAgentListDisplayName("prometheus"))
|
|
//#then
|
|
expect(result).toBe(true)
|
|
})
|
|
|
|
test("returns false for 'oracle'", () => {
|
|
//#given / #when
|
|
const result = isPlanFamily("oracle")
|
|
//#then
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
test("returns false for 'Momus (Plan Critic)' - no substring false-positive", () => {
|
|
//#given / #when
|
|
const result = isPlanFamily("Momus (Plan Critic)")
|
|
//#then
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
test("returns false for 'Momus - Plan Critic' - no substring false-positive", () => {
|
|
//#given / #when
|
|
const result = isPlanFamily("Momus - Plan Critic")
|
|
//#then
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
test("returns false for 'Metis - Plan Consultant' - no substring false-positive", () => {
|
|
//#given / #when
|
|
const result = isPlanFamily("Metis - Plan Consultant")
|
|
//#then
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
test("returns false for undefined", () => {
|
|
//#given / #when
|
|
const result = isPlanFamily(undefined)
|
|
//#then
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
test("returns false for non-plan-family agent display names (regression: isPlanFamily includes() false-positive)", () => {
|
|
//#given / #when / #then
|
|
expect(isPlanFamily(getAgentDisplayName("metis"))).toBe(false)
|
|
expect(isPlanFamily(getAgentDisplayName("momus"))).toBe(false)
|
|
expect(isPlanFamily(getAgentDisplayName("atlas"))).toBe(false)
|
|
})
|
|
|
|
test("PLAN_FAMILY_NAMES contains plan and prometheus", () => {
|
|
//#given / #when / #then
|
|
expect(PLAN_FAMILY_NAMES).toEqual(["plan", "prometheus"])
|
|
})
|
|
})
|
|
|
|
describe("load_skills parsing", () => {
|
|
test("parses valid JSON string into array before validation", async () => {
|
|
//#given
|
|
const { createDelegateTask } = require("./tools")
|
|
|
|
const mockManager = {
|
|
launch: async () => ({
|
|
id: "task-123",
|
|
status: "pending",
|
|
description: "Parse test",
|
|
agent: "sisyphus-junior",
|
|
sessionID: "test-session",
|
|
}),
|
|
}
|
|
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({}) },
|
|
provider: { list: async () => ({ data: { connected: ["openai"] } }) },
|
|
model: { list: async () => ({ data: [{ provider: "openai", id: "gpt-5.3-codex" }] }) },
|
|
session: {
|
|
create: async () => ({ data: { id: "test-session" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
status: async () => ({ data: {} }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
connectedProvidersOverride: TEST_CONNECTED_PROVIDERS,
|
|
availableModelsOverride: createTestAvailableModels(),
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
const resolveSkillContentSpy = spyOn(executor, "resolveSkillContent").mockResolvedValue({
|
|
content: "resolved skill content",
|
|
error: null,
|
|
})
|
|
|
|
const args: DelegateTaskArgsWithSerializedSkills = {
|
|
description: "Parse valid string",
|
|
prompt: "Load skill parsing test",
|
|
category: "quick",
|
|
run_in_background: true,
|
|
load_skills: '["playwright", "git-master"]',
|
|
}
|
|
|
|
//#when
|
|
await tool.execute(args, toolContext)
|
|
|
|
//#then
|
|
expect(args.load_skills).toEqual(["playwright", "git-master"])
|
|
expect(resolveSkillContentSpy).toHaveBeenCalledWith(["playwright", "git-master"], expect.any(Object))
|
|
}, { timeout: 10000 })
|
|
|
|
test("defaults to [] when load_skills is malformed JSON", async () => {
|
|
//#given
|
|
const { createDelegateTask } = require("./tools")
|
|
|
|
const mockManager = {
|
|
launch: async () => ({
|
|
id: "task-456",
|
|
status: "pending",
|
|
description: "Parse test",
|
|
agent: "sisyphus-junior",
|
|
sessionID: "test-session",
|
|
}),
|
|
}
|
|
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({}) },
|
|
provider: { list: async () => ({ data: { connected: ["openai"] } }) },
|
|
model: { list: async () => ({ data: [{ provider: "openai", id: "gpt-5.3-codex" }] }) },
|
|
session: {
|
|
create: async () => ({ data: { id: "test-session" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
status: async () => ({ data: {} }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
connectedProvidersOverride: TEST_CONNECTED_PROVIDERS,
|
|
availableModelsOverride: createTestAvailableModels(),
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
const resolveSkillContentSpy = spyOn(executor, "resolveSkillContent").mockResolvedValue({
|
|
content: "resolved skill content",
|
|
error: null,
|
|
})
|
|
|
|
const args: DelegateTaskArgsWithSerializedSkills = {
|
|
description: "Parse malformed string",
|
|
prompt: "Load skill parsing test",
|
|
category: "quick",
|
|
run_in_background: true,
|
|
load_skills: '["playwright", "git-master"',
|
|
}
|
|
|
|
//#when
|
|
await tool.execute(args, toolContext)
|
|
|
|
//#then
|
|
expect(args.load_skills).toEqual([])
|
|
expect(resolveSkillContentSpy).toHaveBeenCalledWith([], expect.any(Object))
|
|
}, { timeout: 10000 })
|
|
})
|
|
|
|
describe("category delegation config validation", () => {
|
|
test("fills subagent_type as sisyphus-junior when category is provided without subagent_type", async () => {
|
|
// given
|
|
const { createDelegateTask } = require("./tools")
|
|
|
|
const mockManager = {
|
|
launch: async () => ({
|
|
id: "task-123",
|
|
status: "pending",
|
|
description: "Test task",
|
|
agent: "sisyphus-junior",
|
|
sessionID: "test-session",
|
|
}),
|
|
}
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({}) },
|
|
provider: { list: async () => ({ data: { connected: ["openai"] } }) },
|
|
model: { list: async () => ({ data: [{ provider: "openai", id: "gpt-5.3-codex" }] }) },
|
|
session: {
|
|
create: async () => ({ data: { id: "test-session" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
status: async () => ({ data: {} }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
connectedProvidersOverride: TEST_CONNECTED_PROVIDERS,
|
|
availableModelsOverride: createTestAvailableModels(),
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
const args: {
|
|
description: string
|
|
prompt: string
|
|
category: string
|
|
run_in_background: boolean
|
|
load_skills: string[]
|
|
subagent_type?: string
|
|
} = {
|
|
description: "Quick category test",
|
|
prompt: "Do something",
|
|
category: "quick",
|
|
run_in_background: true,
|
|
load_skills: [],
|
|
}
|
|
|
|
// when
|
|
await tool.execute(args, toolContext)
|
|
|
|
// then
|
|
expect(args.subagent_type).toBe("Sisyphus-Junior")
|
|
}, { timeout: 10000 })
|
|
|
|
test("prefers category over subagent_type when both are provided", async () => {
|
|
//#given
|
|
const { createDelegateTask } = require("./tools")
|
|
|
|
const mockManager = {
|
|
launch: async () => ({
|
|
id: "task-override",
|
|
status: "pending",
|
|
description: "Override test",
|
|
agent: "sisyphus-junior",
|
|
sessionID: "test-session",
|
|
}),
|
|
}
|
|
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({}) },
|
|
provider: { list: async () => ({ data: { connected: ["openai"] } }) },
|
|
model: { list: async () => ({ data: [{ provider: "openai", id: "gpt-5.3-codex" }] }) },
|
|
session: {
|
|
create: async () => ({ data: { id: "test-session" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
status: async () => ({ data: {} }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
connectedProvidersOverride: TEST_CONNECTED_PROVIDERS,
|
|
availableModelsOverride: createTestAvailableModels(),
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
const args = {
|
|
description: "Override test",
|
|
prompt: "Do something",
|
|
category: "quick",
|
|
subagent_type: "oracle",
|
|
run_in_background: true,
|
|
load_skills: [],
|
|
}
|
|
|
|
//#when
|
|
await tool.execute(args, toolContext)
|
|
|
|
//#then - category takes precedence, subagent_type is overridden to sisyphus-junior
|
|
expect(args.subagent_type).toBe("Sisyphus-Junior")
|
|
}, { timeout: 10000 })
|
|
|
|
test("proceeds without error when systemDefaultModel is undefined", async () => {
|
|
// given a mock client with no model in config
|
|
const { createDelegateTask } = require("./tools")
|
|
|
|
const mockManager = { launch: async () => ({ id: "task-123", status: "pending", description: "Test task", agent: "sisyphus-junior", sessionId: "test-session" }) }
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({}) }, // No model configured
|
|
provider: { list: async () => ({ data: { connected: ["openai"] } }) },
|
|
model: { list: async () => ({ data: [{ provider: "openai", id: "gpt-5.3-codex" }] }) },
|
|
session: {
|
|
create: async () => ({ data: { id: "test-session" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
status: async () => ({ data: {} }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
connectedProvidersOverride: TEST_CONNECTED_PROVIDERS,
|
|
availableModelsOverride: createTestAvailableModels(),
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when delegating with a category
|
|
const result = await tool.execute(
|
|
{
|
|
description: "Test task",
|
|
prompt: "Do something",
|
|
category: "ultrabrain",
|
|
run_in_background: true,
|
|
load_skills: [],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then proceeds without error - uses fallback chain
|
|
expect(result).not.toContain("oh-my-opencode requires a default model")
|
|
}, { timeout: 10000 })
|
|
|
|
test("returns clear error when no model can be resolved", async () => {
|
|
// given - custom category with no model, no systemDefaultModel, no available models
|
|
const { createDelegateTask } = require("./tools")
|
|
|
|
const mockManager = { launch: async () => ({ id: "task-123" }) }
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({}) }, // No model configured
|
|
model: { list: async () => [] }, // No available models
|
|
session: {
|
|
create: async () => ({ data: { id: "test-session" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
},
|
|
}
|
|
|
|
// Custom category with no model defined
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
userCategories: {
|
|
"custom-no-model": { temperature: 0.5 }, // No model field
|
|
},
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when delegating with a custom category that has no model
|
|
const result = await tool.execute(
|
|
{
|
|
description: "Test task",
|
|
prompt: "Do something",
|
|
category: "custom-no-model",
|
|
run_in_background: true,
|
|
load_skills: [],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then returns clear error message with configuration guidance
|
|
expect(result).toContain("Model not configured")
|
|
expect(result).toContain("custom-no-model")
|
|
expect(result).toContain("Configure in one of")
|
|
})
|
|
})
|
|
|
|
describe("background metadata sessionId", () => {
|
|
test("should wait for background sessionId and set metadata for TUI toolcall counting", async () => {
|
|
//#given - manager.launch returns before sessionID is available
|
|
const { createDelegateTask } = require("./tools")
|
|
|
|
const tasks = new Map<string, { id: string; sessionID?: string; status: string; description: string; agent: string }>()
|
|
const mockManager = {
|
|
getTask: (id: string) => tasks.get(id),
|
|
launch: async () => {
|
|
const task = { id: "bg_1", status: "pending", description: "Test task", agent: "explore" }
|
|
tasks.set(task.id, task)
|
|
setTimeout(() => {
|
|
tasks.set(task.id, { ...task, status: "running", sessionId: "ses_child" })
|
|
}, 20)
|
|
return task
|
|
},
|
|
}
|
|
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [{ name: "explore", mode: "subagent" }] }) },
|
|
config: { get: async () => ({}) },
|
|
provider: { list: async () => ({ data: { connected: ["openai"] } }) },
|
|
model: { list: async () => ({ data: [{ provider: "openai", id: "gpt-5.3-codex" }] }) },
|
|
session: {
|
|
create: async () => ({ data: { id: "test-session" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
status: async () => ({ data: {} }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
connectedProvidersOverride: TEST_CONNECTED_PROVIDERS,
|
|
availableModelsOverride: createTestAvailableModels(),
|
|
})
|
|
|
|
const metadataCalls: Array<{ title?: string; metadata?: Record<string, unknown> }> = []
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
metadata: (input: { title?: string; metadata?: Record<string, unknown> }) => {
|
|
metadataCalls.push(input)
|
|
},
|
|
}
|
|
|
|
const args = {
|
|
description: "Explore task",
|
|
prompt: "Explore features directory deeply",
|
|
subagent_type: "explore",
|
|
run_in_background: true,
|
|
load_skills: [],
|
|
}
|
|
|
|
//#when
|
|
const result = await tool.execute(args, toolContext)
|
|
|
|
//#then - metadata should include sessionId (camelCase) once it's available
|
|
expect(String(result)).toContain("Background task launched")
|
|
const sessionIdCall = metadataCalls.find((c) => c.metadata?.sessionId === "ses_child")
|
|
expect(sessionIdCall).toBeDefined()
|
|
})
|
|
})
|
|
|
|
describe("resolveCategoryConfig", () => {
|
|
test("returns null for unknown category without user config", () => {
|
|
// given
|
|
const categoryName = "unknown-category"
|
|
|
|
// when
|
|
const result = resolveCategoryConfig(categoryName, { systemDefaultModel: SYSTEM_DEFAULT_MODEL })
|
|
|
|
// then
|
|
expect(result).toBeNull()
|
|
})
|
|
|
|
test("allows artistry to use its fallback chain when gemini is missing", () => {
|
|
// given - artistry can fall back from gemini to another capable model
|
|
const categoryName = "artistry"
|
|
const availableModels = new Set<string>(["anthropic/claude-opus-4-7"])
|
|
|
|
// when
|
|
const result = resolveCategoryConfig(categoryName, {
|
|
systemDefaultModel: SYSTEM_DEFAULT_MODEL,
|
|
availableModels,
|
|
})
|
|
|
|
// then
|
|
expect(result).not.toBeNull()
|
|
expect(result?.model).toBe("google/gemini-3.1-pro")
|
|
})
|
|
|
|
test("allows artistry when availability is empty", () => {
|
|
// given - empty availability should not disable fallback-capable categories
|
|
const categoryName = "artistry"
|
|
const availableModels = new Set<string>()
|
|
|
|
// when
|
|
const result = resolveCategoryConfig(categoryName, {
|
|
systemDefaultModel: SYSTEM_DEFAULT_MODEL,
|
|
availableModels,
|
|
})
|
|
|
|
// then
|
|
expect(result).not.toBeNull()
|
|
expect(result?.model).toBe("google/gemini-3.1-pro")
|
|
})
|
|
|
|
test("bypasses requiresModel when explicit user config provided", () => {
|
|
// #given
|
|
const categoryName = "deep"
|
|
const availableModels = new Set<string>(["anthropic/claude-opus-4-7"])
|
|
const userCategories = {
|
|
deep: { model: "anthropic/claude-opus-4-7" },
|
|
}
|
|
|
|
// #when
|
|
const result = resolveCategoryConfig(categoryName, {
|
|
systemDefaultModel: SYSTEM_DEFAULT_MODEL,
|
|
availableModels,
|
|
userCategories,
|
|
})
|
|
|
|
// #then
|
|
expect(result).not.toBeNull()
|
|
expect(result!.config.model).toBe("anthropic/claude-opus-4-7")
|
|
})
|
|
|
|
test("bypasses requiresModel when explicit user config provided even with empty availability", () => {
|
|
// #given
|
|
const categoryName = "deep"
|
|
const availableModels = new Set<string>()
|
|
const userCategories = {
|
|
deep: { model: "anthropic/claude-opus-4-7" },
|
|
}
|
|
|
|
// #when
|
|
const result = resolveCategoryConfig(categoryName, {
|
|
systemDefaultModel: SYSTEM_DEFAULT_MODEL,
|
|
availableModels,
|
|
userCategories,
|
|
})
|
|
|
|
// #then
|
|
expect(result).not.toBeNull()
|
|
expect(result!.config.model).toBe("anthropic/claude-opus-4-7")
|
|
})
|
|
|
|
test("returns default model from DEFAULT_CATEGORIES for builtin category", () => {
|
|
// given
|
|
const categoryName = "visual-engineering"
|
|
|
|
// when
|
|
const result = resolveCategoryConfig(categoryName, { systemDefaultModel: SYSTEM_DEFAULT_MODEL })
|
|
|
|
// then
|
|
expect(result).not.toBeNull()
|
|
expect(result!.config.model).toBe("google/gemini-3.1-pro")
|
|
expect(result!.promptAppend).toContain("VISUAL/UI")
|
|
})
|
|
|
|
test("user config overrides systemDefaultModel", () => {
|
|
// given
|
|
const categoryName = "visual-engineering"
|
|
const userCategories = {
|
|
"visual-engineering": { model: "anthropic/claude-opus-4-7" },
|
|
}
|
|
|
|
// when
|
|
const result = resolveCategoryConfig(categoryName, { userCategories, systemDefaultModel: SYSTEM_DEFAULT_MODEL })
|
|
|
|
// then
|
|
expect(result).not.toBeNull()
|
|
expect(result!.config.model).toBe("anthropic/claude-opus-4-7")
|
|
})
|
|
|
|
test("user prompt_append is appended to default", () => {
|
|
// given
|
|
const categoryName = "visual-engineering"
|
|
const userCategories = {
|
|
"visual-engineering": {
|
|
model: "google/gemini-3.1-pro",
|
|
prompt_append: "Custom instructions here",
|
|
},
|
|
}
|
|
|
|
// when
|
|
const result = resolveCategoryConfig(categoryName, { userCategories, systemDefaultModel: SYSTEM_DEFAULT_MODEL })
|
|
|
|
// then
|
|
expect(result).not.toBeNull()
|
|
expect(result!.promptAppend).toContain("VISUAL/UI")
|
|
expect(result!.promptAppend).toContain("Custom instructions here")
|
|
})
|
|
|
|
test("user can define custom category", () => {
|
|
// given
|
|
const categoryName = "my-custom"
|
|
const userCategories = {
|
|
"my-custom": {
|
|
model: "openai/gpt-5.5",
|
|
temperature: 0.5,
|
|
prompt_append: "You are a custom agent",
|
|
},
|
|
}
|
|
|
|
// when
|
|
const result = resolveCategoryConfig(categoryName, { userCategories, systemDefaultModel: SYSTEM_DEFAULT_MODEL })
|
|
|
|
// then
|
|
expect(result).not.toBeNull()
|
|
expect(result!.config.model).toBe("openai/gpt-5.5")
|
|
expect(result!.config.temperature).toBe(0.5)
|
|
expect(result!.promptAppend).toBe("You are a custom agent")
|
|
})
|
|
|
|
test("user category overrides temperature", () => {
|
|
// given
|
|
const categoryName = "visual-engineering"
|
|
const userCategories = {
|
|
"visual-engineering": {
|
|
model: "google/gemini-3.1-pro",
|
|
temperature: 0.3,
|
|
},
|
|
}
|
|
|
|
// when
|
|
const result = resolveCategoryConfig(categoryName, { userCategories, systemDefaultModel: SYSTEM_DEFAULT_MODEL })
|
|
|
|
// then
|
|
expect(result).not.toBeNull()
|
|
expect(result!.config.temperature).toBe(0.3)
|
|
})
|
|
|
|
test("category built-in model takes precedence over inheritedModel", () => {
|
|
// given - builtin category with its own model, parent model also provided
|
|
const categoryName = "visual-engineering"
|
|
const inheritedModel = "cliproxy/claude-opus-4-7"
|
|
|
|
// when
|
|
const result = resolveCategoryConfig(categoryName, { inheritedModel, systemDefaultModel: SYSTEM_DEFAULT_MODEL })
|
|
|
|
// then - category's built-in model wins over inheritedModel
|
|
expect(result).not.toBeNull()
|
|
expect(result!.config.model).toBe("google/gemini-3.1-pro")
|
|
})
|
|
|
|
test("systemDefaultModel is used as fallback when custom category has no model", () => {
|
|
// given - custom category with no model defined
|
|
const categoryName = "my-custom-no-model"
|
|
const userCategories: Record<string, CategoryConfig> = { "my-custom-no-model": { temperature: 0.5 } }
|
|
const inheritedModel = "cliproxy/claude-opus-4-7"
|
|
|
|
// when
|
|
const result = resolveCategoryConfig(categoryName, { userCategories, inheritedModel, systemDefaultModel: SYSTEM_DEFAULT_MODEL })
|
|
|
|
// then - systemDefaultModel is used since custom category has no built-in model
|
|
expect(result).not.toBeNull()
|
|
expect(result!.config.model).toBe(SYSTEM_DEFAULT_MODEL)
|
|
})
|
|
|
|
test("user model takes precedence over inheritedModel", () => {
|
|
// given
|
|
const categoryName = "visual-engineering"
|
|
const userCategories = {
|
|
"visual-engineering": { model: "my-provider/my-model" },
|
|
}
|
|
const inheritedModel = "cliproxy/claude-opus-4-7"
|
|
|
|
// when
|
|
const result = resolveCategoryConfig(categoryName, { userCategories, inheritedModel, systemDefaultModel: SYSTEM_DEFAULT_MODEL })
|
|
|
|
// then
|
|
expect(result).not.toBeNull()
|
|
expect(result!.config.model).toBe("my-provider/my-model")
|
|
})
|
|
|
|
test("default model from category config is used when no user model and no inheritedModel", () => {
|
|
// given
|
|
const categoryName = "visual-engineering"
|
|
|
|
// when
|
|
const result = resolveCategoryConfig(categoryName, { systemDefaultModel: SYSTEM_DEFAULT_MODEL })
|
|
|
|
// then
|
|
expect(result).not.toBeNull()
|
|
expect(result!.config.model).toBe("google/gemini-3.1-pro")
|
|
})
|
|
})
|
|
|
|
describe("category variant", () => {
|
|
test("passes variant to background model payload", async () => {
|
|
// given
|
|
const { createDelegateTask } = require("./tools")
|
|
let launchInput: any
|
|
|
|
const mockManager = {
|
|
launch: async (input: any) => {
|
|
launchInput = input
|
|
return {
|
|
id: "task-variant",
|
|
sessionID: "session-variant",
|
|
description: "Variant task",
|
|
agent: "sisyphus-junior",
|
|
status: "running",
|
|
}
|
|
},
|
|
}
|
|
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
create: async () => ({ data: { id: "test-session" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
userCategories: {
|
|
ultrabrain: { model: "openai/gpt-5.5", variant: "xhigh" },
|
|
},
|
|
connectedProvidersOverride: TEST_CONNECTED_PROVIDERS,
|
|
availableModelsOverride: createTestAvailableModels(),
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when
|
|
await tool.execute(
|
|
{
|
|
description: "Variant task",
|
|
prompt: "Do something",
|
|
category: "ultrabrain",
|
|
run_in_background: true,
|
|
load_skills: ["git-master"],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then
|
|
expect(launchInput.model).toEqual({
|
|
providerID: "openai",
|
|
modelID: "gpt-5.5",
|
|
variant: "xhigh",
|
|
})
|
|
})
|
|
|
|
test("DEFAULT_CATEGORIES explicit high model passes to background WITHOUT userCategories", async () => {
|
|
// given - NO userCategories, testing DEFAULT_CATEGORIES only
|
|
const { createDelegateTask } = require("./tools")
|
|
let launchInput: any
|
|
|
|
const mockManager = {
|
|
launch: async (input: any) => {
|
|
launchInput = input
|
|
return {
|
|
id: "task-default-variant",
|
|
sessionID: "session-default-variant",
|
|
description: "Default variant task",
|
|
agent: "sisyphus-junior",
|
|
status: "running",
|
|
}
|
|
},
|
|
}
|
|
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
model: { list: async () => [{ provider: "anthropic", id: "claude-opus-4-7" }] },
|
|
session: {
|
|
create: async () => ({ data: { id: "test-session" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
},
|
|
}
|
|
|
|
// NO userCategories - must use DEFAULT_CATEGORIES
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
connectedProvidersOverride: TEST_CONNECTED_PROVIDERS,
|
|
availableModelsOverride: createTestAvailableModels(),
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - unspecified-high uses claude-opus-4-7 max in DEFAULT_CATEGORIES
|
|
await tool.execute(
|
|
{
|
|
description: "Test unspecified-high default variant",
|
|
prompt: "Do something",
|
|
category: "unspecified-high",
|
|
run_in_background: true,
|
|
load_skills: ["git-master"],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - claude-opus-4-7 should be passed with max variant
|
|
expect(launchInput.model).toEqual({
|
|
providerID: "anthropic",
|
|
modelID: "claude-opus-4-7",
|
|
variant: "max",
|
|
})
|
|
}, { timeout: 20000 })
|
|
|
|
test("DEFAULT_CATEGORIES explicit high model passes to sync session.prompt WITHOUT userCategories", async () => {
|
|
// given - NO userCategories, testing DEFAULT_CATEGORIES for sync mode
|
|
const { createDelegateTask } = require("./tools")
|
|
let promptBody: any
|
|
|
|
const mockManager = { launch: async () => ({}) }
|
|
|
|
const promptMock = async (input: any) => {
|
|
promptBody = input.body
|
|
return { data: {} }
|
|
}
|
|
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
model: { list: async () => [{ provider: "anthropic", id: "claude-opus-4-7" }] },
|
|
session: {
|
|
get: async () => ({ data: { directory: "/project" } }),
|
|
create: async () => ({ data: { id: "ses_sync_default_variant" } }),
|
|
prompt: promptMock,
|
|
promptAsync: promptMock,
|
|
messages: async () => ({
|
|
data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "done" }] }]
|
|
}),
|
|
status: async () => ({ data: { "ses_sync_default_variant": { type: "idle" } } }),
|
|
},
|
|
}
|
|
|
|
// NO userCategories - must use DEFAULT_CATEGORIES
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - unspecified-high uses claude-opus-4-7 max in DEFAULT_CATEGORIES
|
|
await tool.execute(
|
|
{
|
|
description: "Test unspecified-high sync variant",
|
|
prompt: "Do something",
|
|
category: "unspecified-high",
|
|
run_in_background: false,
|
|
load_skills: ["git-master"],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - claude-opus-4-7 should be passed with max variant
|
|
expect(promptBody.model).toEqual({
|
|
providerID: "anthropic",
|
|
modelID: "claude-opus-4-7",
|
|
})
|
|
expect(promptBody.variant).toBe("max")
|
|
}, { timeout: 20000 })
|
|
})
|
|
|
|
describe("skills parameter", () => {
|
|
test("skills parameter is required - throws error when not provided", async () => {
|
|
// given
|
|
const { createDelegateTask } = require("./tools")
|
|
|
|
const mockManager = { launch: async () => ({}) }
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
create: async () => ({ data: { id: "test-session" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - skills not provided (undefined)
|
|
// then - should throw error about missing skills
|
|
await expect(tool.execute(
|
|
{
|
|
description: "Test task",
|
|
prompt: "Do something",
|
|
category: "ultrabrain",
|
|
run_in_background: false,
|
|
},
|
|
toolContext
|
|
)).rejects.toThrow("Invalid arguments: 'load_skills' parameter is REQUIRED")
|
|
})
|
|
|
|
test("null skills throws error", async () => {
|
|
// given
|
|
const { createDelegateTask } = require("./tools")
|
|
|
|
const mockManager = { launch: async () => ({}) }
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
create: async () => ({ data: { id: "test-session" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - null passed
|
|
// then - should throw error about null
|
|
await expect(tool.execute(
|
|
{
|
|
description: "Test task",
|
|
prompt: "Do something",
|
|
category: "ultrabrain",
|
|
run_in_background: false,
|
|
load_skills: null,
|
|
},
|
|
toolContext
|
|
)).rejects.toThrow("Invalid arguments: load_skills=null is not allowed")
|
|
})
|
|
|
|
test("empty array [] is allowed and proceeds without skill content", async () => {
|
|
// given
|
|
const { createDelegateTask } = require("./tools")
|
|
let promptBody: any
|
|
|
|
const mockManager = { launch: async () => ({}) }
|
|
|
|
const promptMock = async (input: any) => {
|
|
promptBody = input.body
|
|
return { data: {} }
|
|
}
|
|
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
get: async () => ({ data: { directory: "/project" } }),
|
|
create: async () => ({ data: { id: "test-session" } }),
|
|
prompt: promptMock,
|
|
promptAsync: promptMock,
|
|
messages: async () => ({
|
|
data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Done" }] }]
|
|
}),
|
|
status: async () => ({ data: {} }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - empty array passed
|
|
await tool.execute(
|
|
{
|
|
description: "Test task",
|
|
prompt: "Do something",
|
|
category: "ultrabrain",
|
|
run_in_background: false,
|
|
load_skills: [],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - should proceed without system content from skills
|
|
expect(promptBody).toBeDefined()
|
|
}, { timeout: 20000 })
|
|
})
|
|
|
|
describe("run_in_background parameter", () => {
|
|
test("#given category without run_in_background #when executing #then throws required parameter error", async () => {
|
|
// given
|
|
const { createDelegateTask } = require("./tools")
|
|
const mockManager = { launch: async () => ({}) }
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
create: async () => ({ data: { id: "test-session" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
},
|
|
}
|
|
const tool = createDelegateTask({ manager: mockManager, client: mockClient })
|
|
|
|
// when
|
|
// then
|
|
await expect(tool.execute(
|
|
{
|
|
description: "Category without run flag",
|
|
prompt: "Do something",
|
|
category: "quick",
|
|
load_skills: [],
|
|
},
|
|
{ sessionID: "parent-session", messageID: "parent-message", agent: "sisyphus", abort: new AbortController().signal }
|
|
)).rejects.toThrow("Invalid arguments: 'run_in_background' parameter is REQUIRED")
|
|
})
|
|
|
|
test("#given subagent_type without run_in_background #when executing #then throws required parameter error", async () => {
|
|
// given
|
|
const { createDelegateTask } = require("./tools")
|
|
const mockManager = { launch: async () => ({}) }
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [{ name: "explore", mode: "subagent" }] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
create: async () => ({ data: { id: "test-session" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
},
|
|
}
|
|
const tool = createDelegateTask({ manager: mockManager, client: mockClient })
|
|
|
|
// when
|
|
// then
|
|
await expect(tool.execute(
|
|
{
|
|
description: "Subagent without run flag",
|
|
prompt: "Find patterns",
|
|
subagent_type: "explore",
|
|
load_skills: [],
|
|
},
|
|
{ sessionID: "parent-session", messageID: "parent-message", agent: "sisyphus", abort: new AbortController().signal }
|
|
)).rejects.toThrow("Invalid arguments: 'run_in_background' parameter is REQUIRED")
|
|
})
|
|
|
|
test("#given task_id without run_in_background #when executing #then throws required parameter error", async () => {
|
|
// given
|
|
const { createDelegateTask } = require("./tools")
|
|
const mockManager = { resume: async () => ({ id: "task-1", sessionId: "ses_1", status: "running" }) }
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
create: async () => ({ data: { id: "test-session" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
},
|
|
}
|
|
const tool = createDelegateTask({ manager: mockManager, client: mockClient })
|
|
|
|
// when
|
|
// then
|
|
await expect(tool.execute(
|
|
{
|
|
description: "Continue without run flag",
|
|
prompt: "Continue",
|
|
task_id: "ses_existing",
|
|
load_skills: [],
|
|
},
|
|
{ sessionID: "parent-session", messageID: "parent-message", agent: "sisyphus", abort: new AbortController().signal }
|
|
)).rejects.toThrow("Invalid arguments: 'run_in_background' parameter is REQUIRED")
|
|
})
|
|
|
|
test("#given no category no subagent_type no task_id and no run_in_background #when executing #then throws required parameter error", async () => {
|
|
// given
|
|
const { createDelegateTask } = require("./tools")
|
|
const mockManager = { launch: async () => ({}) }
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
create: async () => ({ data: { id: "test-session" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
},
|
|
}
|
|
const tool = createDelegateTask({ manager: mockManager, client: mockClient })
|
|
|
|
// when
|
|
// then
|
|
await expect(tool.execute(
|
|
{
|
|
description: "Missing required args",
|
|
prompt: "Do something",
|
|
load_skills: [],
|
|
},
|
|
{ sessionID: "parent-session", messageID: "parent-message", agent: "sisyphus", abort: new AbortController().signal }
|
|
)).rejects.toThrow("Invalid arguments: 'run_in_background' parameter is REQUIRED")
|
|
})
|
|
|
|
test("#given category without description #when executing #then auto-generates description from prompt", async () => {
|
|
// given
|
|
const { createDelegateTask } = require("./tools")
|
|
let capturedTitle: string | undefined
|
|
const mockManager = { launch: async () => ({}) }
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
create: async () => ({ data: { id: "test-session" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
},
|
|
}
|
|
const tool = createDelegateTask({ manager: mockManager, client: mockClient })
|
|
|
|
// when
|
|
try {
|
|
await tool.execute(
|
|
{
|
|
prompt: "Fix the broken unit tests in parser module",
|
|
category: "quick",
|
|
run_in_background: false,
|
|
load_skills: [],
|
|
},
|
|
{
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
metadata: async (meta: { title?: string }) => { capturedTitle = meta.title },
|
|
}
|
|
)
|
|
} catch {
|
|
// execution may fail due to incomplete mocks — we only care about the title
|
|
}
|
|
|
|
// then — description auto-generated from first 4 words of prompt
|
|
expect(capturedTitle).toBe("Fix the broken unit")
|
|
})
|
|
|
|
test("#given empty description #when executing #then auto-generates description from prompt", async () => {
|
|
// given
|
|
const { createDelegateTask } = require("./tools")
|
|
let capturedTitle: string | undefined
|
|
const mockManager = { launch: async () => ({}) }
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
create: async () => ({ data: { id: "test-session" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
},
|
|
}
|
|
const tool = createDelegateTask({ manager: mockManager, client: mockClient })
|
|
|
|
// when
|
|
try {
|
|
await tool.execute(
|
|
{
|
|
description: " ",
|
|
prompt: "Refactor authentication module completely",
|
|
category: "quick",
|
|
run_in_background: false,
|
|
load_skills: [],
|
|
},
|
|
{
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
metadata: async (meta: { title?: string }) => { capturedTitle = meta.title },
|
|
}
|
|
)
|
|
} catch {
|
|
// execution may fail due to incomplete mocks
|
|
}
|
|
|
|
// then
|
|
expect(capturedTitle).toBe("Refactor authentication module completely")
|
|
})
|
|
|
|
test("#given explicit description #when executing #then preserves provided description", async () => {
|
|
// given
|
|
const { createDelegateTask } = require("./tools")
|
|
let capturedTitle: string | undefined
|
|
const mockManager = { launch: async () => ({}) }
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
create: async () => ({ data: { id: "test-session" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
},
|
|
}
|
|
const tool = createDelegateTask({ manager: mockManager, client: mockClient })
|
|
|
|
// when
|
|
try {
|
|
await tool.execute(
|
|
{
|
|
description: "My custom task name",
|
|
prompt: "Do something else entirely",
|
|
category: "quick",
|
|
run_in_background: false,
|
|
load_skills: [],
|
|
},
|
|
{
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
metadata: async (meta: { title?: string }) => { capturedTitle = meta.title },
|
|
}
|
|
)
|
|
} catch {
|
|
// execution may fail due to incomplete mocks
|
|
}
|
|
|
|
// then — explicit description preserved
|
|
expect(capturedTitle).toBe("My custom task name")
|
|
})
|
|
|
|
test("#given explicit run_in_background=false #when executing #then sync execution succeeds", async () => {
|
|
// given
|
|
const { createDelegateTask } = require("./tools")
|
|
let promptCalled = false
|
|
const mockManager = { launch: async () => ({}) }
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [{ name: "oracle", mode: "subagent", model: { providerID: "anthropic", modelID: "claude-opus-4-7" } }] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
get: async () => ({ data: { directory: "/project" } }),
|
|
create: async () => ({ data: { id: "ses_explicit_false" } }),
|
|
prompt: async () => {
|
|
promptCalled = true
|
|
return { data: {} }
|
|
},
|
|
promptAsync: async () => {
|
|
promptCalled = true
|
|
return { data: {} }
|
|
},
|
|
messages: async () => ({ data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Done" }] }] }),
|
|
status: async () => ({ data: { ses_explicit_false: { type: "idle" } } }),
|
|
},
|
|
}
|
|
const tool = createDelegateTask({ manager: mockManager, client: mockClient })
|
|
|
|
// when
|
|
const result = await tool.execute(
|
|
{
|
|
description: "Explicit false",
|
|
prompt: "Run sync",
|
|
subagent_type: "oracle",
|
|
run_in_background: false,
|
|
load_skills: [],
|
|
},
|
|
{ sessionID: "parent-session", messageID: "parent-message", agent: "sisyphus", abort: new AbortController().signal }
|
|
)
|
|
|
|
// then
|
|
expect(promptCalled).toBe(true)
|
|
expect(result).toContain("Done")
|
|
}, { timeout: 10000 })
|
|
|
|
test("#given explicit run_in_background=true #when executing #then background execution succeeds", async () => {
|
|
// given
|
|
const { createDelegateTask } = require("./tools")
|
|
let launchCalled = false
|
|
const mockManager = {
|
|
launch: async () => {
|
|
launchCalled = true
|
|
return {
|
|
id: "bg_explicit_true",
|
|
sessionId: "ses_bg_explicit_true",
|
|
description: "Explicit true",
|
|
agent: "Sisyphus-Junior",
|
|
status: "running",
|
|
}
|
|
},
|
|
}
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
model: { list: async () => [] },
|
|
session: {
|
|
create: async () => ({ data: { id: "ses_bg_explicit_true" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
},
|
|
}
|
|
const tool = createDelegateTask({ manager: mockManager, client: mockClient })
|
|
|
|
// when
|
|
const result = await tool.execute(
|
|
{
|
|
description: "Explicit true",
|
|
prompt: "Run background",
|
|
category: "quick",
|
|
run_in_background: true,
|
|
load_skills: [],
|
|
},
|
|
{ sessionID: "parent-session", messageID: "parent-message", agent: "sisyphus", abort: new AbortController().signal }
|
|
)
|
|
|
|
// then
|
|
expect(launchCalled).toBe(true)
|
|
expect(result).toContain("Background task launched")
|
|
}, { timeout: 10000 })
|
|
|
|
test("#given concurrent background launches from the same parent #when one parent call aborts during session wait #then sibling launch is not interrupted", async () => {
|
|
// given
|
|
const { createDelegateTask } = require("./tools")
|
|
const firstAbortController = new AbortController()
|
|
const secondAbortController = new AbortController()
|
|
const taskStates = new Map([
|
|
["bg_tool_first", { reads: 0, abortOnFirstRead: true, sessionId: "ses_tool_first" }],
|
|
["bg_tool_second", { reads: 0, abortOnFirstRead: false, sessionId: "ses_tool_second" }],
|
|
])
|
|
let launchCount = 0
|
|
const mockManager = {
|
|
launch: async () => {
|
|
launchCount += 1
|
|
return launchCount === 1
|
|
? {
|
|
id: "bg_tool_first",
|
|
sessionId: undefined,
|
|
description: "Tool first",
|
|
agent: "Sisyphus-Junior",
|
|
status: "running",
|
|
}
|
|
: {
|
|
id: "bg_tool_second",
|
|
sessionId: undefined,
|
|
description: "Tool second",
|
|
agent: "Sisyphus-Junior",
|
|
status: "running",
|
|
}
|
|
},
|
|
getTask: (taskID: string) => {
|
|
const state = taskStates.get(taskID)
|
|
if (!state) return undefined
|
|
state.reads += 1
|
|
if (state.abortOnFirstRead && state.reads === 1) {
|
|
firstAbortController.abort()
|
|
}
|
|
return state.reads >= 2
|
|
? { sessionId: state.sessionId, status: "running" }
|
|
: { sessionId: undefined, status: "pending" }
|
|
},
|
|
}
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
model: { list: async () => [] },
|
|
session: {
|
|
create: async () => ({ data: { id: "ses_bg_explicit_true" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
},
|
|
}
|
|
const tool = createDelegateTask({ manager: mockManager, client: mockClient })
|
|
|
|
// when
|
|
const [firstResult, secondResult] = await Promise.all([
|
|
tool.execute(
|
|
{
|
|
description: "Tool first",
|
|
prompt: "Run background",
|
|
category: "quick",
|
|
run_in_background: true,
|
|
load_skills: [],
|
|
},
|
|
{ sessionID: "parent-session", messageID: "parent-message-1", agent: "sisyphus", abort: firstAbortController.signal }
|
|
),
|
|
tool.execute(
|
|
{
|
|
description: "Tool second",
|
|
prompt: "Run background",
|
|
category: "quick",
|
|
run_in_background: true,
|
|
load_skills: [],
|
|
},
|
|
{ sessionID: "parent-session", messageID: "parent-message-2", agent: "sisyphus", abort: secondAbortController.signal }
|
|
),
|
|
])
|
|
|
|
// then
|
|
expect(firstResult).toContain("Background task launched")
|
|
expect(firstResult).not.toContain("Task failed to start")
|
|
expect(secondResult).toContain("Background task launched")
|
|
expect(secondResult).toContain("session_id: ses_tool_second")
|
|
expect(secondResult).not.toContain("interrupt")
|
|
}, { timeout: 10000 })
|
|
})
|
|
|
|
describe("task_id with background parameter", () => {
|
|
test("task_id with background=false should wait for result and return content", async () => {
|
|
// Note: This test needs extended timeout because the implementation has MIN_STABILITY_TIME_MS = 5000
|
|
// given
|
|
const { createDelegateTask } = require("./tools")
|
|
|
|
const mockTask = {
|
|
id: "task-123",
|
|
sessionId: "ses_continue_test",
|
|
description: "Continued task",
|
|
agent: "explore",
|
|
status: "running",
|
|
}
|
|
|
|
const mockManager = {
|
|
resume: async () => mockTask,
|
|
launch: async () => mockTask,
|
|
}
|
|
|
|
let messagesCallCount = 0
|
|
|
|
const mockClient = {
|
|
session: {
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async (args?: { path?: { id?: string } }) => {
|
|
const sessionID = args?.path?.id
|
|
// Only track calls for the target session (ses_continue_test),
|
|
// not for parent-session calls from resolveParentContext
|
|
if (sessionID !== "ses_continue_test") {
|
|
return { data: [] }
|
|
}
|
|
messagesCallCount++
|
|
const now = Date.now()
|
|
|
|
const beforeContinuation = [
|
|
{
|
|
info: { id: "msg_001", role: "user", time: { created: now } },
|
|
parts: [{ type: "text", text: "Previous context" }],
|
|
},
|
|
{
|
|
info: { id: "msg_002", role: "assistant", time: { created: now + 1 }, finish: "end_turn" },
|
|
parts: [{ type: "text", text: "Previous result" }],
|
|
},
|
|
]
|
|
|
|
if (messagesCallCount === 1) {
|
|
return { data: beforeContinuation }
|
|
}
|
|
|
|
return {
|
|
data: [
|
|
...beforeContinuation,
|
|
{
|
|
info: { id: "msg_003", role: "user", time: { created: now + 2 } },
|
|
parts: [{ type: "text", text: "Continue the task" }],
|
|
},
|
|
{
|
|
info: { id: "msg_004", role: "assistant", time: { created: now + 3 }, finish: "end_turn" },
|
|
parts: [{ type: "text", text: "This is the continued task result" }],
|
|
},
|
|
],
|
|
}
|
|
},
|
|
status: async () => ({ data: { "ses_continue_test": { type: "idle" } } }),
|
|
},
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
app: {
|
|
agents: async () => ({ data: [] }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when
|
|
const result = await tool.execute(
|
|
{
|
|
description: "Continue test",
|
|
prompt: "Continue the task",
|
|
task_id: "ses_continue_test",
|
|
run_in_background: false,
|
|
load_skills: ["git-master"],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - should contain actual result, not just "Background task continued"
|
|
expect(result).toContain("This is the continued task result")
|
|
expect(result).not.toContain("Background task continued")
|
|
}, { timeout: 10000 })
|
|
|
|
test("sync continuation preserves variant from previous session message", async () => {
|
|
//#given a session with a previous message that has variant "max"
|
|
const { createDelegateTask } = require("./tools")
|
|
|
|
const promptMock = mock(async () => {
|
|
return { data: {} }
|
|
})
|
|
|
|
const baseTime = Date.now()
|
|
const initialMessages = [
|
|
{
|
|
info: {
|
|
id: "msg_001",
|
|
role: "user",
|
|
agent: "sisyphus-junior",
|
|
model: { providerID: "anthropic", modelID: "claude-opus-4-7" },
|
|
variant: "max",
|
|
time: { created: baseTime },
|
|
},
|
|
parts: [{ type: "text", text: "previous message" }],
|
|
},
|
|
{
|
|
info: { id: "msg_002", role: "assistant", time: { created: baseTime + 1 }, finish: "end_turn" },
|
|
parts: [{ type: "text", text: "Completed." }],
|
|
},
|
|
]
|
|
|
|
const messagesCallCounts: Record<string, number> = {}
|
|
|
|
const mockClient = {
|
|
session: {
|
|
prompt: promptMock,
|
|
promptAsync: promptMock,
|
|
messages: async (input: any) => {
|
|
const sessionID = input?.path?.id
|
|
if (typeof sessionID !== "string") {
|
|
return { data: [] }
|
|
}
|
|
|
|
const callCount = (messagesCallCounts[sessionID] ?? 0) + 1
|
|
messagesCallCounts[sessionID] = callCount
|
|
|
|
if (sessionID !== "ses_var_test") {
|
|
return { data: [] }
|
|
}
|
|
|
|
if (callCount === 1) {
|
|
return { data: initialMessages }
|
|
}
|
|
|
|
return {
|
|
data: [
|
|
...initialMessages,
|
|
{
|
|
info: { id: "msg_003", role: "assistant", time: { created: baseTime + 2 }, finish: "end_turn" },
|
|
parts: [{ type: "text", text: "Continued." }],
|
|
},
|
|
],
|
|
}
|
|
},
|
|
status: async () => ({ data: { "ses_var_test": { type: "idle" } } }),
|
|
},
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
app: {
|
|
agents: async () => ({ data: [] }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: { resume: async () => ({ id: "task-var", sessionId: "ses_var_test", description: "Variant test", agent: "sisyphus-junior", status: "running" }) },
|
|
client: mockClient,
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
//#when continuing the session
|
|
await tool.execute(
|
|
{
|
|
description: "Continue with variant",
|
|
prompt: "Continue the task",
|
|
task_id: "ses_var_test",
|
|
run_in_background: false,
|
|
load_skills: [],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
//#then prompt should include variant from previous message
|
|
expect(promptMock).toHaveBeenCalled()
|
|
const callArgs = promptMock.mock.calls[0][0]
|
|
expect(callArgs.body.variant).toBe("max")
|
|
expect(callArgs.body.agent).toBe("sisyphus-junior")
|
|
expect(callArgs.body.model).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-7" })
|
|
}, { timeout: 10000 })
|
|
|
|
test("task_id with background=true should return immediately without waiting", async () => {
|
|
// given
|
|
const { createDelegateTask } = require("./tools")
|
|
|
|
const mockTask = {
|
|
id: "task-456",
|
|
sessionId: "ses_bg_continue",
|
|
description: "Background continued task",
|
|
agent: "explore",
|
|
status: "running",
|
|
}
|
|
|
|
const mockManager = {
|
|
resume: async () => mockTask,
|
|
}
|
|
|
|
const mockClient = {
|
|
session: {
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({
|
|
data: [],
|
|
}),
|
|
},
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when
|
|
const result = await tool.execute(
|
|
{
|
|
description: "Continue bg test",
|
|
prompt: "Continue in background",
|
|
task_id: "ses_bg_continue",
|
|
run_in_background: true,
|
|
load_skills: ["git-master"],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - should return background message
|
|
expect(result).toContain("Background task continued")
|
|
expect(result).toContain("task-456")
|
|
})
|
|
})
|
|
|
|
describe("sync mode new task (run_in_background=false)", () => {
|
|
test("sync mode prompt error returns error message immediately", async () => {
|
|
// given
|
|
const { createDelegateTask } = require("./tools")
|
|
|
|
const mockManager = {
|
|
launch: async () => ({}),
|
|
}
|
|
|
|
const promptMock = async () => {
|
|
throw new Error("JSON Parse error: Unexpected EOF")
|
|
}
|
|
|
|
const mockClient = {
|
|
session: {
|
|
get: async () => ({ data: { directory: "/project" } }),
|
|
create: async () => ({ data: { id: "ses_sync_error_test" } }),
|
|
prompt: promptMock,
|
|
promptAsync: promptMock,
|
|
messages: async () => ({ data: [] }),
|
|
status: async () => ({ data: {} }),
|
|
},
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
app: {
|
|
agents: async () => ({ data: [{ name: "ultrabrain", mode: "subagent" }] }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when
|
|
const result = await tool.execute(
|
|
{
|
|
description: "Sync error test",
|
|
prompt: "Do something",
|
|
category: "ultrabrain",
|
|
run_in_background: false,
|
|
load_skills: ["git-master"],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - should return detailed error message with args and stack trace
|
|
expect(result).toContain("Send prompt failed")
|
|
expect(result).toContain("JSON Parse error")
|
|
expect(result).toContain("**Arguments**:")
|
|
expect(result).toContain("**Stack Trace**:")
|
|
})
|
|
|
|
test("sync mode success returns task result with content", async () => {
|
|
// given
|
|
const { createDelegateTask } = require("./tools")
|
|
|
|
const mockManager = {
|
|
launch: async () => ({}),
|
|
}
|
|
|
|
const mockClient = {
|
|
session: {
|
|
get: async () => ({ data: { directory: "/project" } }),
|
|
create: async () => ({ data: { id: "ses_sync_success" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({
|
|
data: [
|
|
{
|
|
info: { id: "msg_001", role: "user", time: { created: Date.now() } },
|
|
parts: [{ type: "text", text: "Do something" }],
|
|
},
|
|
{
|
|
info: { id: "msg_002", role: "assistant", time: { created: Date.now() + 1 }, finish: "end_turn" },
|
|
parts: [{ type: "text", text: "Sync task completed successfully" }],
|
|
},
|
|
],
|
|
}),
|
|
status: async () => ({ data: { "ses_sync_success": { type: "idle" } } }),
|
|
},
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
app: {
|
|
agents: async () => ({ data: [{ name: "ultrabrain", mode: "subagent" }] }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when
|
|
const result = await tool.execute(
|
|
{
|
|
description: "Sync success test",
|
|
prompt: "Do something",
|
|
category: "ultrabrain",
|
|
run_in_background: false,
|
|
load_skills: ["git-master"],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - should return the task result content
|
|
expect(result).toContain("Sync task completed successfully")
|
|
expect(result).toContain("Task completed")
|
|
}, { timeout: 20000 })
|
|
|
|
test("sync mode agent not found returns helpful error", async () => {
|
|
// given
|
|
const { createDelegateTask } = require("./tools")
|
|
|
|
const mockManager = {
|
|
launch: async () => ({}),
|
|
}
|
|
|
|
const promptMock = async () => {
|
|
throw new Error("Cannot read property 'name' of undefined agent.name")
|
|
}
|
|
|
|
const mockClient = {
|
|
session: {
|
|
get: async () => ({ data: { directory: "/project" } }),
|
|
create: async () => ({ data: { id: "ses_agent_notfound" } }),
|
|
prompt: promptMock,
|
|
promptAsync: promptMock,
|
|
messages: async () => ({ data: [] }),
|
|
status: async () => ({ data: {} }),
|
|
},
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
app: {
|
|
agents: async () => ({ data: [{ name: "ultrabrain", mode: "subagent" }] }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when
|
|
const result = await tool.execute(
|
|
{
|
|
description: "Agent not found test",
|
|
prompt: "Do something",
|
|
category: "ultrabrain",
|
|
run_in_background: false,
|
|
load_skills: ["git-master"],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - should return agent not found error
|
|
expect(result).toContain("not found")
|
|
expect(result).toContain("registered")
|
|
})
|
|
|
|
test("sync mode passes category model to prompt", async () => {
|
|
// given
|
|
const { createDelegateTask } = require("./tools")
|
|
let promptBody: any
|
|
|
|
const mockManager = { launch: async () => ({}) }
|
|
|
|
const promptMock = async (input: any) => {
|
|
promptBody = input.body
|
|
return { data: {} }
|
|
}
|
|
|
|
const mockClient = {
|
|
session: {
|
|
get: async () => ({ data: { directory: "/project" } }),
|
|
create: async () => ({ data: { id: "ses_sync_model" } }),
|
|
prompt: promptMock,
|
|
promptAsync: promptMock,
|
|
messages: async () => ({
|
|
data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Done" }] }]
|
|
}),
|
|
status: async () => ({ data: {} }),
|
|
},
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
app: { agents: async () => ({ data: [] }) },
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
userCategories: {
|
|
"custom-cat": { model: "provider/custom-model" }
|
|
}
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent",
|
|
messageID: "msg",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal
|
|
}
|
|
|
|
// when
|
|
await tool.execute({
|
|
description: "Sync model test",
|
|
prompt: "test",
|
|
category: "custom-cat",
|
|
run_in_background: false,
|
|
load_skills: ["git-master"]
|
|
}, toolContext)
|
|
|
|
// then
|
|
expect(promptBody.model).toEqual({
|
|
providerID: "provider",
|
|
modelID: "custom-model"
|
|
})
|
|
}, { timeout: 20000 })
|
|
})
|
|
|
|
describe("unstable agent forced background mode", () => {
|
|
test("gemini model with run_in_background=false should force background but wait for result", async () => {
|
|
// given - category using gemini model with run_in_background=false
|
|
const { createDelegateTask } = require("./tools")
|
|
let launchCalled = false
|
|
|
|
const launchedTask = {
|
|
id: "task-unstable",
|
|
sessionId: "ses_unstable_gemini",
|
|
description: "Unstable gemini task",
|
|
agent: "sisyphus-junior",
|
|
status: "running",
|
|
}
|
|
const mockManager = {
|
|
launch: async () => {
|
|
launchCalled = true
|
|
return launchedTask
|
|
},
|
|
getTask: () => launchedTask,
|
|
}
|
|
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
model: { list: async () => [{ provider: "google", id: "gemini-3.1-pro" }] },
|
|
session: {
|
|
get: async () => ({ data: { directory: "/project" } }),
|
|
create: async () => ({ data: { id: "ses_unstable_gemini" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({
|
|
data: [
|
|
{ info: { role: "assistant", time: { created: Date.now() } }, parts: [{ type: "text", text: "Gemini task completed successfully" }] }
|
|
]
|
|
}),
|
|
status: async () => ({ data: { "ses_unstable_gemini": { type: "idle" } } }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - using visual-engineering (gemini model) with run_in_background=false
|
|
const result = await tool.execute(
|
|
{
|
|
description: "Test gemini forced background",
|
|
prompt: "Do something visual",
|
|
category: "visual-engineering",
|
|
run_in_background: false,
|
|
load_skills: ["git-master"],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - should launch as background BUT wait for and return actual result
|
|
expect(launchCalled).toBe(true)
|
|
expect(result).toContain("SUPERVISED TASK COMPLETED")
|
|
expect(result).toContain("Gemini task completed successfully")
|
|
}, { timeout: 20000 })
|
|
|
|
test("gemini model with run_in_background=true should not show unstable message (normal background)", async () => {
|
|
// given - category using gemini model with run_in_background=true (normal background flow)
|
|
const { createDelegateTask } = require("./tools")
|
|
let launchCalled = false
|
|
|
|
const mockManager = {
|
|
launch: async () => {
|
|
launchCalled = true
|
|
return {
|
|
id: "task-normal-bg",
|
|
sessionId: "ses_normal_bg",
|
|
description: "Normal background task",
|
|
agent: "sisyphus-junior",
|
|
status: "running",
|
|
}
|
|
},
|
|
}
|
|
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
create: async () => ({ data: { id: "test-session" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - using visual-engineering with run_in_background=true (normal background)
|
|
const result = await tool.execute(
|
|
{
|
|
description: "Test normal background",
|
|
prompt: "Do something visual",
|
|
category: "visual-engineering",
|
|
run_in_background: true, // User explicitly says true - normal background
|
|
load_skills: ["git-master"],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - should NOT show unstable message (it's normal background flow)
|
|
expect(launchCalled).toBe(true)
|
|
expect(result).not.toContain("UNSTABLE AGENT MODE")
|
|
expect(result).toContain("task-normal-bg")
|
|
})
|
|
|
|
test("minimax model with run_in_background=false should force background but wait for result", async () => {
|
|
// given - custom category using minimax model with run_in_background=false
|
|
const { createDelegateTask } = require("./tools")
|
|
let launchCalled = false
|
|
|
|
const launchedTask = {
|
|
id: "task-unstable-minimax",
|
|
sessionId: "ses_unstable_minimax",
|
|
description: "Unstable minimax task",
|
|
agent: "sisyphus-junior",
|
|
status: "running",
|
|
}
|
|
const mockManager = {
|
|
launch: async () => {
|
|
launchCalled = true
|
|
return launchedTask
|
|
},
|
|
getTask: () => launchedTask,
|
|
}
|
|
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
get: async () => ({ data: { directory: "/project" } }),
|
|
create: async () => ({ data: { id: "ses_unstable_minimax" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({
|
|
data: [
|
|
{ info: { role: "assistant", time: { created: Date.now() } }, parts: [{ type: "text", text: "Minimax task completed successfully" }] }
|
|
]
|
|
}),
|
|
status: async () => ({ data: { "ses_unstable_minimax": { type: "idle" } } }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
userCategories: {
|
|
"minimax-cat": {
|
|
model: "minimax/abab-5",
|
|
},
|
|
},
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - using minimax category with run_in_background=false
|
|
const result = await tool.execute(
|
|
{
|
|
description: "Test minimax forced background",
|
|
prompt: "Do something with minimax",
|
|
category: "minimax-cat",
|
|
run_in_background: false,
|
|
load_skills: ["git-master"],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - should launch as background BUT wait for and return actual result
|
|
expect(launchCalled).toBe(true)
|
|
expect(result).toContain("SUPERVISED TASK COMPLETED")
|
|
expect(result).toContain("Minimax task completed successfully")
|
|
}, { timeout: 20000 })
|
|
|
|
test("non-gemini model with run_in_background=false should run sync (not forced to background)", async () => {
|
|
// given - category using non-gemini model with run_in_background=false
|
|
const { createDelegateTask } = require("./tools")
|
|
let launchCalled = false
|
|
let promptCalled = false
|
|
|
|
const mockManager = {
|
|
launch: async () => {
|
|
launchCalled = true
|
|
return { id: "should-not-be-called", sessionId: "x", description: "x", agent: "x", status: "running" }
|
|
},
|
|
}
|
|
|
|
const promptMock = async () => {
|
|
promptCalled = true
|
|
return { data: {} }
|
|
}
|
|
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
get: async () => ({ data: { directory: "/project" } }),
|
|
create: async () => ({ data: { id: "ses_sync_non_gemini" } }),
|
|
prompt: promptMock,
|
|
promptAsync: promptMock,
|
|
messages: async () => ({
|
|
data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Done sync" }] }]
|
|
}),
|
|
status: async () => ({ data: { "ses_sync_non_gemini": { type: "idle" } } }),
|
|
},
|
|
}
|
|
|
|
// Use ultrabrain which uses gpt-5.5 (non-gemini)
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - using ultrabrain (gpt model) with run_in_background=false
|
|
const result = await tool.execute(
|
|
{
|
|
description: "Test non-gemini sync",
|
|
prompt: "Do something smart",
|
|
category: "ultrabrain",
|
|
run_in_background: false,
|
|
load_skills: ["git-master"],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - should run sync, NOT forced to background
|
|
expect(launchCalled).toBe(false) // manager.launch should NOT be called
|
|
expect(promptCalled).toBe(true) // sync mode uses session.prompt
|
|
expect(result).not.toContain("UNSTABLE AGENT MODE")
|
|
}, { timeout: 20000 })
|
|
|
|
test("artistry category (gemini) with run_in_background=false should force background but wait for result", async () => {
|
|
// given - artistry also uses gemini model
|
|
const { createDelegateTask } = require("./tools")
|
|
let launchCalled = false
|
|
|
|
const launchedTask = {
|
|
id: "task-artistry",
|
|
sessionId: "ses_artistry_gemini",
|
|
description: "Artistry gemini task",
|
|
agent: "sisyphus-junior",
|
|
status: "running",
|
|
}
|
|
const mockManager = {
|
|
launch: async () => {
|
|
launchCalled = true
|
|
return launchedTask
|
|
},
|
|
getTask: () => launchedTask,
|
|
}
|
|
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
model: { list: async () => [{ provider: "google", id: "gemini-3.1-pro" }] },
|
|
session: {
|
|
get: async () => ({ data: { directory: "/project" } }),
|
|
create: async () => ({ data: { id: "ses_artistry_gemini" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({
|
|
data: [
|
|
{ info: { role: "assistant", time: { created: Date.now() } }, parts: [{ type: "text", text: "Artistry result here" }] }
|
|
]
|
|
}),
|
|
status: async () => ({ data: { "ses_artistry_gemini": { type: "idle" } } }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - artistry category (gemini-3.1-pro with high variant)
|
|
const result = await tool.execute(
|
|
{
|
|
description: "Test artistry forced background",
|
|
prompt: "Do something artistic",
|
|
category: "artistry",
|
|
run_in_background: false,
|
|
load_skills: ["git-master"],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - should launch as background BUT wait for and return actual result
|
|
expect(launchCalled).toBe(true)
|
|
expect(result).toContain("SUPERVISED TASK COMPLETED")
|
|
expect(result).toContain("Artistry result here")
|
|
}, { timeout: 20000 })
|
|
|
|
test("writing category (kimi) with run_in_background=false should run sync when kimi provider is available", async () => {
|
|
// given - writing uses kimi model which is no longer considered unstable
|
|
// Override provider cache to include kimi-for-coding provider
|
|
providerModelsSpy.mockReturnValue({
|
|
models: {
|
|
anthropic: ["claude-opus-4-7", "claude-sonnet-4-6", "claude-haiku-4-5"],
|
|
google: ["gemini-3.1-pro", "gemini-3-flash"],
|
|
openai: ["gpt-5.5", "gpt-5.5", "gpt-5.3-codex"],
|
|
"kimi-for-coding": ["k2p5"],
|
|
},
|
|
connected: ["anthropic", "google", "openai", "kimi-for-coding"],
|
|
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
})
|
|
cacheSpy.mockReturnValue(["anthropic", "google", "openai", "kimi-for-coding"])
|
|
|
|
const { createDelegateTask } = require("./tools")
|
|
let launchCalled = false
|
|
let promptCalled = false
|
|
|
|
const mockManager = {
|
|
launch: async () => {
|
|
launchCalled = true
|
|
return { id: "should-not-be-called", sessionId: "x", description: "x", agent: "x", status: "running" }
|
|
},
|
|
}
|
|
|
|
const promptMock = async () => {
|
|
promptCalled = true
|
|
return { data: {} }
|
|
}
|
|
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
get: async () => ({ data: { directory: "/project" } }),
|
|
create: async () => ({ data: { id: "ses_writing_kimi" } }),
|
|
prompt: promptMock,
|
|
promptAsync: promptMock,
|
|
messages: async () => ({
|
|
data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Writing result here" }] }]
|
|
}),
|
|
status: async () => ({ data: { "ses_writing_kimi": { type: "idle" } } }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - writing category (kimi) with run_in_background=false
|
|
const result = await tool.execute(
|
|
{
|
|
description: "Test writing sync",
|
|
prompt: "Write something",
|
|
category: "writing",
|
|
run_in_background: false,
|
|
load_skills: ["git-master"],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - should run sync, NOT forced to background (kimi is not unstable)
|
|
expect(launchCalled).toBe(false)
|
|
expect(promptCalled).toBe(true)
|
|
expect(result).not.toContain("SUPERVISED TASK COMPLETED")
|
|
}, { timeout: 20000 })
|
|
|
|
test("is_unstable_agent=true should force background but wait for result", async () => {
|
|
// given - custom category with is_unstable_agent=true but non-gemini model
|
|
const { createDelegateTask } = require("./tools")
|
|
let launchCalled = false
|
|
|
|
const launchedTask = {
|
|
id: "task-custom-unstable",
|
|
sessionId: "ses_custom_unstable",
|
|
description: "Custom unstable task",
|
|
agent: "sisyphus-junior",
|
|
status: "running",
|
|
}
|
|
const mockManager = {
|
|
launch: async () => {
|
|
launchCalled = true
|
|
return launchedTask
|
|
},
|
|
getTask: () => launchedTask,
|
|
}
|
|
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
get: async () => ({ data: { directory: "/project" } }),
|
|
create: async () => ({ data: { id: "ses_custom_unstable" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({
|
|
data: [
|
|
{ info: { role: "assistant", time: { created: Date.now() } }, parts: [{ type: "text", text: "Custom unstable result" }] }
|
|
]
|
|
}),
|
|
status: async () => ({ data: { "ses_custom_unstable": { type: "idle" } } }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
userCategories: {
|
|
"my-unstable-cat": {
|
|
model: "openai/gpt-5.5",
|
|
is_unstable_agent: true,
|
|
},
|
|
},
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - using custom unstable category with run_in_background=false
|
|
const result = await tool.execute(
|
|
{
|
|
description: "Test custom unstable",
|
|
prompt: "Do something",
|
|
category: "my-unstable-cat",
|
|
run_in_background: false,
|
|
load_skills: ["git-master"],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - should launch as background BUT wait for and return actual result
|
|
expect(launchCalled).toBe(true)
|
|
expect(result).toContain("SUPERVISED TASK COMPLETED")
|
|
expect(result).toContain("Custom unstable result")
|
|
}, { timeout: 20000 })
|
|
})
|
|
|
|
describe("category model resolution fallback", () => {
|
|
test("category uses resolved.model when connectedProvidersCache is null and availableModels is empty", async () => {
|
|
// given - connectedProvidersCache returns null (simulates missing cache file)
|
|
// This is a regression test for PR #1227 which removed resolved.model from userModel chain
|
|
cacheSpy.mockReturnValue(null)
|
|
|
|
const { createDelegateTask } = require("./tools")
|
|
let launchInput: any
|
|
|
|
const mockManager = {
|
|
launch: async (input: any) => {
|
|
launchInput = input
|
|
return {
|
|
id: "task-fallback",
|
|
sessionId: "ses_fallback_test",
|
|
description: "Fallback test task",
|
|
agent: "sisyphus-junior",
|
|
status: "running",
|
|
}
|
|
},
|
|
}
|
|
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
model: { list: async () => [] },
|
|
session: {
|
|
create: async () => ({ data: { id: "test-session" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
},
|
|
}
|
|
|
|
// NO userCategories override, NO sisyphusJuniorModel
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
// userCategories: undefined - use DEFAULT_CATEGORIES only
|
|
// sisyphusJuniorModel: undefined
|
|
connectedProvidersOverride: null,
|
|
availableModelsOverride: new Set(),
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - using "quick" category which should use the catalog model
|
|
await tool.execute(
|
|
{
|
|
description: "Test category fallback",
|
|
prompt: "Do something quick",
|
|
category: "quick",
|
|
run_in_background: true,
|
|
load_skills: [],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - model should be openai/gpt-5.4-mini from DEFAULT_CATEGORIES
|
|
// NOT anthropic/claude-sonnet-4-6 (system default)
|
|
expect(launchInput.model.providerID).toBe("openai")
|
|
expect(launchInput.model.modelID).toBe("gpt-5.4-mini")
|
|
})
|
|
|
|
test("category delegation ignores UI-selected (Kimi) system default model", async () => {
|
|
// given - OpenCode system default model is Kimi (selected from UI)
|
|
const { createDelegateTask } = require("./tools")
|
|
let launchInput: any
|
|
|
|
const mockManager = {
|
|
launch: async (input: any) => {
|
|
launchInput = input
|
|
return {
|
|
id: "task-ui-model",
|
|
sessionId: "ses_ui_model_test",
|
|
description: "UI model inheritance test",
|
|
agent: "sisyphus-junior",
|
|
status: "running",
|
|
}
|
|
},
|
|
}
|
|
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
model: { list: async () => [] },
|
|
session: {
|
|
create: async () => ({ data: { id: "test-session" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
userCategories: {
|
|
"fallback-test": { model: "anthropic/claude-opus-4-7" },
|
|
},
|
|
connectedProvidersOverride: TEST_CONNECTED_PROVIDERS,
|
|
availableModelsOverride: createTestAvailableModels(),
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - using "quick" category which should use the catalog model
|
|
await tool.execute(
|
|
{
|
|
description: "UI model inheritance test",
|
|
prompt: "Do something quick",
|
|
category: "quick",
|
|
run_in_background: true,
|
|
load_skills: [],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - category model must win (not Kimi)
|
|
expect(launchInput.model.providerID).toBe("openai")
|
|
expect(launchInput.model.modelID).toBe("gpt-5.4-mini")
|
|
})
|
|
|
|
test("sisyphus-junior model override takes precedence over category model", async () => {
|
|
// given - sisyphus-junior override model differs from category default
|
|
const { createDelegateTask } = require("./tools")
|
|
let launchInput: any
|
|
|
|
const mockManager = {
|
|
launch: async (input: any) => {
|
|
launchInput = input
|
|
return {
|
|
id: "task-override",
|
|
sessionId: "ses_override_test",
|
|
description: "Override precedence test",
|
|
agent: "sisyphus-junior",
|
|
status: "running",
|
|
}
|
|
},
|
|
}
|
|
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
model: { list: async () => [] },
|
|
session: {
|
|
create: async () => ({ data: { id: "test-session" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
sisyphusJuniorModel: "anthropic/claude-sonnet-4-6",
|
|
connectedProvidersOverride: TEST_CONNECTED_PROVIDERS,
|
|
availableModelsOverride: createTestAvailableModels(),
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - using ultrabrain category (default model is openai/gpt-5.5)
|
|
await tool.execute(
|
|
{
|
|
description: "Override precedence test",
|
|
prompt: "Do something",
|
|
category: "ultrabrain",
|
|
run_in_background: true,
|
|
load_skills: [],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - override model should be used instead of category model
|
|
expect(launchInput.model.providerID).toBe("anthropic")
|
|
expect(launchInput.model.modelID).toBe("claude-sonnet-4-6")
|
|
})
|
|
|
|
test("explicit category model takes precedence over sisyphus-junior model", async () => {
|
|
// given - explicit category model differs from sisyphus-junior override
|
|
const { createDelegateTask } = require("./tools")
|
|
let launchInput: any
|
|
|
|
const mockManager = {
|
|
launch: async (input: any) => {
|
|
launchInput = input
|
|
return {
|
|
id: "task-category-precedence",
|
|
sessionId: "ses_category_precedence_test",
|
|
description: "Category precedence test",
|
|
agent: "sisyphus-junior",
|
|
status: "running",
|
|
}
|
|
},
|
|
}
|
|
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
model: { list: async () => [] },
|
|
session: {
|
|
create: async () => ({ data: { id: "test-session" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
sisyphusJuniorModel: "anthropic/claude-sonnet-4-6",
|
|
userCategories: {
|
|
ultrabrain: { model: "openai/gpt-5.5" },
|
|
},
|
|
connectedProvidersOverride: TEST_CONNECTED_PROVIDERS,
|
|
availableModelsOverride: createTestAvailableModels(),
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - using ultrabrain category with explicit model override
|
|
await tool.execute(
|
|
{
|
|
description: "Category precedence test",
|
|
prompt: "Do something",
|
|
category: "ultrabrain",
|
|
run_in_background: true,
|
|
load_skills: [],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - explicit category model should win
|
|
expect(launchInput.model.providerID).toBe("openai")
|
|
expect(launchInput.model.modelID).toBe("gpt-5.5")
|
|
})
|
|
|
|
test("sisyphus-junior model override works with quick category (#1295)", async () => {
|
|
// given - user configures agents.sisyphus-junior.model but uses quick category
|
|
const { createDelegateTask } = require("./tools")
|
|
let launchInput: any
|
|
|
|
const mockManager = {
|
|
launch: async (input: any) => {
|
|
launchInput = input
|
|
return {
|
|
id: "task-1295-quick",
|
|
sessionId: "ses_1295_quick",
|
|
description: "Issue 1295 regression",
|
|
agent: "sisyphus-junior",
|
|
status: "running",
|
|
}
|
|
},
|
|
}
|
|
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
model: { list: async () => [] },
|
|
session: {
|
|
create: async () => ({ data: { id: "test-session" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
sisyphusJuniorModel: "anthropic/claude-sonnet-4-6",
|
|
connectedProvidersOverride: TEST_CONNECTED_PROVIDERS,
|
|
availableModelsOverride: createTestAvailableModels(),
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - using quick category (default: anthropic/claude-haiku-4-5)
|
|
await tool.execute(
|
|
{
|
|
description: "Issue 1295 quick category test",
|
|
prompt: "Quick task",
|
|
category: "quick",
|
|
run_in_background: true,
|
|
load_skills: [],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - sisyphus-junior override model should be used, not category default
|
|
expect(launchInput.model.providerID).toBe("anthropic")
|
|
expect(launchInput.model.modelID).toBe("claude-sonnet-4-6")
|
|
expect(launchInput.fallbackChain).toBeUndefined()
|
|
})
|
|
|
|
test("sisyphus-junior model override works with user-defined category (#1295)", async () => {
|
|
// given - user has a custom category with no model requirement
|
|
const { createDelegateTask } = require("./tools")
|
|
let launchInput: any
|
|
|
|
const mockManager = {
|
|
launch: async (input: any) => {
|
|
launchInput = input
|
|
return {
|
|
id: "task-1295-custom",
|
|
sessionId: "ses_1295_custom",
|
|
description: "Issue 1295 custom category",
|
|
agent: "sisyphus-junior",
|
|
status: "running",
|
|
}
|
|
},
|
|
}
|
|
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
model: { list: async () => [] },
|
|
session: {
|
|
create: async () => ({ data: { id: "test-session" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
sisyphusJuniorModel: "openai/gpt-5.5",
|
|
userCategories: {
|
|
"my-custom": { temperature: 0.5 },
|
|
},
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - using custom category with no explicit model
|
|
await tool.execute(
|
|
{
|
|
description: "Custom category with agent model",
|
|
prompt: "Do something custom",
|
|
category: "my-custom",
|
|
run_in_background: true,
|
|
load_skills: [],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - sisyphus-junior override model should be used as fallback
|
|
expect(launchInput.model.providerID).toBe("openai")
|
|
expect(launchInput.model.modelID).toBe("gpt-5.5")
|
|
})
|
|
})
|
|
|
|
describe("browserProvider propagation", () => {
|
|
test("should resolve agent-browser skill when browserProvider is passed", async () => {
|
|
// given - task configured with browserProvider: "agent-browser"
|
|
const { createDelegateTask } = require("./tools")
|
|
let promptBody: any
|
|
|
|
const mockManager = { launch: async () => ({}) }
|
|
|
|
const promptMock = async (input: any) => {
|
|
promptBody = input.body
|
|
return { data: {} }
|
|
}
|
|
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
get: async () => ({ data: { directory: "/project" } }),
|
|
create: async () => ({ data: { id: "ses_browser_provider" } }),
|
|
prompt: promptMock,
|
|
promptAsync: promptMock,
|
|
messages: async () => ({
|
|
data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Done" }] }]
|
|
}),
|
|
status: async () => ({ data: {} }),
|
|
},
|
|
}
|
|
|
|
// Pass browserProvider to createDelegateTask
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
browserProvider: "agent-browser",
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - request agent-browser skill
|
|
await tool.execute(
|
|
{
|
|
description: "Test browserProvider propagation",
|
|
prompt: "Do something",
|
|
category: "ultrabrain",
|
|
run_in_background: false,
|
|
load_skills: ["agent-browser"],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - agent-browser skill should be resolved
|
|
expect(promptBody).toBeDefined()
|
|
expect(promptBody.system).toBeDefined()
|
|
expect(promptBody.system).toContain("<Category_Context>")
|
|
expect(String(promptBody.system).startsWith("<Category_Context>")).toBe(false)
|
|
}, { timeout: 20000 })
|
|
|
|
test("should resolve agent-browser skill even when browserProvider is not set", async () => {
|
|
// given - delegate_task without browserProvider
|
|
const { createDelegateTask } = require("./tools")
|
|
const mockManager = { launch: async () => ({}) }
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
get: async () => ({ data: { directory: "/project" } }),
|
|
create: async () => ({ data: { id: "ses_no_browser_provider" } }),
|
|
prompt: async () => {
|
|
return { data: {} }
|
|
},
|
|
messages: async () => ({
|
|
data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Done" }] }]
|
|
}),
|
|
status: async () => ({ data: {} }),
|
|
},
|
|
}
|
|
|
|
// No browserProvider passed
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - request agent-browser skill without browserProvider
|
|
const result = await tool.execute(
|
|
{
|
|
description: "Test missing browserProvider",
|
|
prompt: "Do something",
|
|
category: "ultrabrain",
|
|
run_in_background: false,
|
|
load_skills: ["agent-browser"],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - agent-browser skill should NOT resolve without browserProvider
|
|
expect(result).toContain("Skills not found")
|
|
expect(result).toContain("agent-browser")
|
|
})
|
|
})
|
|
|
|
describe("buildSystemContent", () => {
|
|
test("returns undefined when no skills and no category promptAppend", () => {
|
|
// given
|
|
const { buildSystemContent } = require("./tools")
|
|
|
|
// when
|
|
const result = buildSystemContent({ skillContent: undefined, categoryPromptAppend: undefined })
|
|
|
|
// then
|
|
expect(result).toBeUndefined()
|
|
})
|
|
|
|
test("returns skill content only when skills provided without category", () => {
|
|
// given
|
|
const { buildSystemContent } = require("./tools")
|
|
const skillContent = "You are a playwright expert"
|
|
|
|
// when
|
|
const result = buildSystemContent({ skillContent, categoryPromptAppend: undefined })
|
|
|
|
// then
|
|
expect(result).toBe(skillContent)
|
|
})
|
|
|
|
test("returns category promptAppend only when no skills", () => {
|
|
// given
|
|
const { buildSystemContent } = require("./tools")
|
|
const categoryPromptAppend = "Focus on visual design"
|
|
|
|
// when
|
|
const result = buildSystemContent({ skillContent: undefined, categoryPromptAppend })
|
|
|
|
// then
|
|
expect(result).toBe(categoryPromptAppend)
|
|
})
|
|
|
|
test("combines skill content and category promptAppend with separator", () => {
|
|
// given
|
|
const { buildSystemContent } = require("./tools")
|
|
const skillContent = "You are a playwright expert"
|
|
const categoryPromptAppend = "Focus on visual design"
|
|
|
|
// when
|
|
const result = buildSystemContent({ skillContent, categoryPromptAppend })
|
|
|
|
// then
|
|
expect(result).toContain(skillContent)
|
|
expect(result).toContain(categoryPromptAppend)
|
|
expect(result).toContain("\n\n")
|
|
})
|
|
|
|
test("prepends plan agent system prompt when agentName is 'plan'", () => {
|
|
// given
|
|
const { buildSystemContent } = require("./tools")
|
|
const { buildPlanAgentSystemPrepend } = require("./constants")
|
|
|
|
const availableCategories = [
|
|
{
|
|
name: "deep",
|
|
description: "Goal-oriented autonomous problem-solving",
|
|
model: "openai/gpt-5.3-codex",
|
|
},
|
|
]
|
|
const availableSkills = [
|
|
{
|
|
name: "typescript-programmer",
|
|
description: "Production TypeScript code.",
|
|
location: "plugin",
|
|
},
|
|
]
|
|
|
|
// when
|
|
const result = buildSystemContent({
|
|
agentName: "plan",
|
|
availableCategories,
|
|
availableSkills,
|
|
})
|
|
|
|
// then
|
|
expect(result).toContain("<system>")
|
|
expect(result).toContain("MANDATORY CONTEXT GATHERING PROTOCOL")
|
|
expect(result).toContain("### AVAILABLE CATEGORIES")
|
|
expect(result).toContain("`deep`")
|
|
expect(result).not.toContain("prompt-engineer")
|
|
expect(result).toBe(buildPlanAgentSystemPrepend(availableCategories, availableSkills))
|
|
})
|
|
|
|
test("does not prepend plan agent prompt for prometheus agent", () => {
|
|
//#given - prometheus is NOT a plan agent (decoupled)
|
|
const { buildSystemContent } = require("./tools")
|
|
const skillContent = "You are a strategic planner"
|
|
|
|
//#when
|
|
const result = buildSystemContent({
|
|
skillContent,
|
|
agentName: "prometheus",
|
|
})
|
|
|
|
//#then - prometheus should NOT get plan agent system prepend
|
|
expect(result).toBe(skillContent)
|
|
expect(result).not.toContain("MANDATORY CONTEXT GATHERING PROTOCOL")
|
|
})
|
|
|
|
test("does not prepend plan agent prompt for Prometheus (case insensitive)", () => {
|
|
//#given - Prometheus (capitalized) is NOT a plan agent
|
|
const { buildSystemContent } = require("./tools")
|
|
const skillContent = "You are a strategic planner"
|
|
|
|
//#when
|
|
const result = buildSystemContent({
|
|
skillContent,
|
|
agentName: "Prometheus",
|
|
})
|
|
|
|
//#then
|
|
expect(result).toBe(skillContent)
|
|
expect(result).not.toContain("MANDATORY CONTEXT GATHERING PROTOCOL")
|
|
})
|
|
|
|
test("combines plan agent prepend with skill content", () => {
|
|
// given
|
|
const { buildSystemContent } = require("./tools")
|
|
const { buildPlanAgentSystemPrepend } = require("./constants")
|
|
const skillContent = "You are a planning expert"
|
|
|
|
const availableCategories = [
|
|
{
|
|
name: "writing",
|
|
description: "Documentation, prose, technical writing",
|
|
model: "kimi-for-coding/k2p5",
|
|
},
|
|
]
|
|
const availableSkills = [
|
|
{
|
|
name: "python-programmer",
|
|
description: "Production Python code.",
|
|
location: "plugin",
|
|
},
|
|
]
|
|
const planPrepend = buildPlanAgentSystemPrepend(availableCategories, availableSkills)
|
|
|
|
// when
|
|
const result = buildSystemContent({
|
|
skillContent,
|
|
agentName: "plan",
|
|
availableCategories,
|
|
availableSkills,
|
|
})
|
|
|
|
// then
|
|
expect(result).toContain(planPrepend)
|
|
expect(result).toContain(skillContent)
|
|
expect(result!.indexOf(planPrepend)).toBeLessThan(result!.indexOf(skillContent))
|
|
})
|
|
|
|
test("does not prepend plan agent prompt for non-plan agents", () => {
|
|
// given
|
|
const { buildSystemContent } = require("./tools")
|
|
const skillContent = "You are an expert"
|
|
|
|
// when
|
|
const result = buildSystemContent({ skillContent, agentName: "oracle" })
|
|
|
|
// then
|
|
expect(result).toBe(skillContent)
|
|
expect(result).not.toContain("<system>")
|
|
})
|
|
|
|
test("does not prepend plan agent prompt when agentName is undefined", () => {
|
|
// given
|
|
const { buildSystemContent } = require("./tools")
|
|
const skillContent = "You are an expert"
|
|
|
|
// when
|
|
const result = buildSystemContent({ skillContent, agentName: undefined })
|
|
|
|
// then
|
|
expect(result).toBe(skillContent)
|
|
expect(result).not.toContain("<system>")
|
|
})
|
|
})
|
|
|
|
describe("buildTaskPrompt", () => {
|
|
test("appends English ULW TDD and commit guidance for plan agent", () => {
|
|
// given
|
|
const { buildTaskPrompt } = require("./tools")
|
|
const prompt = "Create a work plan for this feature"
|
|
|
|
// when
|
|
const result = buildTaskPrompt(prompt, "plan")
|
|
|
|
// then
|
|
expect(result).toContain(prompt)
|
|
expect(result).toContain("Answer in English.")
|
|
expect(result).toContain("Write the plan in English.")
|
|
expect(result).toContain("Plan well for ultrawork execution.")
|
|
expect(result).toContain("Use TDD-oriented planning.")
|
|
expect(result).toContain("Include a clear atomic commit strategy.")
|
|
})
|
|
|
|
test("does not append plan guidance for non-plan agents", () => {
|
|
// given
|
|
const { buildTaskPrompt } = require("./tools")
|
|
const prompt = "Investigate this module"
|
|
|
|
// when
|
|
const result = buildTaskPrompt(prompt, "explore")
|
|
|
|
// then
|
|
expect(result).toBe(prompt)
|
|
})
|
|
|
|
test("excludes TDD line when tddEnabled is false", () => {
|
|
// given
|
|
const { buildTaskPrompt } = require("./tools")
|
|
const prompt = "Create a work plan for this feature"
|
|
|
|
// when
|
|
const result = buildTaskPrompt(prompt, "plan", false)
|
|
|
|
// then
|
|
expect(result).toContain(prompt)
|
|
expect(result).toContain("Answer in English.")
|
|
expect(result).toContain("Write the plan in English.")
|
|
expect(result).toContain("Plan well for ultrawork execution.")
|
|
expect(result).toContain("Include a clear atomic commit strategy.")
|
|
expect(result).not.toContain("Use TDD-oriented planning.")
|
|
})
|
|
|
|
test("includes TDD line when tddEnabled is true", () => {
|
|
// given
|
|
const { buildTaskPrompt } = require("./tools")
|
|
const prompt = "Create a work plan for this feature"
|
|
|
|
// when
|
|
const result = buildTaskPrompt(prompt, "plan", true)
|
|
|
|
// then
|
|
expect(result).toContain("Use TDD-oriented planning.")
|
|
})
|
|
})
|
|
|
|
describe("modelInfo detection via resolveCategoryConfig", () => {
|
|
test("catalog model is used for category with catalog entry", () => {
|
|
// given - ultrabrain has catalog entry
|
|
const categoryName = "ultrabrain"
|
|
|
|
// when
|
|
const resolved = resolveCategoryConfig(categoryName, { systemDefaultModel: SYSTEM_DEFAULT_MODEL })
|
|
|
|
// then - catalog model is used
|
|
expect(resolved).not.toBeNull()
|
|
expect(resolved!.config.model).toBe("openai/gpt-5.5")
|
|
expect(resolved!.config.variant).toBe("xhigh")
|
|
})
|
|
|
|
test("default model is used for category with default entry", () => {
|
|
// given - unspecified-low has default model
|
|
const categoryName = "unspecified-low"
|
|
|
|
// when
|
|
const resolved = resolveCategoryConfig(categoryName, { systemDefaultModel: SYSTEM_DEFAULT_MODEL })
|
|
|
|
// then - default model from DEFAULT_CATEGORIES is used
|
|
expect(resolved).not.toBeNull()
|
|
expect(resolved!.config.model).toBe("anthropic/claude-sonnet-4-6")
|
|
})
|
|
|
|
test("category built-in model takes precedence over inheritedModel for builtin category", () => {
|
|
// given - builtin ultrabrain category with its own model, inherited model also provided
|
|
const categoryName = "ultrabrain"
|
|
const inheritedModel = "cliproxy/claude-opus-4-7"
|
|
|
|
// when
|
|
const resolved = resolveCategoryConfig(categoryName, { inheritedModel, systemDefaultModel: SYSTEM_DEFAULT_MODEL })
|
|
|
|
// then - category's built-in model wins (ultrabrain uses gpt-5.5)
|
|
expect(resolved).not.toBeNull()
|
|
const actualModel = resolved!.config.model
|
|
expect(actualModel).toBe("openai/gpt-5.5")
|
|
})
|
|
|
|
test("when user defines model - modelInfo should report user-defined regardless of inheritedModel", () => {
|
|
// given
|
|
const categoryName = "ultrabrain"
|
|
const userCategories = { "ultrabrain": { model: "my-provider/custom-model" } }
|
|
const inheritedModel = "cliproxy/claude-opus-4-7"
|
|
|
|
// when
|
|
const resolved = resolveCategoryConfig(categoryName, { userCategories, inheritedModel, systemDefaultModel: SYSTEM_DEFAULT_MODEL })
|
|
|
|
// then - actualModel should be userModel, type should be "user-defined"
|
|
expect(resolved).not.toBeNull()
|
|
const actualModel = resolved!.config.model
|
|
const userDefinedModel = userCategories[categoryName]?.model
|
|
expect(actualModel).toBe(userDefinedModel)
|
|
expect(actualModel).toBe("my-provider/custom-model")
|
|
})
|
|
|
|
test("detection logic: actualModel comparison correctly identifies source", () => {
|
|
// given - This test verifies the fix for PR #770 bug
|
|
// The bug was: checking `if (inheritedModel)` instead of `if (actualModel === inheritedModel)`
|
|
const categoryName = "ultrabrain"
|
|
const inheritedModel = "cliproxy/claude-opus-4-7"
|
|
const userCategories = { "ultrabrain": { model: "user/model" } }
|
|
|
|
// when - user model wins
|
|
const resolved = resolveCategoryConfig(categoryName, { userCategories, inheritedModel, systemDefaultModel: SYSTEM_DEFAULT_MODEL })
|
|
const actualModel = resolved!.config.model
|
|
const userDefinedModel = userCategories[categoryName]?.model
|
|
|
|
// then - detection should compare against actual resolved model
|
|
const detectedType = actualModel === userDefinedModel
|
|
? "user-defined"
|
|
: actualModel === inheritedModel
|
|
? "inherited"
|
|
: actualModel === SYSTEM_DEFAULT_MODEL
|
|
? "system-default"
|
|
: undefined
|
|
|
|
expect(detectedType).toBe("user-defined")
|
|
expect(actualModel).not.toBe(inheritedModel)
|
|
})
|
|
|
|
// These tests verify the NEW behavior where categories do NOT have default models
|
|
|
|
test("FIXED: category built-in model takes precedence over inheritedModel", () => {
|
|
// given a builtin category with its own model, and an inherited model from parent
|
|
// The CORRECT chain: userConfig?.model ?? categoryBuiltIn ?? systemDefaultModel
|
|
const categoryName = "ultrabrain"
|
|
const inheritedModel = "anthropic/claude-opus-4-7"
|
|
|
|
// when category has a built-in model (gpt-5.5 for ultrabrain)
|
|
const resolved = resolveCategoryConfig(categoryName, { inheritedModel, systemDefaultModel: SYSTEM_DEFAULT_MODEL })
|
|
|
|
// then category's built-in model should be used, NOT inheritedModel
|
|
expect(resolved).not.toBeNull()
|
|
expect(resolved!.model).toBe("openai/gpt-5.5")
|
|
})
|
|
|
|
test("FIXED: systemDefaultModel is used when no userConfig.model and no inheritedModel", () => {
|
|
// given a custom category with no default model
|
|
const categoryName = "custom-no-default"
|
|
const userCategories: Record<string, CategoryConfig> = { "custom-no-default": { temperature: 0.5 } }
|
|
const systemDefaultModel = "anthropic/claude-sonnet-4-6"
|
|
|
|
// when no inheritedModel is provided, only systemDefaultModel
|
|
const resolved = resolveCategoryConfig(categoryName, {
|
|
userCategories,
|
|
systemDefaultModel
|
|
})
|
|
|
|
// then systemDefaultModel should be returned
|
|
expect(resolved).not.toBeNull()
|
|
expect(resolved!.model).toBe("anthropic/claude-sonnet-4-6")
|
|
})
|
|
|
|
test("FIXED: userConfig.model always takes priority over everything", () => {
|
|
// given userConfig.model is explicitly set
|
|
const categoryName = "ultrabrain"
|
|
const userCategories = { "ultrabrain": { model: "custom/user-model" } }
|
|
const inheritedModel = "anthropic/claude-opus-4-7"
|
|
const systemDefaultModel = "anthropic/claude-sonnet-4-6"
|
|
|
|
// when resolveCategoryConfig is called with all sources
|
|
const resolved = resolveCategoryConfig(categoryName, {
|
|
userCategories,
|
|
inheritedModel,
|
|
systemDefaultModel
|
|
})
|
|
|
|
// then userConfig.model should win
|
|
expect(resolved).not.toBeNull()
|
|
expect(resolved!.model).toBe("custom/user-model")
|
|
})
|
|
|
|
test("FIXED: empty string in userConfig.model is treated as unset and falls back to systemDefault", () => {
|
|
// given userConfig.model is empty string "" for a custom category (no built-in model)
|
|
const categoryName = "custom-empty-model"
|
|
const userCategories = { "custom-empty-model": { model: "", temperature: 0.3 } }
|
|
const inheritedModel = "anthropic/claude-opus-4-7"
|
|
|
|
// when resolveCategoryConfig is called
|
|
const resolved = resolveCategoryConfig(categoryName, { userCategories, inheritedModel, systemDefaultModel: SYSTEM_DEFAULT_MODEL })
|
|
|
|
// then should fall back to systemDefaultModel since custom category has no built-in model
|
|
expect(resolved).not.toBeNull()
|
|
expect(resolved!.model).toBe(SYSTEM_DEFAULT_MODEL)
|
|
})
|
|
|
|
test("FIXED: undefined userConfig.model falls back to category built-in model", () => {
|
|
// given user sets a builtin category but leaves model undefined
|
|
const categoryName = "visual-engineering"
|
|
const userCategories: Record<string, CategoryConfig> = { "visual-engineering": { temperature: 0.2 } }
|
|
const inheritedModel = "anthropic/claude-opus-4-7"
|
|
|
|
// when resolveCategoryConfig is called
|
|
const resolved = resolveCategoryConfig(categoryName, { userCategories, inheritedModel, systemDefaultModel: SYSTEM_DEFAULT_MODEL })
|
|
|
|
// then should use category's built-in model (gemini-3.1-pro for visual-engineering)
|
|
expect(resolved).not.toBeNull()
|
|
expect(resolved!.model).toBe("google/gemini-3.1-pro")
|
|
})
|
|
|
|
test("systemDefaultModel is used when no other model is available", () => {
|
|
// given - custom category with no model, but systemDefaultModel is set
|
|
const categoryName = "my-custom"
|
|
const userCategories: Record<string, CategoryConfig> = { "my-custom": { temperature: 0.5 } }
|
|
const systemDefaultModel = "anthropic/claude-sonnet-4-6"
|
|
|
|
// when
|
|
const resolved = resolveCategoryConfig(categoryName, { userCategories, systemDefaultModel })
|
|
|
|
// then - actualModel should be systemDefaultModel
|
|
expect(resolved).not.toBeNull()
|
|
expect(resolved!.model).toBe(systemDefaultModel)
|
|
})
|
|
})
|
|
|
|
describe("plan family mutual delegation block", () => {
|
|
test("plan cannot delegate to plan (self-delegation)", async () => {
|
|
//#given
|
|
const { createDelegateTask } = require("./tools")
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [{ name: "plan", mode: "subagent" }] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: { get: async () => ({ data: { directory: "/project" } }), create: async () => ({ data: { id: "s" } }), prompt: async () => ({ data: {} }), promptAsync: async () => ({ data: {} }), messages: async () => ({ data: [] }), status: async () => ({ data: {} }) },
|
|
}
|
|
const tool = createDelegateTask({ manager: { launch: async () => ({}) }, client: mockClient })
|
|
|
|
//#when
|
|
const result = await tool.execute(
|
|
{ description: "test", prompt: "Create a plan", subagent_type: "plan", run_in_background: false, load_skills: [] },
|
|
{ sessionID: "p", messageID: "m", agent: "plan", abort: new AbortController().signal }
|
|
)
|
|
|
|
//#then
|
|
expect(result).toContain("plan-family")
|
|
expect(result).toContain("directly")
|
|
})
|
|
|
|
test("prometheus cannot delegate to plan (cross-blocking)", async () => {
|
|
//#given
|
|
const { createDelegateTask } = require("./tools")
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [{ name: "plan", mode: "subagent" }] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: { get: async () => ({ data: { directory: "/project" } }), create: async () => ({ data: { id: "s" } }), prompt: async () => ({ data: {} }), promptAsync: async () => ({ data: {} }), messages: async () => ({ data: [] }), status: async () => ({ data: {} }) },
|
|
}
|
|
const tool = createDelegateTask({ manager: { launch: async () => ({}) }, client: mockClient })
|
|
|
|
//#when
|
|
const result = await tool.execute(
|
|
{ description: "test", prompt: "Create a plan", subagent_type: "plan", run_in_background: false, load_skills: [] },
|
|
{ sessionID: "p", messageID: "m", agent: "prometheus", abort: new AbortController().signal }
|
|
)
|
|
|
|
//#then
|
|
expect(result).toContain("plan-family")
|
|
})
|
|
|
|
test("prometheus display name cannot delegate to plan (cross-blocking)", async () => {
|
|
//#given
|
|
const { createDelegateTask } = require("./tools")
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [{ name: "plan", mode: "subagent" }] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: { get: async () => ({ data: { directory: "/project" } }), create: async () => ({ data: { id: "s" } }), prompt: async () => ({ data: {} }), promptAsync: async () => ({ data: {} }), messages: async () => ({ data: [] }), status: async () => ({ data: {} }) },
|
|
}
|
|
const tool = createDelegateTask({ manager: { launch: async () => ({}) }, client: mockClient })
|
|
|
|
//#when
|
|
const result = await tool.execute(
|
|
{ description: "test", prompt: "Create a plan", subagent_type: "plan", run_in_background: false, load_skills: [] },
|
|
{ sessionID: "p", messageID: "m", agent: getAgentDisplayName("prometheus"), abort: new AbortController().signal }
|
|
)
|
|
|
|
//#then
|
|
expect(result).toContain("plan-family")
|
|
})
|
|
|
|
test("plan cannot delegate to prometheus even when it is exposed as a primary agent", async () => {
|
|
//#given
|
|
const { createDelegateTask } = require("./tools")
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [{ name: "prometheus", mode: "primary" }] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: { get: async () => ({ data: { directory: "/project" } }), create: async () => ({ data: { id: "s" } }), prompt: async () => ({ data: {} }), promptAsync: async () => ({ data: {} }), messages: async () => ({ data: [] }), status: async () => ({ data: {} }) },
|
|
}
|
|
const tool = createDelegateTask({ manager: { launch: async () => ({}) }, client: mockClient })
|
|
|
|
//#when
|
|
const result = await tool.execute(
|
|
{ description: "test", prompt: "Execute", subagent_type: "prometheus", run_in_background: false, load_skills: [] },
|
|
{ sessionID: "p", messageID: "m", agent: "plan", abort: new AbortController().signal }
|
|
)
|
|
|
|
//#then
|
|
expect(result).toContain("plan-family")
|
|
})
|
|
|
|
test("sisyphus CAN delegate to plan (not in plan family)", async () => {
|
|
//#given
|
|
const { createDelegateTask } = require("./tools")
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [{ name: "plan", mode: "subagent" }] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
get: async () => ({ data: { directory: "/project" } }),
|
|
create: async () => ({ data: { id: "ses_ok" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Plan created" }] }] }),
|
|
status: async () => ({ data: { "ses_ok": { type: "idle" } } }),
|
|
},
|
|
}
|
|
const tool = createDelegateTask({ manager: { launch: async () => ({}) }, client: mockClient })
|
|
|
|
//#when
|
|
const result = await tool.execute(
|
|
{ description: "test", prompt: "Create a plan", subagent_type: "plan", run_in_background: false, load_skills: [] },
|
|
{ sessionID: "p", messageID: "m", agent: "sisyphus", abort: new AbortController().signal }
|
|
)
|
|
|
|
//#then
|
|
expect(result).not.toContain("plan-family")
|
|
expect(result).toContain("Plan created")
|
|
}, { timeout: 20000 })
|
|
})
|
|
|
|
describe("subagent_type model extraction (issue #1225)", () => {
|
|
test("background mode passes matched agent model to manager.launch", async () => {
|
|
// given - agent with model registered, using subagent_type with run_in_background=true
|
|
const { createDelegateTask } = require("./tools")
|
|
let launchInput: any
|
|
|
|
const mockManager = {
|
|
launch: async (input: any) => {
|
|
launchInput = input
|
|
return {
|
|
id: "task-explore",
|
|
sessionId: "ses_explore_model",
|
|
description: "Explore task",
|
|
agent: "explore",
|
|
status: "running",
|
|
}
|
|
},
|
|
}
|
|
|
|
const mockClient = {
|
|
app: {
|
|
agents: async () => ({
|
|
data: [
|
|
{ name: "explore", mode: "subagent", model: { providerID: "anthropic", modelID: "claude-haiku-4-5" } },
|
|
],
|
|
}),
|
|
},
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
create: async () => ({ data: { id: "ses_explore_model" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - delegating to explore agent via subagent_type
|
|
await tool.execute(
|
|
{
|
|
description: "Explore codebase",
|
|
prompt: "Find auth patterns",
|
|
subagent_type: "explore",
|
|
run_in_background: true,
|
|
load_skills: [],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - matched agent's model should be passed to manager.launch
|
|
expect(launchInput.model).toEqual({
|
|
providerID: "anthropic",
|
|
modelID: "claude-haiku-4-5",
|
|
})
|
|
})
|
|
|
|
test("sync mode passes matched agent model to session.prompt", async () => {
|
|
// given - agent with model registered, using subagent_type with run_in_background=false
|
|
const { createDelegateTask } = require("./tools")
|
|
let promptBody: any
|
|
|
|
const mockManager = { launch: async () => ({}) }
|
|
|
|
const promptMock = async (input: any) => {
|
|
promptBody = input.body
|
|
return { data: {} }
|
|
}
|
|
|
|
const mockClient = {
|
|
app: {
|
|
agents: async () => ({
|
|
data: [
|
|
{ name: "oracle", mode: "subagent", model: { providerID: "anthropic", modelID: "claude-opus-4-7" } },
|
|
],
|
|
}),
|
|
},
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
get: async () => ({ data: { directory: "/project" } }),
|
|
create: async () => ({ data: { id: "ses_oracle_model" } }),
|
|
prompt: promptMock,
|
|
promptAsync: promptMock,
|
|
messages: async () => ({
|
|
data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Consultation done" }] }],
|
|
}),
|
|
status: async () => ({ data: { "ses_oracle_model": { type: "idle" } } }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - delegating to oracle agent via subagent_type in sync mode
|
|
await tool.execute(
|
|
{
|
|
description: "Consult oracle",
|
|
prompt: "Review architecture",
|
|
subagent_type: "oracle",
|
|
run_in_background: false,
|
|
load_skills: [],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - matched agent's model should be passed to session.prompt
|
|
expect(promptBody.model).toEqual({
|
|
providerID: "anthropic",
|
|
modelID: "claude-opus-4-7",
|
|
})
|
|
}, { timeout: 20000 })
|
|
|
|
test("agent without model resolves via fallback chain", async () => {
|
|
// given - agent registered without model field, fallback chain should resolve
|
|
const { createDelegateTask } = require("./tools")
|
|
let promptBody: any
|
|
|
|
const mockManager = { launch: async () => ({}) }
|
|
|
|
const promptMock = async (input: any) => {
|
|
promptBody = input.body
|
|
return { data: {} }
|
|
}
|
|
|
|
const mockClient = {
|
|
app: {
|
|
agents: async () => ({
|
|
data: [
|
|
{ name: "explore", mode: "subagent" },
|
|
],
|
|
}),
|
|
},
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
get: async () => ({ data: { directory: "/project" } }),
|
|
create: async () => ({ data: { id: "ses_no_model_agent" } }),
|
|
prompt: promptMock,
|
|
promptAsync: promptMock,
|
|
messages: async () => ({
|
|
data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Done" }] }],
|
|
}),
|
|
status: async () => ({ data: { "ses_no_model_agent": { type: "idle" } } }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - delegating to agent without model
|
|
await tool.execute(
|
|
{
|
|
description: "Explore without model",
|
|
prompt: "Find something",
|
|
subagent_type: "explore",
|
|
run_in_background: false,
|
|
load_skills: [],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - model should be resolved via AGENT_MODEL_REQUIREMENTS fallback chain
|
|
expect(promptBody.model).toBeDefined()
|
|
}, { timeout: 20000 })
|
|
|
|
test("agentOverrides model takes priority over matchedAgent.model (#1357)", async () => {
|
|
// given - user configured oracle to use a specific model in oh-my-opencode.json
|
|
const { createDelegateTask } = require("./tools")
|
|
let promptBody: any
|
|
|
|
const mockManager = { launch: async () => ({}) }
|
|
|
|
const promptMock = async (input: any) => {
|
|
promptBody = input.body
|
|
return { data: {} }
|
|
}
|
|
|
|
const mockClient = {
|
|
app: {
|
|
agents: async () => ({
|
|
data: [
|
|
{ name: "oracle", mode: "subagent", model: { providerID: "openai", modelID: "gpt-5.5" } },
|
|
],
|
|
}),
|
|
},
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
get: async () => ({ data: { directory: "/project" } }),
|
|
create: async () => ({ data: { id: "ses_override_model" } }),
|
|
prompt: promptMock,
|
|
promptAsync: promptMock,
|
|
messages: async () => ({
|
|
data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Done" }] }],
|
|
}),
|
|
status: async () => ({ data: { "ses_override_model": { type: "idle" } } }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
agentOverrides: {
|
|
oracle: { model: "anthropic/claude-opus-4-7" },
|
|
},
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - delegating to oracle via subagent_type with user override
|
|
await tool.execute(
|
|
{
|
|
description: "Consult oracle with override",
|
|
prompt: "Review architecture",
|
|
subagent_type: "oracle",
|
|
run_in_background: false,
|
|
load_skills: [],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - user-configured model should take priority over matchedAgent.model
|
|
expect(promptBody.model).toEqual({
|
|
providerID: "anthropic",
|
|
modelID: "claude-opus-4-7",
|
|
})
|
|
}, { timeout: 20000 })
|
|
|
|
test("agentOverrides variant is applied when model is overridden (#1357)", async () => {
|
|
// given - user configured oracle with model and variant
|
|
const { createDelegateTask } = require("./tools")
|
|
let promptBody: any
|
|
|
|
const mockManager = { launch: async () => ({}) }
|
|
|
|
const promptMock = async (input: any) => {
|
|
promptBody = input.body
|
|
return { data: {} }
|
|
}
|
|
|
|
const mockClient = {
|
|
app: {
|
|
agents: async () => ({
|
|
data: [
|
|
{ name: "oracle", mode: "subagent", model: { providerID: "openai", modelID: "gpt-5.5" } },
|
|
],
|
|
}),
|
|
},
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
get: async () => ({ data: { directory: "/project" } }),
|
|
create: async () => ({ data: { id: "ses_variant_test" } }),
|
|
prompt: promptMock,
|
|
promptAsync: promptMock,
|
|
messages: async () => ({
|
|
data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Done" }] }],
|
|
}),
|
|
status: async () => ({ data: { "ses_variant_test": { type: "idle" } } }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
agentOverrides: {
|
|
oracle: { model: "anthropic/claude-opus-4-7", variant: "max" },
|
|
},
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - delegating to oracle via subagent_type with variant override
|
|
await tool.execute(
|
|
{
|
|
description: "Consult oracle with variant",
|
|
prompt: "Review architecture",
|
|
subagent_type: "oracle",
|
|
run_in_background: false,
|
|
load_skills: [],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - user-configured variant should be applied
|
|
expect(promptBody.variant).toBe("max")
|
|
}, { timeout: 20000 })
|
|
|
|
test("fallback chain resolves model when no override and no matchedAgent.model (#1357)", async () => {
|
|
// given - agent registered without model, no override, but AGENT_MODEL_REQUIREMENTS has fallback
|
|
const { createDelegateTask } = require("./tools")
|
|
let promptBody: any
|
|
|
|
const mockManager = { launch: async () => ({}) }
|
|
|
|
const promptMock = async (input: any) => {
|
|
promptBody = input.body
|
|
return { data: {} }
|
|
}
|
|
|
|
const mockClient = {
|
|
app: {
|
|
agents: async () => ({
|
|
data: [
|
|
{ name: "oracle", mode: "subagent" }, // no model field
|
|
],
|
|
}),
|
|
},
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
get: async () => ({ data: { directory: "/project" } }),
|
|
create: async () => ({ data: { id: "ses_fallback_test" } }),
|
|
prompt: promptMock,
|
|
promptAsync: promptMock,
|
|
messages: async () => ({
|
|
data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Done" }] }],
|
|
}),
|
|
status: async () => ({ data: { "ses_fallback_test": { type: "idle" } } }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
// no agentOverrides
|
|
connectedProvidersOverride: TEST_CONNECTED_PROVIDERS,
|
|
availableModelsOverride: createTestAvailableModels(),
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - delegating to oracle with no override and no matchedAgent model
|
|
await tool.execute(
|
|
{
|
|
description: "Consult oracle with fallback",
|
|
prompt: "Review architecture",
|
|
subagent_type: "oracle",
|
|
run_in_background: false,
|
|
load_skills: [],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - should resolve via AGENT_MODEL_REQUIREMENTS fallback chain for oracle
|
|
// oracle fallback chain: gpt-5.5 (openai) > gemini-3.1-pro (google) > claude-opus-4-7 (anthropic)
|
|
// Since openai is in connectedProviders, should resolve to openai/gpt-5.5
|
|
expect(promptBody.model).toBeDefined()
|
|
expect(promptBody.model.providerID).toBe("openai")
|
|
expect(promptBody.model.modelID).toContain("gpt-5.5")
|
|
}, { timeout: 20000 })
|
|
})
|
|
|
|
describe("subagent task permission", () => {
|
|
test("plan subagent should have task permission enabled", async () => {
|
|
//#given - sisyphus delegates to plan agent
|
|
const { createDelegateTask } = require("./tools")
|
|
let promptBody: any
|
|
|
|
const mockManager = { launch: async () => ({}) }
|
|
|
|
const promptMock = async (input: any) => {
|
|
promptBody = input.body
|
|
return { data: {} }
|
|
}
|
|
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [{ name: "plan", mode: "subagent" }] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
get: async () => ({ data: { directory: "/project" } }),
|
|
create: async () => ({ data: { id: "ses_plan_delegate" } }),
|
|
prompt: promptMock,
|
|
promptAsync: promptMock,
|
|
messages: async () => ({
|
|
data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Plan created" }] }]
|
|
}),
|
|
status: async () => ({ data: { "ses_plan_delegate": { type: "idle" } } }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
//#when - sisyphus delegates to plan
|
|
await tool.execute(
|
|
{
|
|
description: "Test plan task permission",
|
|
prompt: "Create a plan",
|
|
subagent_type: "plan",
|
|
run_in_background: false,
|
|
load_skills: [],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
//#then - plan agent should have task permission
|
|
expect(promptBody.tools.task).toBe(true)
|
|
}, { timeout: 20000 })
|
|
|
|
test("prometheus primary agent should not be callable via task", async () => {
|
|
//#given
|
|
const { createDelegateTask } = require("./tools")
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [{ name: "prometheus", mode: "primary" }] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
get: async () => ({ data: { directory: "/project" } }),
|
|
create: async () => ({ data: { id: "ses_prometheus_task" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Plan created" }] }] }),
|
|
status: async () => ({ data: { "ses_prometheus_task": { type: "idle" } } }),
|
|
},
|
|
}
|
|
const tool = createDelegateTask({ manager: { launch: async () => ({}) }, client: mockClient })
|
|
|
|
//#when
|
|
const result = await tool.execute(
|
|
{ description: "Test prometheus task permission", prompt: "Create a plan", subagent_type: "prometheus", run_in_background: false, load_skills: [] },
|
|
{ sessionID: "p", messageID: "m", agent: "sisyphus", abort: new AbortController().signal }
|
|
)
|
|
|
|
//#then
|
|
expect(result).toContain('Cannot delegate to primary agent "prometheus" via task. Select that agent directly instead.')
|
|
}, { timeout: 20000 })
|
|
|
|
test("non-plan subagent should NOT have task permission", async () => {
|
|
//#given - sisyphus delegates to oracle (non-plan)
|
|
const { createDelegateTask } = require("./tools")
|
|
let promptBody: any
|
|
|
|
const mockManager = { launch: async () => ({}) }
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [{ name: "oracle", mode: "subagent" }] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
session: {
|
|
get: async () => ({ data: { directory: "/project" } }),
|
|
create: async () => ({ data: { id: "ses_oracle_no_delegate" } }),
|
|
prompt: async (input: any) => {
|
|
promptBody = input.body
|
|
return { data: {} }
|
|
},
|
|
promptAsync: async (input: any) => {
|
|
promptBody = input.body
|
|
return { data: {} }
|
|
},
|
|
messages: async () => ({
|
|
data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Consultation done" }] }]
|
|
}),
|
|
status: async () => ({ data: { "ses_oracle_no_delegate": { type: "idle" } } }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - sisyphus delegates to oracle
|
|
await tool.execute(
|
|
{
|
|
description: "Test oracle no task permission",
|
|
prompt: "Consult on architecture",
|
|
subagent_type: "oracle",
|
|
run_in_background: false,
|
|
load_skills: [],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - oracle should NOT have task permission
|
|
expect(promptBody.tools.task).toBe(false)
|
|
}, { timeout: 20000 })
|
|
})
|
|
|
|
describe("session title and metadata format (OpenCode compatibility)", () => {
|
|
test("sync session title follows OpenCode format: '{description} (@{agent} subagent)'", async () => {
|
|
// given
|
|
const { createDelegateTask } = require("./tools")
|
|
let createBody: any
|
|
|
|
const mockManager = { launch: async () => ({}) }
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
model: { list: async () => [{ id: SYSTEM_DEFAULT_MODEL }] },
|
|
session: {
|
|
get: async () => ({ data: { directory: "/project" } }),
|
|
create: async (input: any) => {
|
|
createBody = input.body
|
|
return { data: { id: "ses_title_test" } }
|
|
},
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({
|
|
data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "done" }] }]
|
|
}),
|
|
status: async () => ({ data: { "ses_title_test": { type: "idle" } } }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when - sync task with category
|
|
await tool.execute(
|
|
{
|
|
description: "Implement feature X",
|
|
prompt: "Build the feature",
|
|
category: "quick",
|
|
run_in_background: false,
|
|
load_skills: [],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - title should follow OpenCode format
|
|
expect(createBody.title).toBe("Implement feature X (@Sisyphus-Junior subagent)")
|
|
}, { timeout: 10000 })
|
|
|
|
test("sync task output includes <task_metadata> block with session_id", async () => {
|
|
// given
|
|
const { createDelegateTask } = require("./tools")
|
|
|
|
const mockManager = { launch: async () => ({}) }
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
model: { list: async () => [{ id: SYSTEM_DEFAULT_MODEL }] },
|
|
session: {
|
|
get: async () => ({ data: { directory: "/project" } }),
|
|
create: async () => ({ data: { id: "ses_metadata_test" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({
|
|
data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Task completed" }] }]
|
|
}),
|
|
status: async () => ({ data: { "ses_metadata_test": { type: "idle" } } }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when
|
|
const result = await tool.execute(
|
|
{
|
|
description: "Test metadata format",
|
|
prompt: "Do something",
|
|
category: "quick",
|
|
run_in_background: false,
|
|
load_skills: [],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - output should contain <task_metadata> block
|
|
expect(result).toContain("<task_metadata>")
|
|
expect(result).toContain("session_id: ses_metadata_test")
|
|
expect(result).toContain("</task_metadata>")
|
|
}, { timeout: 10000 })
|
|
|
|
test("background task output includes <task_metadata> block with session_id", async () => {
|
|
// given
|
|
const { createDelegateTask } = require("./tools")
|
|
|
|
const mockManager = {
|
|
launch: async () => ({
|
|
id: "bg_meta_test",
|
|
sessionId: "ses_bg_metadata",
|
|
description: "Background metadata test",
|
|
agent: "sisyphus-junior",
|
|
status: "running",
|
|
}),
|
|
}
|
|
const mockClient = {
|
|
app: { agents: async () => ({ data: [] }) },
|
|
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
|
model: { list: async () => [] },
|
|
session: {
|
|
create: async () => ({ data: { id: "test-session" } }),
|
|
prompt: async () => ({ data: {} }),
|
|
promptAsync: async () => ({ data: {} }),
|
|
messages: async () => ({ data: [] }),
|
|
},
|
|
}
|
|
|
|
const tool = createDelegateTask({
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
userCategories: {
|
|
"sisyphus-junior": { model: "anthropic/claude-sonnet-4-6" },
|
|
},
|
|
})
|
|
|
|
const toolContext = {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
}
|
|
|
|
// when
|
|
const result = await tool.execute(
|
|
{
|
|
description: "Background metadata test",
|
|
prompt: "Do something",
|
|
category: "quick",
|
|
run_in_background: true,
|
|
load_skills: [],
|
|
},
|
|
toolContext
|
|
)
|
|
|
|
// then - output should contain <task_metadata> block
|
|
expect(result).toContain("<task_metadata>")
|
|
expect(result).toContain("session_id: ses_bg_metadata")
|
|
expect(result).toContain("</task_metadata>")
|
|
}, { timeout: 10000 })
|
|
})
|
|
})
|