fix: ulw keyword word boundary and skill_mcp parseArguments object handling

- Add word boundary to ulw/ultrawork regex to prevent false matches on substrings like 'StatefulWidget' (fixes #779)
- Handle object type in parseArguments to prevent [object Object] JSON parse error (fixes #747)
- Add test cases for word boundary behavior
This commit is contained in:
popododo0720
2026-01-14 21:36:32 +09:00
parent c559037f72
commit 5c4f4fc655
3 changed files with 103 additions and 2 deletions

View File

@@ -192,7 +192,7 @@ THE USER ASKED FOR X. DELIVER EXACTLY X. NOT A SUBSET. NOT A DEMO. NOT A STARTIN
export const KEYWORD_DETECTORS: Array<{ pattern: RegExp; message: string | ((agentName?: string) => string) }> = [
{
pattern: /(ultrawork|ulw)/i,
pattern: /\b(ultrawork|ulw)\b/i,
message: getUltraworkMessage,
},
// SEARCH: EN/KO/JP/CN/VN

View File

@@ -123,3 +123,98 @@ describe("keyword-detector session filtering", () => {
expect(toastCalls).toContain("Ultrawork Mode Activated")
})
})
describe("keyword-detector word boundary", () => {
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 NOT trigger ultrawork on partial matches like 'StatefulWidget' containing 'ulw'", async () => {
// #given - text contains 'ulw' as part of another word (StatefulWidget)
setMainSession(undefined)
const toastCalls: string[] = []
const hook = createKeywordDetectorHook(createMockPluginInput({ toastCalls }))
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "refactor the StatefulWidget component" }],
}
// #when - message with partial 'ulw' match is processed
await hook["chat.message"](
{ sessionID: "any-session" },
output
)
// #then - ultrawork should NOT be triggered
expect(output.message.variant).toBeUndefined()
expect(toastCalls).not.toContain("Ultrawork Mode Activated")
})
test("should trigger ultrawork on standalone 'ulw' keyword", async () => {
// #given - text contains standalone 'ulw'
setMainSession(undefined)
const toastCalls: string[] = []
const hook = createKeywordDetectorHook(createMockPluginInput({ toastCalls }))
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "ulw do this task" }],
}
// #when - message with standalone 'ulw' is processed
await hook["chat.message"](
{ sessionID: "any-session" },
output
)
// #then - ultrawork should be triggered
expect(output.message.variant).toBe("max")
expect(toastCalls).toContain("Ultrawork Mode Activated")
})
test("should NOT trigger ultrawork on file references containing 'ulw' substring", async () => {
// #given - file reference contains 'ulw' as substring
setMainSession(undefined)
const toastCalls: string[] = []
const hook = createKeywordDetectorHook(createMockPluginInput({ toastCalls }))
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "@StatefulWidget.tsx please review this file" }],
}
// #when - message referencing file with 'ulw' substring is processed
await hook["chat.message"](
{ sessionID: "any-session" },
output
)
// #then - ultrawork should NOT be triggered
expect(output.message.variant).toBeUndefined()
expect(toastCalls).not.toContain("Ultrawork Mode Activated")
})
})

View File

@@ -69,8 +69,14 @@ function formatAvailableMcps(skills: LoadedSkill[]): string {
return mcps.length > 0 ? mcps.join("\n") : " (none found)"
}
function parseArguments(argsJson: string | undefined): Record<string, unknown> {
function parseArguments(argsJson: string | Record<string, unknown> | undefined): Record<string, unknown> {
if (!argsJson) return {}
// Handle case when argsJson is already an object (from tool calling pipeline)
if (typeof argsJson === "object" && argsJson !== null) {
return argsJson
}
try {
const parsed = JSON.parse(argsJson)
if (typeof parsed !== "object" || parsed === null) {