fix(prompt-gate): fail closed on unsafe tails

This commit is contained in:
YeonGyu-Kim
2026-05-28 18:14:09 +09:00
parent 105a7ea5e6
commit 4bb4acf09a
8 changed files with 716 additions and 144 deletions
@@ -0,0 +1,79 @@
/// <reference types="bun-types" />
import { afterEach, describe, expect, test } from "bun:test"
import {
_setPromptGateMessagesFetchTimeoutMsForTesting,
dispatchInternalPrompt,
releaseAllPromptAsyncReservationsForTesting,
} from "./prompt-async-gate"
describe("dispatchInternalPrompt message fetch safety", () => {
afterEach(() => {
// then
_setPromptGateMessagesFetchTimeoutMsForTesting(undefined)
releaseAllPromptAsyncReservationsForTesting()
})
test("#given latest-message fetch hangs #when an internal promptAsync is requested #then no prompt is sent", async () => {
// given
_setPromptGateMessagesFetchTimeoutMsForTesting(5)
let promptCalls = 0
const client = {
session: {
status: async () => ({ data: { ses_messages_hang: { type: "idle" } } }),
messages: async () => new Promise(() => {}),
promptAsync: async () => {
promptCalls += 1
},
},
}
// when
const result = await dispatchInternalPrompt({
mode: "async",
client,
sessionID: "ses_messages_hang",
input: { path: { id: "ses_messages_hang" }, body: { parts: [] } },
source: "test:messages-hang",
settleMs: 0,
postDispatchHoldMs: 0,
dispatchTimeoutMs: 50,
})
// then
expect(result.status).toBe("queued")
expect(promptCalls).toBe(0)
})
test("#given latest-message fetch throws #when an internal promptAsync is requested #then no prompt is sent", async () => {
// given
let promptCalls = 0
const client = {
session: {
status: async () => ({ data: { ses_messages_throw: { type: "idle" } } }),
messages: async () => {
throw new Error("message endpoint failed")
},
promptAsync: async () => {
promptCalls += 1
},
},
}
// when
const result = await dispatchInternalPrompt({
mode: "async",
client,
sessionID: "ses_messages_throw",
input: { path: { id: "ses_messages_throw" }, body: { parts: [] } },
source: "test:messages-throw",
settleMs: 0,
postDispatchHoldMs: 0,
dispatchTimeoutMs: 50,
})
// then
expect(result.status).toBe("queued")
expect(promptCalls).toBe(0)
})
})
@@ -0,0 +1,53 @@
import { afterEach, describe, expect, test } from "bun:test"
import {
dispatchInternalPrompt,
releaseAllPromptAsyncReservationsForTesting,
} from "./prompt-async-gate"
describe("dispatchInternalPrompt question tool gating", () => {
afterEach(() => {
releaseAllPromptAsyncReservationsForTesting()
})
test("#given completed assistant question has no real user answer #when an internal promptAsync is requested #then no prompt is sent", async () => {
// given
let promptCalls = 0
const client = {
session: {
status: async () => ({ data: { ses_completed_question: { type: "idle" } } }),
messages: async () => ({
data: [
{
info: {
id: "msg_assistant",
role: "assistant",
finish: "tool-calls",
time: { completed: 1_762_000_000_000 },
},
parts: [{ type: "tool", tool: "question", state: { status: "error" } }],
},
],
}),
promptAsync: async () => {
promptCalls += 1
},
},
}
// when
const result = await dispatchInternalPrompt({
mode: "async",
client,
sessionID: "ses_completed_question",
input: { path: { id: "ses_completed_question" }, body: { parts: [] } },
source: "test:completed-question",
settleMs: 0,
postDispatchHoldMs: 0,
})
// then
expect(result.status).toBe("queued")
expect(promptCalls).toBe(0)
})
})
@@ -1001,37 +1001,6 @@ describe("dispatchInternalPrompt shared gate behavior", () => {
expect(promptCalls).toBe(1)
})
test("#given latest-message fetch hangs #when an internal promptAsync is requested #then the tool-state check times out and dispatch continues", async () => {
// given
_setPromptGateMessagesFetchTimeoutMsForTesting(5)
let promptCalls = 0
const client = {
session: {
status: async () => ({ data: { ses_messages_hang: { type: "idle" } } }),
messages: async () => new Promise(() => {}),
promptAsync: async () => {
promptCalls += 1
},
},
}
// when
const result = await dispatchInternalPrompt({
mode: "async",
client,
sessionID: "ses_messages_hang",
input: { path: { id: "ses_messages_hang" }, body: { parts: [] } },
source: "test:messages-hang",
settleMs: 0,
postDispatchHoldMs: 0,
dispatchTimeoutMs: 50,
})
// then
expect(result.status).toBe("dispatched")
expect(promptCalls).toBe(1)
})
test("#given dispatch hold has expired #when the same session prompts again #then the next promptAsync is accepted", async () => {
// given
let promptCalls = 0