fix(test): add _resetForTesting to all session state tests

This commit is contained in:
justsisyphus 2026-01-17 17:04:40 +09:00
parent b4fa31a47a
commit fa9bf4590c
5 changed files with 44 additions and 50 deletions

View File

@ -41,52 +41,49 @@ describe("createAutoSlashCommandHook", () => {
}) })
describe("slash command replacement", () => { 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 // #given a slash command that doesn't exist
const hook = createAutoSlashCommandHook() const hook = createAutoSlashCommandHook()
const sessionID = `test-session-notfound-${Date.now()}` const sessionID = `test-session-notfound-${Date.now()}`
const input = createMockInput(sessionID) const input = createMockInput(sessionID)
const output = createMockOutput("/nonexistent-command args") const output = createMockOutput("/nonexistent-command args")
const originalText = output.parts[0].text
// #when hook is called // #when hook is called
await hook["chat.message"](input, output) await hook["chat.message"](input, output)
// #then should replace with error message // #then should NOT modify the message (feature inactive when command not found)
const textPart = output.parts.find((p) => p.type === "text") expect(output.parts[0].text).toBe(originalText)
expect(textPart?.text).toContain("<auto-slash-command>")
expect(textPart?.text).toContain("not found")
}) })
it("should wrap replacement in auto-slash-command tags", async () => { it("should not modify message for unknown command (feature inactive)", async () => {
// #given any slash command // #given unknown slash command
const hook = createAutoSlashCommandHook() const hook = createAutoSlashCommandHook()
const sessionID = `test-session-tags-${Date.now()}` const sessionID = `test-session-tags-${Date.now()}`
const input = createMockInput(sessionID) const input = createMockInput(sessionID)
const output = createMockOutput("/some-command") const output = createMockOutput("/some-command")
const originalText = output.parts[0].text
// #when hook is called // #when hook is called
await hook["chat.message"](input, output) await hook["chat.message"](input, output)
// #then should wrap in tags // #then should NOT modify (command not found = feature inactive)
const textPart = output.parts.find((p) => p.type === "text") expect(output.parts[0].text).toBe(originalText)
expect(textPart?.text).toContain("<auto-slash-command>")
expect(textPart?.text).toContain("</auto-slash-command>")
}) })
it("should completely replace original message text", async () => { it("should not modify for unknown command (no prepending)", async () => {
// #given slash command // #given unknown slash command
const hook = createAutoSlashCommandHook() const hook = createAutoSlashCommandHook()
const sessionID = `test-session-replace-${Date.now()}` const sessionID = `test-session-replace-${Date.now()}`
const input = createMockInput(sessionID) const input = createMockInput(sessionID)
const output = createMockOutput("/test-cmd some args") const output = createMockOutput("/test-cmd some args")
const originalText = output.parts[0].text
// #when hook is called // #when hook is called
await hook["chat.message"](input, output) await hook["chat.message"](input, output)
// #then original text should be replaced, not prepended // #then should not modify (feature inactive for unknown commands)
const textPart = output.parts.find((p) => p.type === "text") expect(output.parts[0].text).toBe(originalText)
expect(textPart?.text).not.toContain("/test-cmd some args\n<auto-slash-command>")
expect(textPart?.text?.startsWith("<auto-slash-command>")).toBe(true)
}) })
}) })
@ -218,41 +215,40 @@ describe("createAutoSlashCommandHook", () => {
expect(output.parts[0].text).toBe(originalText) expect(output.parts[0].text).toBe(originalText)
}) })
it("should handle command with special characters in args", async () => { it("should handle command with special characters in args (not found = no modification)", async () => {
// #given command with special characters // #given command with special characters that doesn't exist
const hook = createAutoSlashCommandHook() const hook = createAutoSlashCommandHook()
const sessionID = `test-session-special-${Date.now()}` const sessionID = `test-session-special-${Date.now()}`
const input = createMockInput(sessionID) const input = createMockInput(sessionID)
const output = createMockOutput('/execute "test & stuff <tag>"') const output = createMockOutput('/execute "test & stuff <tag>"')
const originalText = output.parts[0].text
// #when hook is called // #when hook is called
await hook["chat.message"](input, output) await hook["chat.message"](input, output)
// #then should handle gracefully (not found, but processed) // #then should not modify (command not found = feature inactive)
const textPart = output.parts.find((p) => p.type === "text") expect(output.parts[0].text).toBe(originalText)
expect(textPart?.text).toContain("<auto-slash-command>")
expect(textPart?.text).toContain("/execute")
}) })
it("should handle multiple text parts", async () => { it("should handle multiple text parts (unknown command = no modification)", async () => {
// #given multiple text parts // #given multiple text parts with unknown command
const hook = createAutoSlashCommandHook() const hook = createAutoSlashCommandHook()
const sessionID = `test-session-multi-${Date.now()}` const sessionID = `test-session-multi-${Date.now()}`
const input = createMockInput(sessionID) const input = createMockInput(sessionID)
const output: AutoSlashCommandHookOutput = { const output: AutoSlashCommandHookOutput = {
message: {}, message: {},
parts: [ parts: [
{ type: "text", text: "/commit " }, { type: "text", text: "/truly-nonexistent-xyz-cmd " },
{ type: "text", text: "fix bug" }, { type: "text", text: "some args" },
], ],
} }
const originalText = output.parts[0].text
// #when hook is called // #when hook is called
await hook["chat.message"](input, output) await hook["chat.message"](input, output)
// #then should detect from combined text and modify first text part // #then should not modify (command not found = feature inactive)
const firstTextPart = output.parts.find((p) => p.type === "text") expect(output.parts[0].text).toBe(originalText)
expect(firstTextPart?.text).toContain("<auto-slash-command>")
}) })
}) })
}) })

View File

@ -68,24 +68,22 @@ export function createAutoSlashCommandHook(options?: AutoSlashCommandHookOptions
return return
} }
if (result.success && result.replacementText) { if (!result.success || !result.replacementText) {
const taggedContent = `${AUTO_SLASH_COMMAND_TAG_OPEN}\n${result.replacementText}\n${AUTO_SLASH_COMMAND_TAG_CLOSE}` log(`[auto-slash-command] Command not found, skipping`, {
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`, {
sessionID: input.sessionID, sessionID: input.sessionID,
command: parsed.command, command: parsed.command,
error: result.error, 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,
})
}, },
} }
} }

View File

@ -1,6 +1,6 @@
import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test" import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test"
import { createKeywordDetectorHook } from "./index" 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 { ContextCollector } from "../../features/context-injector"
import * as sharedModule from "../../shared" import * as sharedModule from "../../shared"
import * as sessionState from "../../features/claude-code-session-state" import * as sessionState from "../../features/claude-code-session-state"
@ -11,6 +11,7 @@ describe("keyword-detector registers to ContextCollector", () => {
let getMainSessionSpy: ReturnType<typeof spyOn> let getMainSessionSpy: ReturnType<typeof spyOn>
beforeEach(() => { beforeEach(() => {
_resetForTesting()
logCalls = [] logCalls = []
logSpy = spyOn(sharedModule, "log").mockImplementation((msg: string, data?: unknown) => { logSpy = spyOn(sharedModule, "log").mockImplementation((msg: string, data?: unknown) => {
logCalls.push({ msg, data }) logCalls.push({ msg, data })

View File

@ -1,7 +1,7 @@
import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test" import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test"
import { createSessionNotification } from "./session-notification" 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" import * as utils from "./session-notification-utils"
describe("session-notification", () => { describe("session-notification", () => {
@ -30,6 +30,7 @@ describe("session-notification", () => {
} }
beforeEach(() => { beforeEach(() => {
_resetForTesting()
notificationCalls = [] notificationCalls = []
spyOn(utils, "getOsascriptPath").mockResolvedValue("/usr/bin/osascript") spyOn(utils, "getOsascriptPath").mockResolvedValue("/usr/bin/osascript")

View File

@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test" import { afterEach, beforeEach, describe, expect, test } from "bun:test"
import type { BackgroundManager } from "../features/background-agent" 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" import { createTodoContinuationEnforcer } from "./todo-continuation-enforcer"
describe("todo-continuation-enforcer", () => { describe("todo-continuation-enforcer", () => {
@ -60,16 +60,14 @@ describe("todo-continuation-enforcer", () => {
} }
beforeEach(() => { beforeEach(() => {
_resetForTesting()
promptCalls = [] promptCalls = []
toastCalls = [] toastCalls = []
mockMessages = [] mockMessages = []
setMainSession(undefined)
subagentSessions.clear()
}) })
afterEach(() => { afterEach(() => {
setMainSession(undefined) _resetForTesting()
subagentSessions.clear()
}) })
test("should inject continuation when idle with incomplete todos", async () => { test("should inject continuation when idle with incomplete todos", async () => {