Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions src/features/hook-message-injector/injector.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
import { join } from "node:path"
import { findNearestMessageWithFields } from "./injector"

const TEST_DIR = "/tmp/test-hook-message-injector"

describe("findNearestMessageWithFields", () => {
beforeEach(() => {
if (existsSync(TEST_DIR)) {
rmSync(TEST_DIR, { recursive: true })
}
mkdirSync(TEST_DIR, { recursive: true })
})

afterEach(() => {
if (existsSync(TEST_DIR)) {
rmSync(TEST_DIR, { recursive: true })
}
})

test("returns message with model info when available", () => {
// #given
const messageWithModel = {
id: "msg_001",
agent: "Sisyphus",
model: { providerID: "openai", modelID: "gpt-5.2" },
tools: { write: true },
}
writeFileSync(join(TEST_DIR, "msg_001.json"), JSON.stringify(messageWithModel))

// #when
const result = findNearestMessageWithFields(TEST_DIR)

// #then
expect(result).not.toBeNull()
expect(result?.agent).toBe("Sisyphus")
expect(result?.model?.providerID).toBe("openai")
expect(result?.model?.modelID).toBe("gpt-5.2")
})

test("returns most recent message with model info", () => {
// #given
const olderMessage = {
id: "msg_001",
agent: "Sisyphus",
model: { providerID: "anthropic", modelID: "claude-opus-4-5" },
}
const newerMessage = {
id: "msg_002",
agent: "oracle",
model: { providerID: "openai", modelID: "gpt-5.2" },
}
writeFileSync(join(TEST_DIR, "msg_001.json"), JSON.stringify(olderMessage))
writeFileSync(join(TEST_DIR, "msg_002.json"), JSON.stringify(newerMessage))

// #when
const result = findNearestMessageWithFields(TEST_DIR)

// #then
expect(result?.agent).toBe("oracle")
expect(result?.model?.providerID).toBe("openai")
expect(result?.model?.modelID).toBe("gpt-5.2")
})

test("skips messages without complete model info", () => {
// #given
const incompleteMessage = {
id: "msg_002",
agent: "explore",
model: { providerID: "openai" },
}
const completeMessage = {
id: "msg_001",
agent: "Sisyphus",
model: { providerID: "anthropic", modelID: "claude-opus-4-5" },
}
writeFileSync(join(TEST_DIR, "msg_001.json"), JSON.stringify(completeMessage))
writeFileSync(join(TEST_DIR, "msg_002.json"), JSON.stringify(incompleteMessage))

// #when
const result = findNearestMessageWithFields(TEST_DIR)

// #then
expect(result?.agent).toBe("Sisyphus")
expect(result?.model?.providerID).toBe("anthropic")
expect(result?.model?.modelID).toBe("claude-opus-4-5")
})

test("falls back to message with agent only when no model info exists", () => {
// #given
const agentOnlyMessage = {
id: "msg_001",
agent: "librarian",
}
writeFileSync(join(TEST_DIR, "msg_001.json"), JSON.stringify(agentOnlyMessage))

// #when
const result = findNearestMessageWithFields(TEST_DIR)

// #then
expect(result?.agent).toBe("librarian")
expect(result?.model).toBeUndefined()
})

test("returns null for empty directory", () => {
// #given - empty directory (already created in beforeEach)

// #when
const result = findNearestMessageWithFields(TEST_DIR)

// #then
expect(result).toBeNull()
})

test("returns null for non-existent directory", () => {
// #given
const nonExistentDir = "/tmp/non-existent-test-dir-12345"

// #when
const result = findNearestMessageWithFields(nonExistentDir)

// #then
expect(result).toBeNull()
})

test("preserves tools field from stored message", () => {
// #given
const messageWithTools = {
id: "msg_001",
agent: "frontend-ui-ux-engineer",
model: { providerID: "google", modelID: "gemini-3-pro-preview" },
tools: { write: true, edit: true, bash: false },
}
writeFileSync(join(TEST_DIR, "msg_001.json"), JSON.stringify(messageWithTools))

// #when
const result = findNearestMessageWithFields(TEST_DIR)

// #then
expect(result?.tools).toEqual({ write: true, edit: true, bash: false })
})

test("handles malformed JSON files gracefully", () => {
// #given
const validMessage = {
id: "msg_001",
agent: "Sisyphus",
model: { providerID: "openai", modelID: "gpt-5.2" },
}
writeFileSync(join(TEST_DIR, "msg_001.json"), JSON.stringify(validMessage))
writeFileSync(join(TEST_DIR, "msg_002.json"), "{ invalid json }")

// #when
const result = findNearestMessageWithFields(TEST_DIR)

// #then
expect(result?.agent).toBe("Sisyphus")
})
})
14 changes: 13 additions & 1 deletion src/features/hook-message-injector/injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,19 @@ export function findNearestMessageWithFields(messageDir: string): StoredMessage
try {
const content = readFileSync(join(messageDir, file), "utf-8")
const msg = JSON.parse(content) as StoredMessage
if (msg.agent && msg.model?.providerID && msg.model?.modelID) {
if (msg.model?.providerID && msg.model?.modelID) {
return msg
}
} catch {
continue
}
}

for (const file of files) {
try {
const content = readFileSync(join(messageDir, file), "utf-8")
const msg = JSON.parse(content) as StoredMessage
if (msg.agent) {
return msg
}
} catch {
Expand Down
5 changes: 4 additions & 1 deletion src/hooks/todo-continuation-enforcer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,14 @@ export function createTodoContinuationEnforcer(ctx: PluginInput): TodoContinuati
return
}

log(`[${HOOK_NAME}] Injecting continuation prompt`, { sessionID, agent: prevMessage?.agent })
log(`[${HOOK_NAME}] Injecting continuation prompt`, { sessionID, agent: prevMessage?.agent, model: prevMessage?.model })
await ctx.client.session.prompt({
path: { id: sessionID },
body: {
agent: prevMessage?.agent,
model: prevMessage?.model?.providerID && prevMessage?.model?.modelID
? { providerID: prevMessage.model.providerID, modelID: prevMessage.model.modelID }
: undefined,
parts: [
{
type: "text",
Expand Down