Merge remote-tracking branch 'origin/dev' into opencode/mighty-wolf
This commit is contained in:
@@ -576,6 +576,60 @@ describe("background-agent spawner fallback model promotion", () => {
|
||||
expect(promptCalls).toHaveLength(1)
|
||||
expect(promptCalls[0]?.body?.agent).toBe("sisyphus-junior")
|
||||
})
|
||||
|
||||
test("strips legacy ZWSP-prefixed agent names from persisted background spawn prompt body (GH-3259)", async () => {
|
||||
//#given - persisted spawn input from v3.14.0-v3.16.0 with ZWSP prefix on agent
|
||||
const promptCalls: Array<{ body?: { agent?: string } }> = []
|
||||
|
||||
const client = {
|
||||
session: {
|
||||
get: async () => ({ data: { directory: "/parent/dir" } }),
|
||||
create: async () => ({ data: { id: "ses_child_legacy_zwsp" } }),
|
||||
promptAsync: async (args?: { body?: { agent?: string } }) => {
|
||||
promptCalls.push(args ?? {})
|
||||
return {}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const task = createTask({
|
||||
description: "Legacy ZWSP",
|
||||
prompt: "Do work",
|
||||
agent: "\u200B\u200BHephaestus - Deep Agent",
|
||||
parentSessionID: "ses_parent",
|
||||
parentMessageID: "msg_parent",
|
||||
})
|
||||
|
||||
const item = {
|
||||
task,
|
||||
input: {
|
||||
description: task.description,
|
||||
prompt: task.prompt,
|
||||
agent: task.agent,
|
||||
parentSessionID: task.parentSessionID,
|
||||
parentMessageID: task.parentMessageID,
|
||||
parentModel: task.parentModel,
|
||||
parentAgent: task.parentAgent,
|
||||
model: task.model,
|
||||
},
|
||||
}
|
||||
|
||||
const ctx = {
|
||||
client,
|
||||
directory: "/fallback",
|
||||
concurrencyManager: { release: () => {} },
|
||||
tmuxEnabled: false,
|
||||
onTaskError: () => {},
|
||||
}
|
||||
|
||||
//#when
|
||||
await startTask(item as any, ctx as any)
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
//#then
|
||||
expect(promptCalls).toHaveLength(1)
|
||||
expect(promptCalls[0]?.body?.agent).toBe("Hephaestus - Deep Agent")
|
||||
})
|
||||
})
|
||||
|
||||
describe("background-agent spawner tmux callback ordering", () => {
|
||||
|
||||
@@ -25,8 +25,30 @@ describe("createBuiltinSkills", () => {
|
||||
// then
|
||||
const playwrightSkill = skills.find((s) => s.name === "playwright")
|
||||
const agentBrowserSkill = skills.find((s) => s.name === "agent-browser")
|
||||
const devBrowserSkill = skills.find((s) => s.name === "dev-browser")
|
||||
expect(playwrightSkill).toBeDefined()
|
||||
expect(agentBrowserSkill).toBeUndefined()
|
||||
expect(devBrowserSkill).toBeUndefined()
|
||||
})
|
||||
|
||||
test("returns dev-browser skill when browserProvider is 'dev-browser'", () => {
|
||||
// given
|
||||
const options = { browserProvider: "dev-browser" as const }
|
||||
|
||||
// when
|
||||
const skills = createBuiltinSkills(options)
|
||||
|
||||
// then
|
||||
const skillNames = skills.map((skill) => skill.name)
|
||||
const devBrowserSkill = skills.find((skill) => skill.name === "dev-browser")
|
||||
const playwrightSkill = skills.find((skill) => skill.name === "playwright")
|
||||
const agentBrowserSkill = skills.find((skill) => skill.name === "agent-browser")
|
||||
expect(devBrowserSkill).toBeDefined()
|
||||
expect(devBrowserSkill!.description).toContain("Browser automation")
|
||||
expect(playwrightSkill).toBeUndefined()
|
||||
expect(agentBrowserSkill).toBeUndefined()
|
||||
expect(skillNames).not.toContain("playwright-cli")
|
||||
expect(skills.some((skill) => skill.allowedTools?.includes("Bash(playwright-cli:*)"))).toBe(false)
|
||||
})
|
||||
|
||||
test("returns agent-browser skill when browserProvider is 'agent-browser'", () => {
|
||||
@@ -67,9 +89,10 @@ describe("createBuiltinSkills", () => {
|
||||
// when
|
||||
const defaultSkills = createBuiltinSkills()
|
||||
const agentBrowserSkills = createBuiltinSkills({ browserProvider: "agent-browser" })
|
||||
const devBrowserSkills = createBuiltinSkills({ browserProvider: "dev-browser" })
|
||||
|
||||
// then
|
||||
for (const skills of [defaultSkills, agentBrowserSkills]) {
|
||||
for (const skills of [defaultSkills, agentBrowserSkills, devBrowserSkills]) {
|
||||
expect(skills.find((s) => s.name === "frontend-ui-ux")).toBeDefined()
|
||||
expect(skills.find((s) => s.name === "git-master")).toBeDefined()
|
||||
expect(skills.find((s) => s.name === "review-work")).toBeDefined()
|
||||
@@ -77,16 +100,18 @@ describe("createBuiltinSkills", () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("returns exactly 6 skills regardless of provider", () => {
|
||||
test("returns exactly 5 skills regardless of provider", () => {
|
||||
// given
|
||||
|
||||
// when
|
||||
const defaultSkills = createBuiltinSkills()
|
||||
const agentBrowserSkills = createBuiltinSkills({ browserProvider: "agent-browser" })
|
||||
const devBrowserSkills = createBuiltinSkills({ browserProvider: "dev-browser" })
|
||||
|
||||
// then
|
||||
expect(defaultSkills).toHaveLength(6)
|
||||
expect(agentBrowserSkills).toHaveLength(6)
|
||||
expect(defaultSkills).toHaveLength(5)
|
||||
expect(agentBrowserSkills).toHaveLength(5)
|
||||
expect(devBrowserSkills).toHaveLength(5)
|
||||
})
|
||||
|
||||
test("should exclude playwright when it is in disabledSkills", () => {
|
||||
@@ -100,10 +125,10 @@ describe("createBuiltinSkills", () => {
|
||||
expect(skills.map((s) => s.name)).not.toContain("playwright")
|
||||
expect(skills.map((s) => s.name)).toContain("frontend-ui-ux")
|
||||
expect(skills.map((s) => s.name)).toContain("git-master")
|
||||
expect(skills.map((s) => s.name)).toContain("dev-browser")
|
||||
expect(skills.map((s) => s.name)).not.toContain("dev-browser")
|
||||
expect(skills.map((s) => s.name)).toContain("review-work")
|
||||
expect(skills.map((s) => s.name)).toContain("ai-slop-remover")
|
||||
expect(skills.length).toBe(5)
|
||||
expect(skills.length).toBe(4)
|
||||
})
|
||||
|
||||
test("should exclude multiple skills when they are in disabledSkills", () => {
|
||||
@@ -117,17 +142,15 @@ describe("createBuiltinSkills", () => {
|
||||
expect(skills.map((s) => s.name)).not.toContain("playwright")
|
||||
expect(skills.map((s) => s.name)).not.toContain("git-master")
|
||||
expect(skills.map((s) => s.name)).toContain("frontend-ui-ux")
|
||||
expect(skills.map((s) => s.name)).toContain("dev-browser")
|
||||
expect(skills.map((s) => s.name)).not.toContain("dev-browser")
|
||||
expect(skills.map((s) => s.name)).toContain("review-work")
|
||||
expect(skills.map((s) => s.name)).toContain("ai-slop-remover")
|
||||
expect(skills.length).toBe(4)
|
||||
expect(skills.length).toBe(3)
|
||||
})
|
||||
|
||||
test("should return an empty array when all skills are disabled", () => {
|
||||
// #given
|
||||
const options = {
|
||||
disabledSkills: new Set(["playwright", "frontend-ui-ux", "git-master", "dev-browser", "review-work", "ai-slop-remover"]),
|
||||
}
|
||||
const options = { disabledSkills: new Set(["playwright", "frontend-ui-ux", "git-master", "review-work", "ai-slop-remover"]) }
|
||||
|
||||
// #when
|
||||
const skills = createBuiltinSkills(options)
|
||||
@@ -144,7 +167,7 @@ describe("createBuiltinSkills", () => {
|
||||
const skills = createBuiltinSkills(options)
|
||||
|
||||
// #then
|
||||
expect(skills.length).toBe(6)
|
||||
expect(skills.length).toBe(5)
|
||||
})
|
||||
|
||||
test("review-work skill has correct structure", () => {
|
||||
|
||||
@@ -21,15 +21,17 @@ export function createBuiltinSkills(options: CreateBuiltinSkillsOptions = {}): B
|
||||
const { browserProvider = "playwright", disabledSkills } = options
|
||||
|
||||
let browserSkill: BuiltinSkill
|
||||
if (browserProvider === "agent-browser") {
|
||||
browserSkill = agentBrowserSkill
|
||||
} else if (browserProvider === "playwright-cli") {
|
||||
browserSkill = playwrightCliSkill
|
||||
} else {
|
||||
browserSkill = playwrightSkill
|
||||
}
|
||||
if (browserProvider === "agent-browser") {
|
||||
browserSkill = agentBrowserSkill
|
||||
} else if (browserProvider === "dev-browser") {
|
||||
browserSkill = devBrowserSkill
|
||||
} else if (browserProvider === "playwright-cli") {
|
||||
browserSkill = playwrightCliSkill
|
||||
} else {
|
||||
browserSkill = playwrightSkill
|
||||
}
|
||||
|
||||
const skills = [browserSkill, frontendUiUxSkill, gitMasterSkill, devBrowserSkill, reviewWorkSkill, aiSlopRemoverSkill]
|
||||
const skills = [browserSkill, frontendUiUxSkill, gitMasterSkill, reviewWorkSkill, aiSlopRemoverSkill]
|
||||
|
||||
if (!disabledSkills) {
|
||||
return skills
|
||||
|
||||
@@ -247,4 +247,18 @@ describe("claude-code-session-state", () => {
|
||||
expect(getSessionAgent(sessionID)).toBe(newAgent)
|
||||
})
|
||||
})
|
||||
|
||||
describe("backward compatibility", () => {
|
||||
test("strips legacy ZWSP-prefixed agent names from persisted session state (GH-3259)", () => {
|
||||
// given - persisted session payload from v3.14.0-v3.16.0 with ZWSP prefix
|
||||
const sessionID = "test-session-legacy-zwsp"
|
||||
const legacyAgent = "\u200B\u200BHephaestus - Deep Agent"
|
||||
|
||||
// when
|
||||
setSessionAgent(sessionID, legacyAgent)
|
||||
|
||||
// then
|
||||
expect(getSessionAgent(sessionID)).toBe("Hephaestus - Deep Agent")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe, test, expect, mock, beforeEach, spyOn, afterAll } from 'bun:t
|
||||
import type { TmuxConfig } from '../../config/schema'
|
||||
import type { WindowState, PaneAction } from './types'
|
||||
import type { ActionResult, ExecuteContext } from './action-executor'
|
||||
import type { TmuxUtilDeps } from './manager'
|
||||
import type { TmuxSessionManager as TmuxSessionManagerType, TmuxUtilDeps } from './manager'
|
||||
import * as sharedModule from '../../shared'
|
||||
|
||||
type ExecuteActionsResult = {
|
||||
@@ -287,20 +287,95 @@ describe('TmuxSessionManager', () => {
|
||||
|
||||
test('falls back to default port when serverUrl has port 0', async () => {
|
||||
// given
|
||||
mockIsInsideTmux.mockReturnValue(true)
|
||||
const { TmuxSessionManager } = await import('./manager')
|
||||
const ctx = {
|
||||
...createMockContext(),
|
||||
serverUrl: new URL('http://127.0.0.1:0/'),
|
||||
}
|
||||
const config = createTmuxConfig({ enabled: true,
|
||||
layout: 'main-vertical',
|
||||
main_pane_size: 60,
|
||||
main_pane_min_width: 80,
|
||||
agent_pane_min_width: 40, })
|
||||
const previousOpenCodePort = process.env.OPENCODE_PORT
|
||||
delete process.env.OPENCODE_PORT
|
||||
let manager: TmuxSessionManagerType | undefined
|
||||
try {
|
||||
mockIsInsideTmux.mockReturnValue(true)
|
||||
const { TmuxSessionManager } = await import('./manager')
|
||||
const ctx = {
|
||||
...createMockContext(),
|
||||
serverUrl: new URL('http://127.0.0.1:0/'),
|
||||
}
|
||||
const config = createTmuxConfig({ enabled: true,
|
||||
layout: 'main-vertical',
|
||||
main_pane_size: 60,
|
||||
main_pane_min_width: 80,
|
||||
agent_pane_min_width: 40, })
|
||||
|
||||
// when
|
||||
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
|
||||
// when
|
||||
manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
|
||||
} finally {
|
||||
if (previousOpenCodePort === undefined) {
|
||||
delete process.env.OPENCODE_PORT
|
||||
} else {
|
||||
process.env.OPENCODE_PORT = previousOpenCodePort
|
||||
}
|
||||
}
|
||||
|
||||
// then
|
||||
expect((manager as any).serverUrl).toBe('http://localhost:4096')
|
||||
})
|
||||
|
||||
test('falls back to configured OPENCODE_PORT when serverUrl has port 0', async () => {
|
||||
// given
|
||||
const previousOpenCodePort = process.env.OPENCODE_PORT
|
||||
process.env.OPENCODE_PORT = '5678'
|
||||
let manager: TmuxSessionManagerType | undefined
|
||||
try {
|
||||
mockIsInsideTmux.mockReturnValue(true)
|
||||
const { TmuxSessionManager } = await import('./manager')
|
||||
const ctx = {
|
||||
...createMockContext(),
|
||||
serverUrl: new URL('http://127.0.0.1:0/'),
|
||||
}
|
||||
const config = createTmuxConfig({ enabled: true,
|
||||
layout: 'main-vertical',
|
||||
main_pane_size: 60,
|
||||
main_pane_min_width: 80,
|
||||
agent_pane_min_width: 40, })
|
||||
|
||||
// when
|
||||
manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
|
||||
} finally {
|
||||
if (previousOpenCodePort === undefined) {
|
||||
delete process.env.OPENCODE_PORT
|
||||
} else {
|
||||
process.env.OPENCODE_PORT = previousOpenCodePort
|
||||
}
|
||||
}
|
||||
|
||||
// then
|
||||
expect((manager as any).serverUrl).toBe('http://localhost:5678')
|
||||
})
|
||||
|
||||
test('ignores invalid OPENCODE_PORT when serverUrl has port 0', async () => {
|
||||
// given
|
||||
const previousOpenCodePort = process.env.OPENCODE_PORT
|
||||
process.env.OPENCODE_PORT = 'not-a-port'
|
||||
let manager: TmuxSessionManagerType | undefined
|
||||
try {
|
||||
mockIsInsideTmux.mockReturnValue(true)
|
||||
const { TmuxSessionManager } = await import('./manager')
|
||||
const ctx = {
|
||||
...createMockContext(),
|
||||
serverUrl: new URL('http://127.0.0.1:0/'),
|
||||
}
|
||||
const config = createTmuxConfig({ enabled: true,
|
||||
layout: 'main-vertical',
|
||||
main_pane_size: 60,
|
||||
main_pane_min_width: 80,
|
||||
agent_pane_min_width: 40, })
|
||||
|
||||
// when
|
||||
manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
|
||||
} finally {
|
||||
if (previousOpenCodePort === undefined) {
|
||||
delete process.env.OPENCODE_PORT
|
||||
} else {
|
||||
process.env.OPENCODE_PORT = previousOpenCodePort
|
||||
}
|
||||
}
|
||||
|
||||
// then
|
||||
expect((manager as any).serverUrl).toBe('http://localhost:4096')
|
||||
@@ -1989,7 +2064,7 @@ describe('TmuxSessionManager', () => {
|
||||
const cleanupPromise = manager.cleanup()
|
||||
|
||||
// then
|
||||
await expect(cleanupPromise).resolves.toBeUndefined()
|
||||
expect(await cleanupPromise).toBeUndefined()
|
||||
expect(mockKillTmuxSessionIfExists).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -72,7 +72,11 @@ export class TmuxSessionManager {
|
||||
this.client = ctx.client
|
||||
this.tmuxConfig = tmuxConfig
|
||||
this.deps = deps
|
||||
const defaultPort = process.env.OPENCODE_PORT ?? "4096"
|
||||
const configuredPort = process.env.OPENCODE_PORT
|
||||
const parsedPort = configuredPort ? Number(configuredPort) : 4096
|
||||
const defaultPort = Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535
|
||||
? String(parsedPort)
|
||||
: "4096"
|
||||
const fallbackUrl = `http://localhost:${defaultPort}`
|
||||
const rawServerUrl = ctx.serverUrl?.toString()
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user