fix(keyword-detector): use mainSessionID for session check instead of unreliable API
The keyword-detector was using ctx.client.session.get() to check parentID for determining subagent sessions, but this API didn't reliably return parentID. This caused non-ultrawork keywords (search, analyze) to be injected in subagent sessions when they should only work in main sessions. Changed to use getMainSessionID() comparison, consistent with other hooks like session-notification and todo-continuation-enforcer. - Replace unreliable parentID API check with mainSessionID comparison - Add comprehensive test coverage for session filtering behavior - Remove unnecessary session.get API call
This commit is contained in:
125
src/hooks/keyword-detector/index.test.ts
Normal file
125
src/hooks/keyword-detector/index.test.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test"
|
||||||
|
import { createKeywordDetectorHook } from "./index"
|
||||||
|
import { setMainSession } from "../../features/claude-code-session-state"
|
||||||
|
import * as sharedModule from "../../shared"
|
||||||
|
|
||||||
|
describe("keyword-detector session filtering", () => {
|
||||||
|
let logCalls: Array<{ msg: string; data?: unknown }>
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setMainSession(undefined)
|
||||||
|
logCalls = []
|
||||||
|
spyOn(sharedModule, "log").mockImplementation((msg: string, data?: unknown) => {
|
||||||
|
logCalls.push({ msg, data })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
setMainSession(undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
function createMockPluginInput(options: { toastCalls?: string[] } = {}) {
|
||||||
|
const toastCalls = options.toastCalls ?? []
|
||||||
|
return {
|
||||||
|
client: {
|
||||||
|
tui: {
|
||||||
|
showToast: async (opts: any) => {
|
||||||
|
toastCalls.push(opts.body.title)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
|
||||||
|
test("should skip non-ultrawork keywords in non-main session (using mainSessionID check)", async () => {
|
||||||
|
// #given - main session is set, different session submits search keyword
|
||||||
|
const mainSessionID = "main-123"
|
||||||
|
const subagentSessionID = "subagent-456"
|
||||||
|
setMainSession(mainSessionID)
|
||||||
|
|
||||||
|
const hook = createKeywordDetectorHook(createMockPluginInput())
|
||||||
|
const output = {
|
||||||
|
message: {} as Record<string, unknown>,
|
||||||
|
parts: [{ type: "text", text: "search mode 찾아줘" }],
|
||||||
|
}
|
||||||
|
|
||||||
|
// #when - non-main session triggers keyword detection
|
||||||
|
await hook["chat.message"](
|
||||||
|
{ sessionID: subagentSessionID },
|
||||||
|
output
|
||||||
|
)
|
||||||
|
|
||||||
|
// #then - search keyword should be filtered out based on mainSessionID comparison
|
||||||
|
const skipLog = logCalls.find(c => c.msg.includes("Skipping non-ultrawork keywords in non-main session"))
|
||||||
|
expect(skipLog).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should allow ultrawork keywords in non-main session", async () => {
|
||||||
|
// #given - main session is set, different session submits ultrawork keyword
|
||||||
|
const mainSessionID = "main-123"
|
||||||
|
const subagentSessionID = "subagent-456"
|
||||||
|
setMainSession(mainSessionID)
|
||||||
|
|
||||||
|
const toastCalls: string[] = []
|
||||||
|
const hook = createKeywordDetectorHook(createMockPluginInput({ toastCalls }))
|
||||||
|
const output = {
|
||||||
|
message: {} as Record<string, unknown>,
|
||||||
|
parts: [{ type: "text", text: "ultrawork mode" }],
|
||||||
|
}
|
||||||
|
|
||||||
|
// #when - non-main session triggers ultrawork keyword
|
||||||
|
await hook["chat.message"](
|
||||||
|
{ sessionID: subagentSessionID },
|
||||||
|
output
|
||||||
|
)
|
||||||
|
|
||||||
|
// #then - ultrawork should still work (variant set to max)
|
||||||
|
expect(output.message.variant).toBe("max")
|
||||||
|
expect(toastCalls).toContain("Ultrawork Mode Activated")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should allow all keywords in main session", async () => {
|
||||||
|
// #given - main session submits search keyword
|
||||||
|
const mainSessionID = "main-123"
|
||||||
|
setMainSession(mainSessionID)
|
||||||
|
|
||||||
|
const hook = createKeywordDetectorHook(createMockPluginInput())
|
||||||
|
const output = {
|
||||||
|
message: {} as Record<string, unknown>,
|
||||||
|
parts: [{ type: "text", text: "search mode 찾아줘" }],
|
||||||
|
}
|
||||||
|
|
||||||
|
// #when - main session triggers keyword detection
|
||||||
|
await hook["chat.message"](
|
||||||
|
{ sessionID: mainSessionID },
|
||||||
|
output
|
||||||
|
)
|
||||||
|
|
||||||
|
// #then - search keyword should be detected (output unchanged but detection happens)
|
||||||
|
// Note: search keywords don't set variant, they inject messages via context-injector
|
||||||
|
// This test verifies the detection logic runs without filtering
|
||||||
|
expect(output.message.variant).toBeUndefined() // search doesn't set variant
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should allow all keywords when mainSessionID is not set", async () => {
|
||||||
|
// #given - no main session set (early startup or standalone mode)
|
||||||
|
setMainSession(undefined)
|
||||||
|
|
||||||
|
const toastCalls: string[] = []
|
||||||
|
const hook = createKeywordDetectorHook(createMockPluginInput({ toastCalls }))
|
||||||
|
const output = {
|
||||||
|
message: {} as Record<string, unknown>,
|
||||||
|
parts: [{ type: "text", text: "ultrawork search" }],
|
||||||
|
}
|
||||||
|
|
||||||
|
// #when - any session triggers keyword detection
|
||||||
|
await hook["chat.message"](
|
||||||
|
{ sessionID: "any-session" },
|
||||||
|
output
|
||||||
|
)
|
||||||
|
|
||||||
|
// #then - all keywords should work
|
||||||
|
expect(output.message.variant).toBe("max")
|
||||||
|
expect(toastCalls).toContain("Ultrawork Mode Activated")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
import { detectKeywordsWithType, extractPromptText, removeCodeBlocks } from "./detector"
|
import { detectKeywordsWithType, extractPromptText, removeCodeBlocks } from "./detector"
|
||||||
import { log } from "../../shared"
|
import { log } from "../../shared"
|
||||||
|
import { getMainSessionID } from "../../features/claude-code-session-state"
|
||||||
|
|
||||||
export * from "./detector"
|
export * from "./detector"
|
||||||
export * from "./constants"
|
export * from "./constants"
|
||||||
@@ -27,30 +28,21 @@ export function createKeywordDetectorHook(ctx: PluginInput) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a subagent session (has parent)
|
// Only ultrawork keywords work in non-main sessions
|
||||||
// Only ultrawork keywords work in subagent sessions
|
|
||||||
// Other keywords (search, analyze, etc.) only work in main sessions
|
// Other keywords (search, analyze, etc.) only work in main sessions
|
||||||
try {
|
const mainSessionID = getMainSessionID()
|
||||||
const sessionInfo = await ctx.client.session.get({ path: { id: input.sessionID } })
|
const isNonMainSession = mainSessionID && input.sessionID !== mainSessionID
|
||||||
const isSubagentSession = !!sessionInfo.data?.parentID
|
|
||||||
|
|
||||||
if (isSubagentSession) {
|
if (isNonMainSession) {
|
||||||
// Filter to only ultrawork keywords in subagent sessions
|
|
||||||
detectedKeywords = detectedKeywords.filter((k) => k.type === "ultrawork")
|
detectedKeywords = detectedKeywords.filter((k) => k.type === "ultrawork")
|
||||||
if (detectedKeywords.length === 0) {
|
if (detectedKeywords.length === 0) {
|
||||||
log(`[keyword-detector] Skipping non-ultrawork keywords in subagent session`, {
|
log(`[keyword-detector] Skipping non-ultrawork keywords in non-main session`, {
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
parentID: sessionInfo.data?.parentID,
|
mainSessionID,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
log(`[keyword-detector] Failed to get session info, proceeding with all keywords`, {
|
|
||||||
error: err,
|
|
||||||
sessionID: input.sessionID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasUltrawork = detectedKeywords.some((k) => k.type === "ultrawork")
|
const hasUltrawork = detectedKeywords.some((k) => k.type === "ultrawork")
|
||||||
if (hasUltrawork) {
|
if (hasUltrawork) {
|
||||||
|
|||||||
Reference in New Issue
Block a user