@koda-sl/baker-bridge 0.143.0 → 0.145.0-dev.5f3658e72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2121,6 +2121,61 @@ describe("BridgeJobExecutor", () => {
2121
2121
  await inactiveExecutor.execute({ threadId: "thread_inactive_close", turnId: "turn_inactive_close", prompt: "Stop", history: history([]) }, inactiveControls);
2122
2122
  expect(inactiveCloseCalls).toEqual(["close"]);
2123
2123
  });
2124
+ // BAKER-1025 (AskUserQuestion), BAKER-502 (the report held behind it), BAKER-2215
2125
+ // (request_tag_input), and the ExitPlanMode and request_connection sightings: each held
2126
+ // the turn ~900 s before the same deny, although the card stays up and a later answer
2127
+ // resumes the chat as a new turn anyway.
2128
+ it.each([
2129
+ { toolName: "AskUserQuestion", input: { questions: [{ question: "Which budget?" }] }, planMode: false },
2130
+ { toolName: "ExitPlanMode", input: { plan: "Draft flow" }, planMode: true },
2131
+ {
2132
+ toolName: "mcp__baker_ui__request_tag_input",
2133
+ input: { changes: [{ mode: "create", tagType: "clarity" }] },
2134
+ planMode: false,
2135
+ },
2136
+ {
2137
+ toolName: CONNECTION_INPUT_TOOL_NAME,
2138
+ input: { connections: [{ platform: "hubspot", reason: "Send leads to HubSpot" }] },
2139
+ planMode: false,
2140
+ },
2141
+ ])("releases the turn within a minute when $toolName goes unanswered", async ({ toolName, input, planMode }) => {
2142
+ vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "setInterval", "clearInterval"] });
2143
+ try {
2144
+ let guardResult;
2145
+ let reachedTool = () => undefined;
2146
+ const toolReached = new Promise((resolve) => {
2147
+ reachedTool = resolve;
2148
+ });
2149
+ const executor = new BridgeJobExecutor({
2150
+ sdk: {
2151
+ query: (queryInput) => ({
2152
+ async *[Symbol.asyncIterator]() {
2153
+ reachedTool();
2154
+ guardResult = await queryInput.options.canUseTool?.(toolName, input, {
2155
+ toolUseID: "tool_unanswered",
2156
+ requestId: "req_unanswered",
2157
+ signal: queryInput.options.abortController?.signal ?? new AbortController().signal,
2158
+ });
2159
+ yield resultMessage(0, "session_unanswered");
2160
+ },
2161
+ }),
2162
+ },
2163
+ model: "claude-test",
2164
+ sessionStore: { load: async () => undefined, save: async () => undefined, clear: async () => undefined },
2165
+ relay: recordingRelay(),
2166
+ });
2167
+ const controls = activeControls();
2168
+ const outcome = executor.execute({ threadId: "thread_unanswered", turnId: "turn_unanswered", prompt: "Ask first", planMode }, controls);
2169
+ await toolReached;
2170
+ await vi.advanceTimersByTimeAsync(59_000);
2171
+ expect(guardResult).toMatchObject({ behavior: "deny", message: expect.stringContaining("has not answered") });
2172
+ await vi.runOnlyPendingTimersAsync();
2173
+ await outcome;
2174
+ }
2175
+ finally {
2176
+ vi.useRealTimers();
2177
+ }
2178
+ });
2124
2179
  it("denies an unanswered AskUserQuestion gracefully instead of erroring the turn", async () => {
2125
2180
  const relay = recordingRelay();
2126
2181
  let inputResolvedCalls = 0;
@@ -2157,6 +2212,9 @@ describe("BridgeJobExecutor", () => {
2157
2212
  // cleanly so the user can answer with a normal message later.
2158
2213
  expect(guardResult).toMatchObject({ behavior: "deny" });
2159
2214
  expect(guardResult.message).toContain("has not answered");
2215
+ // BAKER-245: the card stays on screen, so a closing note that restated it made the
2216
+ // marketer read the same question three times (prose, card, prose).
2217
+ expect(guardResult.message).toMatch(/do not repeat the question/i);
2160
2218
  expect(outcome).toMatchObject({ kind: "completed", costUsd: 0, model: "claude-test" });
2161
2219
  expect(controls.pendingQuestions.size).toBe(0);
2162
2220
  // The backend's pending question must SURVIVE the live wait ending: the