fix: remove dead-code files accidentally resurrected by previous commit
The previous commit (2dfa6336f) used 'git add -A' which swept up files that prior commits had intentionally deleted but were still present untracked in the local workspace. This commit re-deletes them to match the upstream intent. Files removed (each was previously deleted in the cited commit): - drafts/gpt-5-5/{README,deep,hephaestus,oracle,sisyphus-junior,sisyphus}.md (deleted in40ded422cchore(drafts): remove stale gpt-5.5 prompt drafts) - src/plugin-dispose.{ts,test.ts} (deleted ine2f5c0d36refactor(plugin): remove orphaned createPluginDispose) - src/features/tmux-subagent/{cleanup,session-created-handler,session-deleted-handler}.ts (deleted in7a7926f22chore(tmux-subagent): remove dead event-handler modules) - src/tools/delegate-task/{model-string-parser,resolve-call-id,resolve-call-id.test}.ts (model-string-parser deleted in db056346d; resolve-call-id was scratch) - src/__debug-test.test.ts (debug scratch never intended for git) Typecheck + model-requirements tests still pass. The legitimate metis + AGENTS.md edits from the previous commit remain on dev.
This commit is contained in:
@@ -1,235 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"
|
||||
|
||||
const mockInitConfigContext = mock(() => {})
|
||||
const mockInjectServerAuthIntoClient = mock(() => {})
|
||||
const mockLogLegacyPluginStartupWarning = mock(() => {})
|
||||
const mockLoadPluginConfig = mock(() => ({}))
|
||||
const mockIsTmuxIntegrationEnabled = mock(() => false)
|
||||
const mockCreateRuntimeTmuxConfig = mock(() => ({
|
||||
enabled: false,
|
||||
layout: "tiled" as const,
|
||||
main_pane_size: 60,
|
||||
main_pane_min_width: 80,
|
||||
agent_pane_min_width: 40,
|
||||
isolation: "inline" as const,
|
||||
}))
|
||||
const mockCreateManagers = mock(() => ({
|
||||
backgroundManager: { shutdown: async () => {} },
|
||||
skillMcpManager: { disconnectAll: async () => {} },
|
||||
configHandler: async () => {},
|
||||
}))
|
||||
const mockCreateTools = mock(async () => ({
|
||||
mergedSkills: [],
|
||||
availableSkills: [],
|
||||
filteredTools: {},
|
||||
}))
|
||||
const mockCreateHooks = mock(() => ({
|
||||
disposeHooks: () => {},
|
||||
compactionContextInjector: undefined,
|
||||
compactionTodoPreserver: undefined,
|
||||
claudeCodeHooks: undefined,
|
||||
}))
|
||||
const mockCreatePluginInterface = mock(() => ({}))
|
||||
const mockCreatePluginPostHog = mock(() => ({
|
||||
trackActive: () => {
|
||||
throw new Error("telemetry failed")
|
||||
},
|
||||
capture: mock(() => {}),
|
||||
captureException: mock(() => {}),
|
||||
shutdown: mock(async () => {}),
|
||||
}))
|
||||
const mockGetPostHogDistinctId = mock(() => "plugin-distinct-id")
|
||||
|
||||
function installModuleMocks(): void {
|
||||
mock.module("./cli/config-manager/config-context", () => ({
|
||||
initConfigContext: mockInitConfigContext,
|
||||
}))
|
||||
mock.module("./shared/external-plugin-detector", () => ({
|
||||
detectExternalSkillPlugin: mock(() => ({ detected: false, pluginName: null })),
|
||||
getSkillPluginConflictWarning: mock(() => ""),
|
||||
}))
|
||||
mock.module("./shared", () => ({
|
||||
injectServerAuthIntoClient: mockInjectServerAuthIntoClient,
|
||||
log: mock(() => {}),
|
||||
logLegacyPluginStartupWarning: mockLogLegacyPluginStartupWarning,
|
||||
}))
|
||||
mock.module("./plugin-config", () => ({
|
||||
loadPluginConfig: mockLoadPluginConfig,
|
||||
}))
|
||||
mock.module("./create-runtime-tmux-config", () => ({
|
||||
createRuntimeTmuxConfig: mockCreateRuntimeTmuxConfig,
|
||||
isTmuxIntegrationEnabled: mockIsTmuxIntegrationEnabled,
|
||||
}))
|
||||
mock.module("./create-managers", () => ({
|
||||
createManagers: mockCreateManagers,
|
||||
}))
|
||||
mock.module("./create-tools", () => ({
|
||||
createTools: mockCreateTools,
|
||||
}))
|
||||
mock.module("./create-hooks", () => ({
|
||||
createHooks: mockCreateHooks,
|
||||
}))
|
||||
mock.module("./plugin-interface", () => ({
|
||||
createPluginInterface: mockCreatePluginInterface,
|
||||
}))
|
||||
mock.module("./plugin-state", () => ({
|
||||
createModelCacheState: mock(() => ({})),
|
||||
}))
|
||||
mock.module("./shared/first-message-variant", () => ({
|
||||
createFirstMessageVariantGate: mock(() => ({
|
||||
shouldOverride: () => false,
|
||||
markApplied: () => {},
|
||||
markSessionCreated: () => {},
|
||||
clear: () => {},
|
||||
})),
|
||||
}))
|
||||
mock.module("./openclaw", () => ({
|
||||
initializeOpenClaw: mock(async () => {}),
|
||||
}))
|
||||
mock.module("./tools/interactive-bash", () => ({
|
||||
interactive_bash: {},
|
||||
startBackgroundCheck: mock(() => {}),
|
||||
}))
|
||||
mock.module("./tools/lsp/client", () => ({
|
||||
lspManager: {
|
||||
getClient: mock(async () => ({
|
||||
diagnostics: mock(async () => ({ items: [] })),
|
||||
})),
|
||||
stopAll: mock(async () => {}),
|
||||
releaseClient: mock(() => {}),
|
||||
cleanupTempDirectoryClients: mock(async () => {}),
|
||||
},
|
||||
}))
|
||||
mock.module("./shared/posthog", () => ({
|
||||
createPluginPostHog: mockCreatePluginPostHog,
|
||||
getPostHogDistinctId: mockGetPostHogDistinctId,
|
||||
}))
|
||||
mock.module("./shared/posthog-activity-state", () => ({
|
||||
getPluginLoadedCaptureState: () => ({
|
||||
dayUTC: "2026-04-18",
|
||||
capturePluginLoaded: true,
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
describe("oh-my-openagent telemetry isolation", () => {
|
||||
beforeEach(() => {
|
||||
mock.restore()
|
||||
installModuleMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
})
|
||||
|
||||
it("does not crash plugin load when telemetry throws", async () => {
|
||||
// given
|
||||
const { default: plugin } = await import(`./index?telemetry=${Date.now()}-${Math.random()}`)
|
||||
|
||||
// when
|
||||
const result = await plugin.server({
|
||||
directory: "/tmp/project",
|
||||
client: {},
|
||||
} as Parameters<typeof plugin.server>[0])
|
||||
|
||||
// then
|
||||
expect(typeof result).toBe("object")
|
||||
expect(result).not.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("oh-my-openagent plugin_loaded daily dedupe", () => {
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
})
|
||||
|
||||
async function loadPluginWithMocks(
|
||||
captureMock: ReturnType<typeof mock>,
|
||||
pluginLoadedState:
|
||||
| { dayUTC: string; capturePluginLoaded: boolean }
|
||||
| { throwError: true },
|
||||
): Promise<typeof import("./index").default> {
|
||||
mock.restore()
|
||||
installModuleMocks()
|
||||
mock.module("./shared/posthog", () => ({
|
||||
createPluginPostHog: () => ({
|
||||
trackActive: () => {},
|
||||
capture: captureMock,
|
||||
captureException: mock(() => {}),
|
||||
shutdown: mock(async () => {}),
|
||||
}),
|
||||
getPostHogDistinctId: mockGetPostHogDistinctId,
|
||||
}))
|
||||
mock.module("./shared/posthog-activity-state", () => ({
|
||||
getPluginLoadedCaptureState: () => {
|
||||
if ("throwError" in pluginLoadedState) {
|
||||
throw new Error("activity-state read failed")
|
||||
}
|
||||
return pluginLoadedState
|
||||
},
|
||||
}))
|
||||
const { default: plugin } = await import(
|
||||
`./index?telemetry-dedupe=${Date.now()}-${Math.random()}`
|
||||
)
|
||||
return plugin
|
||||
}
|
||||
|
||||
it("emits plugin_loaded capture when capturePluginLoaded is true", async () => {
|
||||
// given
|
||||
const captureMock = mock(() => {})
|
||||
const plugin = await loadPluginWithMocks(captureMock, {
|
||||
dayUTC: "2026-04-18",
|
||||
capturePluginLoaded: true,
|
||||
})
|
||||
|
||||
// when
|
||||
await plugin.server({
|
||||
directory: "/tmp/project",
|
||||
client: {},
|
||||
} as Parameters<typeof plugin.server>[0])
|
||||
|
||||
// then
|
||||
expect(captureMock).toHaveBeenCalledTimes(1)
|
||||
const [firstCall] = captureMock.mock.calls
|
||||
const [capturePayload] = firstCall as unknown as [
|
||||
{ event: string; distinctId: string },
|
||||
]
|
||||
expect(capturePayload?.event).toBe("plugin_loaded")
|
||||
expect(capturePayload?.distinctId).toBe("plugin-distinct-id")
|
||||
})
|
||||
|
||||
it("skips plugin_loaded capture when capturePluginLoaded is false", async () => {
|
||||
// given
|
||||
const captureMock = mock(() => {})
|
||||
const plugin = await loadPluginWithMocks(captureMock, {
|
||||
dayUTC: "2026-04-18",
|
||||
capturePluginLoaded: false,
|
||||
})
|
||||
|
||||
// when
|
||||
await plugin.server({
|
||||
directory: "/tmp/project",
|
||||
client: {},
|
||||
} as Parameters<typeof plugin.server>[0])
|
||||
|
||||
// then
|
||||
expect(captureMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("skips plugin_loaded capture when getPluginLoadedCaptureState throws", async () => {
|
||||
// given
|
||||
const captureMock = mock(() => {})
|
||||
const plugin = await loadPluginWithMocks(captureMock, { throwError: true })
|
||||
|
||||
// when
|
||||
const result = await plugin.server({
|
||||
directory: "/tmp/project",
|
||||
client: {},
|
||||
} as Parameters<typeof plugin.server>[0])
|
||||
|
||||
// then
|
||||
expect(captureMock).not.toHaveBeenCalled()
|
||||
expect(typeof result).toBe("object")
|
||||
expect(result).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,42 +0,0 @@
|
||||
import type { TmuxConfig } from "../../config/schema"
|
||||
import { log } from "../../shared"
|
||||
import type { TrackedSession } from "./types"
|
||||
import { queryWindowState } from "./pane-state-querier"
|
||||
import { executeAction } from "./action-executor"
|
||||
|
||||
export async function cleanupTmuxSessions(params: {
|
||||
tmuxConfig: TmuxConfig
|
||||
serverUrl: string
|
||||
sourcePaneId: string | undefined
|
||||
sessions: Map<string, TrackedSession>
|
||||
stopPolling: () => void
|
||||
}): Promise<void> {
|
||||
params.stopPolling()
|
||||
|
||||
if (params.sessions.size === 0) {
|
||||
log("[tmux-session-manager] cleanup complete")
|
||||
return
|
||||
}
|
||||
|
||||
log("[tmux-session-manager] closing all panes", { count: params.sessions.size })
|
||||
const state = params.sourcePaneId ? await queryWindowState(params.sourcePaneId) : null
|
||||
|
||||
if (state) {
|
||||
const closePromises = Array.from(params.sessions.values()).map((tracked) =>
|
||||
executeAction(
|
||||
{ type: "close", paneId: tracked.paneId, sessionId: tracked.sessionId },
|
||||
{ config: params.tmuxConfig, serverUrl: params.serverUrl, windowState: state },
|
||||
).catch((error) =>
|
||||
log("[tmux-session-manager] cleanup error for pane", {
|
||||
paneId: tracked.paneId,
|
||||
error: String(error),
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
await Promise.all(closePromises)
|
||||
}
|
||||
|
||||
params.sessions.clear()
|
||||
log("[tmux-session-manager] cleanup complete")
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
import type { TmuxConfig } from "../../config/schema"
|
||||
import type { CapacityConfig, TrackedSession } from "./types"
|
||||
import { log } from "../../shared"
|
||||
import { queryWindowState } from "./pane-state-querier"
|
||||
import { decideSpawnActions, type SessionMapping } from "./decision-engine"
|
||||
import { executeActions } from "./action-executor"
|
||||
import type { SessionCreatedEvent } from "./session-created-event"
|
||||
import { createTrackedSession } from "./tracked-session-state"
|
||||
|
||||
type OpencodeClient = PluginInput["client"]
|
||||
|
||||
export interface SessionCreatedHandlerDeps {
|
||||
client: OpencodeClient
|
||||
tmuxConfig: TmuxConfig
|
||||
serverUrl: string
|
||||
sourcePaneId: string | undefined
|
||||
sessions: Map<string, TrackedSession>
|
||||
pendingSessions: Set<string>
|
||||
isInsideTmux: () => boolean
|
||||
isEnabled: () => boolean
|
||||
getCapacityConfig: () => CapacityConfig
|
||||
getSessionMappings: () => SessionMapping[]
|
||||
waitForSessionReady: (sessionId: string) => Promise<boolean>
|
||||
startPolling: () => void
|
||||
}
|
||||
|
||||
export async function handleSessionCreated(
|
||||
deps: SessionCreatedHandlerDeps,
|
||||
event: SessionCreatedEvent,
|
||||
): Promise<void> {
|
||||
const enabled = deps.isEnabled()
|
||||
log("[tmux-session-manager] onSessionCreated called", {
|
||||
enabled,
|
||||
tmuxConfigEnabled: deps.tmuxConfig.enabled,
|
||||
isInsideTmux: deps.isInsideTmux(),
|
||||
eventType: event.type,
|
||||
infoId: event.properties?.info?.id,
|
||||
infoParentID: event.properties?.info?.parentID,
|
||||
})
|
||||
|
||||
if (!enabled) return
|
||||
if (event.type !== "session.created") return
|
||||
|
||||
const info = event.properties?.info
|
||||
if (!info?.id || !info?.parentID) return
|
||||
|
||||
const sessionId = info.id
|
||||
const title = info.title ?? "Subagent"
|
||||
|
||||
if (deps.sessions.has(sessionId) || deps.pendingSessions.has(sessionId)) {
|
||||
log("[tmux-session-manager] session already tracked or pending", { sessionId })
|
||||
return
|
||||
}
|
||||
|
||||
if (!deps.sourcePaneId) {
|
||||
log("[tmux-session-manager] no source pane id")
|
||||
return
|
||||
}
|
||||
|
||||
deps.pendingSessions.add(sessionId)
|
||||
|
||||
try {
|
||||
const state = await queryWindowState(deps.sourcePaneId)
|
||||
if (!state) {
|
||||
log("[tmux-session-manager] failed to query window state")
|
||||
return
|
||||
}
|
||||
|
||||
log("[tmux-session-manager] window state queried", {
|
||||
windowWidth: state.windowWidth,
|
||||
mainPane: state.mainPane?.paneId,
|
||||
agentPaneCount: state.agentPanes.length,
|
||||
agentPanes: state.agentPanes.map((p) => p.paneId),
|
||||
})
|
||||
|
||||
const decision = decideSpawnActions(
|
||||
state,
|
||||
sessionId,
|
||||
title,
|
||||
deps.getCapacityConfig(),
|
||||
deps.getSessionMappings(),
|
||||
)
|
||||
|
||||
log("[tmux-session-manager] spawn decision", {
|
||||
canSpawn: decision.canSpawn,
|
||||
reason: decision.reason,
|
||||
actionCount: decision.actions.length,
|
||||
actions: decision.actions.map((a) => {
|
||||
if (a.type === "close") return { type: "close", paneId: a.paneId }
|
||||
if (a.type === "replace") {
|
||||
return { type: "replace", paneId: a.paneId, newSessionId: a.newSessionId }
|
||||
}
|
||||
return { type: "spawn", sessionId: a.sessionId }
|
||||
}),
|
||||
})
|
||||
|
||||
if (!decision.canSpawn) {
|
||||
log("[tmux-session-manager] cannot spawn", { reason: decision.reason })
|
||||
return
|
||||
}
|
||||
|
||||
const result = await executeActions(decision.actions, {
|
||||
config: deps.tmuxConfig,
|
||||
serverUrl: deps.serverUrl,
|
||||
windowState: state,
|
||||
})
|
||||
|
||||
for (const { action, result: actionResult } of result.results) {
|
||||
if (action.type === "close" && actionResult.success) {
|
||||
deps.sessions.delete(action.sessionId)
|
||||
log("[tmux-session-manager] removed closed session from cache", {
|
||||
sessionId: action.sessionId,
|
||||
})
|
||||
}
|
||||
if (action.type === "replace" && actionResult.success) {
|
||||
deps.sessions.delete(action.oldSessionId)
|
||||
log("[tmux-session-manager] removed replaced session from cache", {
|
||||
oldSessionId: action.oldSessionId,
|
||||
newSessionId: action.newSessionId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.success || !result.spawnedPaneId) {
|
||||
log("[tmux-session-manager] spawn failed", {
|
||||
success: result.success,
|
||||
results: result.results.map((r) => ({
|
||||
type: r.action.type,
|
||||
success: r.result.success,
|
||||
error: r.result.error,
|
||||
})),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const sessionReady = await deps.waitForSessionReady(sessionId)
|
||||
if (!sessionReady) {
|
||||
log("[tmux-session-manager] session not ready after timeout, closing spawned pane", {
|
||||
sessionId,
|
||||
paneId: result.spawnedPaneId,
|
||||
})
|
||||
|
||||
await executeActions(
|
||||
[{ type: "close", paneId: result.spawnedPaneId, sessionId }],
|
||||
{
|
||||
config: deps.tmuxConfig,
|
||||
serverUrl: deps.serverUrl,
|
||||
windowState: state,
|
||||
},
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
deps.sessions.set(
|
||||
sessionId,
|
||||
createTrackedSession({
|
||||
sessionId,
|
||||
paneId: result.spawnedPaneId,
|
||||
description: title,
|
||||
}),
|
||||
)
|
||||
|
||||
log("[tmux-session-manager] pane spawned and tracked", {
|
||||
sessionId,
|
||||
paneId: result.spawnedPaneId,
|
||||
sessionReady,
|
||||
})
|
||||
|
||||
deps.startPolling()
|
||||
} finally {
|
||||
deps.pendingSessions.delete(sessionId)
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import type { TmuxConfig } from "../../config/schema"
|
||||
import type { TrackedSession } from "./types"
|
||||
import { log } from "../../shared"
|
||||
import { queryWindowState } from "./pane-state-querier"
|
||||
import { decideCloseAction, type SessionMapping } from "./decision-engine"
|
||||
import { executeAction } from "./action-executor"
|
||||
|
||||
export interface SessionDeletedHandlerDeps {
|
||||
tmuxConfig: TmuxConfig
|
||||
serverUrl: string
|
||||
sourcePaneId: string | undefined
|
||||
sessions: Map<string, TrackedSession>
|
||||
isEnabled: () => boolean
|
||||
getSessionMappings: () => SessionMapping[]
|
||||
stopPolling: () => void
|
||||
}
|
||||
|
||||
export async function handleSessionDeleted(
|
||||
deps: SessionDeletedHandlerDeps,
|
||||
event: { sessionID: string },
|
||||
): Promise<void> {
|
||||
if (!deps.isEnabled()) return
|
||||
if (!deps.sourcePaneId) return
|
||||
|
||||
const tracked = deps.sessions.get(event.sessionID)
|
||||
if (!tracked) return
|
||||
|
||||
log("[tmux-session-manager] onSessionDeleted", { sessionId: event.sessionID })
|
||||
|
||||
const state = await queryWindowState(deps.sourcePaneId)
|
||||
if (!state) {
|
||||
deps.sessions.delete(event.sessionID)
|
||||
return
|
||||
}
|
||||
|
||||
const closeAction = decideCloseAction(state, event.sessionID, deps.getSessionMappings())
|
||||
if (closeAction) {
|
||||
await executeAction(closeAction, {
|
||||
config: deps.tmuxConfig,
|
||||
serverUrl: deps.serverUrl,
|
||||
windowState: state,
|
||||
})
|
||||
}
|
||||
|
||||
deps.sessions.delete(event.sessionID)
|
||||
|
||||
if (deps.sessions.size === 0) {
|
||||
deps.stopPolling()
|
||||
}
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
import { describe, expect, spyOn, test } from "bun:test"
|
||||
|
||||
import { disposeCreatedHooks } from "./create-hooks"
|
||||
import { createPluginDispose } from "./plugin-dispose"
|
||||
|
||||
describe("createPluginDispose", () => {
|
||||
test("#given plugin with active managers and hooks #when dispose() is called #then backgroundManager.shutdown() is called", async () => {
|
||||
// given
|
||||
const backgroundManager = {
|
||||
shutdown: async (): Promise<void> => {},
|
||||
}
|
||||
const skillMcpManager = {
|
||||
disconnectAll: async (): Promise<void> => {},
|
||||
}
|
||||
const lspManager = {
|
||||
stopAll: async (): Promise<void> => {},
|
||||
}
|
||||
const shutdownSpy = spyOn(backgroundManager, "shutdown")
|
||||
const dispose = createPluginDispose({
|
||||
backgroundManager,
|
||||
skillMcpManager,
|
||||
lspManager,
|
||||
disposeHooks: (): void => {},
|
||||
})
|
||||
|
||||
// when
|
||||
await dispose()
|
||||
|
||||
// then
|
||||
expect(shutdownSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test("#given plugin with active MCP connections #when dispose() is called #then skillMcpManager.disconnectAll() is called", async () => {
|
||||
// given
|
||||
const backgroundManager = {
|
||||
shutdown: async (): Promise<void> => {},
|
||||
}
|
||||
const skillMcpManager = {
|
||||
disconnectAll: async (): Promise<void> => {},
|
||||
}
|
||||
const lspManager = {
|
||||
stopAll: async (): Promise<void> => {},
|
||||
}
|
||||
const disconnectAllSpy = spyOn(skillMcpManager, "disconnectAll")
|
||||
const dispose = createPluginDispose({
|
||||
backgroundManager,
|
||||
skillMcpManager,
|
||||
lspManager,
|
||||
disposeHooks: (): void => {},
|
||||
})
|
||||
|
||||
// when
|
||||
await dispose()
|
||||
|
||||
// then
|
||||
expect(disconnectAllSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test("#given plugin with hooks that have dispose #when dispose() is called #then each hook's dispose is called", async () => {
|
||||
// given
|
||||
const claudeCodeHooks = {
|
||||
dispose: (): void => {},
|
||||
}
|
||||
const commentChecker = {
|
||||
dispose: (): void => {},
|
||||
}
|
||||
const runtimeFallback = {
|
||||
dispose: (): void => {},
|
||||
}
|
||||
const todoContinuationEnforcer = {
|
||||
dispose: (): void => {},
|
||||
}
|
||||
const autoSlashCommand = {
|
||||
dispose: (): void => {},
|
||||
}
|
||||
const lspManager = {
|
||||
stopAll: async (): Promise<void> => {},
|
||||
}
|
||||
const claudeCodeHooksDisposeSpy = spyOn(claudeCodeHooks, "dispose")
|
||||
const commentCheckerDisposeSpy = spyOn(commentChecker, "dispose")
|
||||
const runtimeFallbackDisposeSpy = spyOn(runtimeFallback, "dispose")
|
||||
const todoContinuationEnforcerDisposeSpy = spyOn(todoContinuationEnforcer, "dispose")
|
||||
const autoSlashCommandDisposeSpy = spyOn(autoSlashCommand, "dispose")
|
||||
const dispose = createPluginDispose({
|
||||
backgroundManager: {
|
||||
shutdown: async (): Promise<void> => {},
|
||||
},
|
||||
skillMcpManager: {
|
||||
disconnectAll: async (): Promise<void> => {},
|
||||
},
|
||||
lspManager,
|
||||
disposeHooks: (): void => {
|
||||
disposeCreatedHooks({
|
||||
claudeCodeHooks,
|
||||
commentChecker,
|
||||
runtimeFallback,
|
||||
todoContinuationEnforcer,
|
||||
autoSlashCommand,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
// when
|
||||
await dispose()
|
||||
|
||||
// then
|
||||
expect(claudeCodeHooksDisposeSpy).toHaveBeenCalledTimes(1)
|
||||
expect(commentCheckerDisposeSpy).toHaveBeenCalledTimes(1)
|
||||
expect(runtimeFallbackDisposeSpy).toHaveBeenCalledTimes(1)
|
||||
expect(todoContinuationEnforcerDisposeSpy).toHaveBeenCalledTimes(1)
|
||||
expect(autoSlashCommandDisposeSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test("#given dispose already called #when dispose() called again #then no errors", async () => {
|
||||
// given
|
||||
const backgroundManager = {
|
||||
shutdown: async (): Promise<void> => {},
|
||||
}
|
||||
const skillMcpManager = {
|
||||
disconnectAll: async (): Promise<void> => {},
|
||||
}
|
||||
const lspManager = {
|
||||
stopAll: async (): Promise<void> => {},
|
||||
}
|
||||
const disposeHooks = {
|
||||
run: (): void => {},
|
||||
}
|
||||
const shutdownSpy = spyOn(backgroundManager, "shutdown")
|
||||
const disconnectAllSpy = spyOn(skillMcpManager, "disconnectAll")
|
||||
const stopAllSpy = spyOn(lspManager, "stopAll")
|
||||
const disposeHooksSpy = spyOn(disposeHooks, "run")
|
||||
const dispose = createPluginDispose({
|
||||
backgroundManager,
|
||||
skillMcpManager,
|
||||
lspManager,
|
||||
disposeHooks: disposeHooks.run,
|
||||
})
|
||||
|
||||
// when
|
||||
await dispose()
|
||||
await dispose()
|
||||
|
||||
// then
|
||||
expect(shutdownSpy).toHaveBeenCalledTimes(1)
|
||||
expect(disconnectAllSpy).toHaveBeenCalledTimes(1)
|
||||
expect(stopAllSpy).toHaveBeenCalledTimes(1)
|
||||
expect(disposeHooksSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test("#given backgroundManager.shutdown() throws #when dispose() is called #then skillMcpManager.disconnectAll() and disposeHooks() are still called", async () => {
|
||||
// given
|
||||
const backgroundManager = {
|
||||
shutdown: async (): Promise<void> => {
|
||||
throw new Error("shutdown failed")
|
||||
},
|
||||
}
|
||||
const skillMcpManager = {
|
||||
disconnectAll: async (): Promise<void> => {},
|
||||
}
|
||||
const lspManager = {
|
||||
stopAll: async (): Promise<void> => {},
|
||||
}
|
||||
const disposeHooksCalls: number[] = []
|
||||
const disconnectAllSpy = spyOn(skillMcpManager, "disconnectAll")
|
||||
const dispose = createPluginDispose({
|
||||
backgroundManager,
|
||||
skillMcpManager,
|
||||
lspManager,
|
||||
disposeHooks: (): void => {
|
||||
disposeHooksCalls.push(1)
|
||||
},
|
||||
})
|
||||
|
||||
// when
|
||||
await dispose()
|
||||
|
||||
// then
|
||||
expect(disconnectAllSpy).toHaveBeenCalledTimes(1)
|
||||
expect(disposeHooksCalls).toHaveLength(1)
|
||||
})
|
||||
|
||||
test("#given skillMcpManager.disconnectAll() throws #when dispose() is called #then disposeHooks() is still called", async () => {
|
||||
// given
|
||||
const backgroundManager = {
|
||||
shutdown: async (): Promise<void> => {},
|
||||
}
|
||||
const skillMcpManager = {
|
||||
disconnectAll: async (): Promise<void> => {
|
||||
throw new Error("disconnectAll failed")
|
||||
},
|
||||
}
|
||||
const lspManager = {
|
||||
stopAll: async (): Promise<void> => {},
|
||||
}
|
||||
const disposeHooksCalls: number[] = []
|
||||
const shutdownSpy = spyOn(backgroundManager, "shutdown")
|
||||
const dispose = createPluginDispose({
|
||||
backgroundManager,
|
||||
skillMcpManager,
|
||||
lspManager,
|
||||
disposeHooks: (): void => {
|
||||
disposeHooksCalls.push(1)
|
||||
},
|
||||
})
|
||||
|
||||
// when
|
||||
await dispose()
|
||||
|
||||
// then
|
||||
expect(shutdownSpy).toHaveBeenCalledTimes(1)
|
||||
expect(disposeHooksCalls).toHaveLength(1)
|
||||
})
|
||||
|
||||
test("#given active LSP clients #when dispose runs #then lsp manager is stopped", async () => {
|
||||
// given
|
||||
const lspManager = {
|
||||
stopAll: async (): Promise<void> => {},
|
||||
}
|
||||
const stopAllSpy = spyOn(lspManager, "stopAll")
|
||||
const dispose = createPluginDispose({
|
||||
backgroundManager: {
|
||||
shutdown: async (): Promise<void> => {},
|
||||
},
|
||||
skillMcpManager: {
|
||||
disconnectAll: async (): Promise<void> => {},
|
||||
},
|
||||
lspManager,
|
||||
disposeHooks: (): void => {},
|
||||
})
|
||||
|
||||
// when
|
||||
await dispose()
|
||||
|
||||
// then
|
||||
expect(stopAllSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@@ -1,51 +0,0 @@
|
||||
import { log } from "./shared"
|
||||
|
||||
export type PluginDispose = () => Promise<void>
|
||||
|
||||
export function createPluginDispose(args: {
|
||||
backgroundManager: {
|
||||
shutdown: () => void | Promise<void>
|
||||
}
|
||||
skillMcpManager: {
|
||||
disconnectAll: () => Promise<void>
|
||||
}
|
||||
lspManager: {
|
||||
stopAll: () => Promise<void>
|
||||
}
|
||||
disposeHooks: () => void
|
||||
}): PluginDispose {
|
||||
const { backgroundManager, skillMcpManager, lspManager, disposeHooks } = args
|
||||
let disposePromise: Promise<void> | null = null
|
||||
|
||||
return async (): Promise<void> => {
|
||||
if (disposePromise) {
|
||||
await disposePromise
|
||||
return
|
||||
}
|
||||
|
||||
disposePromise = (async (): Promise<void> => {
|
||||
try {
|
||||
await backgroundManager.shutdown()
|
||||
} catch (error) {
|
||||
log("[plugin-dispose] backgroundManager.shutdown() error:", error)
|
||||
}
|
||||
try {
|
||||
await skillMcpManager.disconnectAll()
|
||||
} catch (error) {
|
||||
log("[plugin-dispose] skillMcpManager.disconnectAll() error:", error)
|
||||
}
|
||||
try {
|
||||
await lspManager.stopAll()
|
||||
} catch (error) {
|
||||
log("[plugin-dispose] lspManager.stopAll() error:", error)
|
||||
}
|
||||
try {
|
||||
disposeHooks()
|
||||
} catch (error) {
|
||||
log("[plugin-dispose] disposeHooks() error:", error)
|
||||
}
|
||||
})()
|
||||
|
||||
await disposePromise
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
const KNOWN_VARIANTS = new Set([
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
"max",
|
||||
"minimal",
|
||||
"none",
|
||||
"auto",
|
||||
"thinking",
|
||||
])
|
||||
|
||||
export function parseVariantFromModelID(rawModelID: string): { modelID: string; variant?: string } {
|
||||
const trimmedModelID = rawModelID.trim()
|
||||
if (!trimmedModelID) {
|
||||
return { modelID: "" }
|
||||
}
|
||||
|
||||
const parenthesizedVariant = trimmedModelID.match(/^(.*)\(([^()]+)\)\s*$/)
|
||||
if (parenthesizedVariant) {
|
||||
const modelID = parenthesizedVariant[1]?.trim() ?? ""
|
||||
const variant = parenthesizedVariant[2]?.trim()
|
||||
return variant ? { modelID, variant } : { modelID }
|
||||
}
|
||||
|
||||
const spaceVariant = trimmedModelID.match(/^(.*\S)\s+([a-z][a-z0-9_-]*)$/i)
|
||||
if (spaceVariant) {
|
||||
const modelID = spaceVariant[1]?.trim() ?? ""
|
||||
const variant = spaceVariant[2]?.trim().toLowerCase()
|
||||
if (variant && KNOWN_VARIANTS.has(variant)) {
|
||||
return { modelID, variant }
|
||||
}
|
||||
}
|
||||
|
||||
return { modelID: trimmedModelID }
|
||||
}
|
||||
|
||||
export function parseModelString(
|
||||
model: string,
|
||||
): { providerID: string; modelID: string; variant?: string } | undefined {
|
||||
const trimmedModel = model.trim()
|
||||
if (!trimmedModel) return undefined
|
||||
|
||||
const parts = trimmedModel.split("/")
|
||||
if (parts.length < 2) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const providerID = parts[0]?.trim()
|
||||
const rawModelID = parts.slice(1).join("/").trim()
|
||||
if (!providerID || !rawModelID) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const parsedModel = parseVariantFromModelID(rawModelID)
|
||||
if (!parsedModel.modelID) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return parsedModel.variant
|
||||
? { providerID, modelID: parsedModel.modelID, variant: parsedModel.variant }
|
||||
: { providerID, modelID: parsedModel.modelID }
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { describe, test, expect } from "bun:test"
|
||||
import { resolveCallID } from "./resolve-call-id"
|
||||
import type { ToolContextWithMetadata } from "./types"
|
||||
|
||||
describe("resolveCallID", () => {
|
||||
function makeCtx(overrides: Partial<ToolContextWithMetadata> = {}): ToolContextWithMetadata {
|
||||
return {
|
||||
sessionID: "ses_test",
|
||||
messageID: "msg_test",
|
||||
agent: "sisyphus",
|
||||
abort: new AbortController().signal,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
test("#given callID is set #then returns callID", () => {
|
||||
const ctx = makeCtx({ callID: "call_abc" })
|
||||
expect(resolveCallID(ctx)).toBe("call_abc")
|
||||
})
|
||||
|
||||
test("#given only callId is set #then returns callId", () => {
|
||||
const ctx = makeCtx({ callId: "call_def" })
|
||||
expect(resolveCallID(ctx)).toBe("call_def")
|
||||
})
|
||||
|
||||
test("#given only call_id is set #then returns call_id", () => {
|
||||
const ctx = makeCtx({ call_id: "call_ghi" })
|
||||
expect(resolveCallID(ctx)).toBe("call_ghi")
|
||||
})
|
||||
|
||||
test("#given callID and callId are both set #then prefers callID", () => {
|
||||
const ctx = makeCtx({ callID: "preferred", callId: "fallback" })
|
||||
expect(resolveCallID(ctx)).toBe("preferred")
|
||||
})
|
||||
|
||||
test("#given no call ID variants are set #then returns undefined", () => {
|
||||
const ctx = makeCtx()
|
||||
expect(resolveCallID(ctx)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -1,5 +0,0 @@
|
||||
import type { ToolContextWithMetadata } from "./types"
|
||||
|
||||
export function resolveCallID(ctx: ToolContextWithMetadata): string | undefined {
|
||||
return ctx.callID ?? ctx.callId ?? ctx.call_id
|
||||
}
|
||||
Reference in New Issue
Block a user