diff --git a/src/hooks/auto-slash-command/index.test.ts b/src/hooks/auto-slash-command/index.test.ts index ac8033bf9..3ad556380 100644 --- a/src/hooks/auto-slash-command/index.test.ts +++ b/src/hooks/auto-slash-command/index.test.ts @@ -41,52 +41,49 @@ describe("createAutoSlashCommandHook", () => { }) describe("slash command replacement", () => { - it("should replace message with error when command not found", async () => { + it("should not modify message when command not found", async () => { // #given a slash command that doesn't exist const hook = createAutoSlashCommandHook() const sessionID = `test-session-notfound-${Date.now()}` const input = createMockInput(sessionID) const output = createMockOutput("/nonexistent-command args") + const originalText = output.parts[0].text // #when hook is called await hook["chat.message"](input, output) - // #then should replace with error message - const textPart = output.parts.find((p) => p.type === "text") - expect(textPart?.text).toContain("") - expect(textPart?.text).toContain("not found") + // #then should NOT modify the message (feature inactive when command not found) + expect(output.parts[0].text).toBe(originalText) }) - it("should wrap replacement in auto-slash-command tags", async () => { - // #given any slash command + it("should not modify message for unknown command (feature inactive)", async () => { + // #given unknown slash command const hook = createAutoSlashCommandHook() const sessionID = `test-session-tags-${Date.now()}` const input = createMockInput(sessionID) const output = createMockOutput("/some-command") + const originalText = output.parts[0].text // #when hook is called await hook["chat.message"](input, output) - // #then should wrap in tags - const textPart = output.parts.find((p) => p.type === "text") - expect(textPart?.text).toContain("") - expect(textPart?.text).toContain("") + // #then should NOT modify (command not found = feature inactive) + expect(output.parts[0].text).toBe(originalText) }) - it("should completely replace original message text", async () => { - // #given slash command + it("should not modify for unknown command (no prepending)", async () => { + // #given unknown slash command const hook = createAutoSlashCommandHook() const sessionID = `test-session-replace-${Date.now()}` const input = createMockInput(sessionID) const output = createMockOutput("/test-cmd some args") + const originalText = output.parts[0].text // #when hook is called await hook["chat.message"](input, output) - // #then original text should be replaced, not prepended - const textPart = output.parts.find((p) => p.type === "text") - expect(textPart?.text).not.toContain("/test-cmd some args\n") - expect(textPart?.text?.startsWith("")).toBe(true) + // #then should not modify (feature inactive for unknown commands) + expect(output.parts[0].text).toBe(originalText) }) }) @@ -218,41 +215,40 @@ describe("createAutoSlashCommandHook", () => { expect(output.parts[0].text).toBe(originalText) }) - it("should handle command with special characters in args", async () => { - // #given command with special characters + it("should handle command with special characters in args (not found = no modification)", async () => { + // #given command with special characters that doesn't exist const hook = createAutoSlashCommandHook() const sessionID = `test-session-special-${Date.now()}` const input = createMockInput(sessionID) const output = createMockOutput('/execute "test & stuff "') + const originalText = output.parts[0].text // #when hook is called await hook["chat.message"](input, output) - // #then should handle gracefully (not found, but processed) - const textPart = output.parts.find((p) => p.type === "text") - expect(textPart?.text).toContain("") - expect(textPart?.text).toContain("/execute") + // #then should not modify (command not found = feature inactive) + expect(output.parts[0].text).toBe(originalText) }) - it("should handle multiple text parts", async () => { - // #given multiple text parts + it("should handle multiple text parts (unknown command = no modification)", async () => { + // #given multiple text parts with unknown command const hook = createAutoSlashCommandHook() const sessionID = `test-session-multi-${Date.now()}` const input = createMockInput(sessionID) const output: AutoSlashCommandHookOutput = { message: {}, parts: [ - { type: "text", text: "/commit " }, - { type: "text", text: "fix bug" }, + { type: "text", text: "/truly-nonexistent-xyz-cmd " }, + { type: "text", text: "some args" }, ], } + const originalText = output.parts[0].text // #when hook is called await hook["chat.message"](input, output) - // #then should detect from combined text and modify first text part - const firstTextPart = output.parts.find((p) => p.type === "text") - expect(firstTextPart?.text).toContain("") + // #then should not modify (command not found = feature inactive) + expect(output.parts[0].text).toBe(originalText) }) }) }) diff --git a/src/hooks/auto-slash-command/index.ts b/src/hooks/auto-slash-command/index.ts index 0b034a283..88b50617a 100644 --- a/src/hooks/auto-slash-command/index.ts +++ b/src/hooks/auto-slash-command/index.ts @@ -68,24 +68,22 @@ export function createAutoSlashCommandHook(options?: AutoSlashCommandHookOptions return } - if (result.success && result.replacementText) { - const taggedContent = `${AUTO_SLASH_COMMAND_TAG_OPEN}\n${result.replacementText}\n${AUTO_SLASH_COMMAND_TAG_CLOSE}` - output.parts[idx].text = taggedContent - - log(`[auto-slash-command] Replaced message with command template`, { - sessionID: input.sessionID, - command: parsed.command, - }) - } else { - const errorMessage = `${AUTO_SLASH_COMMAND_TAG_OPEN}\n[AUTO-SLASH-COMMAND ERROR]\n${result.error}\n\nOriginal input: ${parsed.raw}\n${AUTO_SLASH_COMMAND_TAG_CLOSE}` - output.parts[idx].text = errorMessage - - log(`[auto-slash-command] Command not found, showing error`, { + if (!result.success || !result.replacementText) { + log(`[auto-slash-command] Command not found, skipping`, { sessionID: input.sessionID, command: parsed.command, error: result.error, }) + return } + + const taggedContent = `${AUTO_SLASH_COMMAND_TAG_OPEN}\n${result.replacementText}\n${AUTO_SLASH_COMMAND_TAG_CLOSE}` + output.parts[idx].text = taggedContent + + log(`[auto-slash-command] Replaced message with command template`, { + sessionID: input.sessionID, + command: parsed.command, + }) }, } } diff --git a/src/hooks/keyword-detector/index.test.ts b/src/hooks/keyword-detector/index.test.ts index b93b70227..26c50630f 100644 --- a/src/hooks/keyword-detector/index.test.ts +++ b/src/hooks/keyword-detector/index.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test" import { createKeywordDetectorHook } from "./index" -import { setMainSession, updateSessionAgent, clearSessionAgent } from "../../features/claude-code-session-state" +import { setMainSession, updateSessionAgent, clearSessionAgent, _resetForTesting } from "../../features/claude-code-session-state" import { ContextCollector } from "../../features/context-injector" import * as sharedModule from "../../shared" import * as sessionState from "../../features/claude-code-session-state" @@ -11,6 +11,7 @@ describe("keyword-detector registers to ContextCollector", () => { let getMainSessionSpy: ReturnType beforeEach(() => { + _resetForTesting() logCalls = [] logSpy = spyOn(sharedModule, "log").mockImplementation((msg: string, data?: unknown) => { logCalls.push({ msg, data }) diff --git a/src/hooks/session-notification.test.ts b/src/hooks/session-notification.test.ts index ad6eb5384..a19320cca 100644 --- a/src/hooks/session-notification.test.ts +++ b/src/hooks/session-notification.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test" import { createSessionNotification } from "./session-notification" -import { setMainSession, subagentSessions } from "../features/claude-code-session-state" +import { setMainSession, subagentSessions, _resetForTesting } from "../features/claude-code-session-state" import * as utils from "./session-notification-utils" describe("session-notification", () => { @@ -30,6 +30,7 @@ describe("session-notification", () => { } beforeEach(() => { + _resetForTesting() notificationCalls = [] spyOn(utils, "getOsascriptPath").mockResolvedValue("/usr/bin/osascript") diff --git a/src/hooks/todo-continuation-enforcer.test.ts b/src/hooks/todo-continuation-enforcer.test.ts index bf3343be1..d7c19577b 100644 --- a/src/hooks/todo-continuation-enforcer.test.ts +++ b/src/hooks/todo-continuation-enforcer.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test" import type { BackgroundManager } from "../features/background-agent" -import { setMainSession, subagentSessions } from "../features/claude-code-session-state" +import { setMainSession, subagentSessions, _resetForTesting } from "../features/claude-code-session-state" import { createTodoContinuationEnforcer } from "./todo-continuation-enforcer" describe("todo-continuation-enforcer", () => { @@ -60,16 +60,14 @@ describe("todo-continuation-enforcer", () => { } beforeEach(() => { + _resetForTesting() promptCalls = [] toastCalls = [] mockMessages = [] - setMainSession(undefined) - subagentSessions.clear() }) afterEach(() => { - setMainSession(undefined) - subagentSessions.clear() + _resetForTesting() }) test("should inject continuation when idle with incomplete todos", async () => {