Skip to content

Commit b0ecb91

Browse files
committed
test(sdk): repro the managed path dropping an injected message from history
The deployed QA lane finds the two surfaces disagree: a `chat.createSession()` recap in the same run recalls a mid-turn steer, while the managed `chat.agent` loop denies it. This pins the managed half at unit level, which the tracked gap has never had. Turn 2's prompt comes back as the original message and the following one, with the injected one absent, so the message reaches the model inside turn 1 and then leaves no trace in history. Verified as a real assertion failure rather than trusting `it.fails`, which would also pass on a timeout. Not fixed here, and not caused by this branch: nothing in it touches the accumulator. Recorded so the day the managed path starts carrying it is noticed, and so the difference between the surfaces has a repro that needs no deployed environment.
1 parent f6bfe94 commit b0ecb91

1 file changed

Lines changed: 98 additions & 0 deletions

File tree

packages/trigger-sdk/test/steering-injection.test.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,3 +351,101 @@ describe("chat.agent injection claims only its own batch", () => {
351351
}
352352
});
353353
});
354+
355+
/**
356+
* Whether an injected message survives into the next turn's model context.
357+
*
358+
* Recorded here because the deployed QA lane finds the two surfaces disagree:
359+
* a `chat.createSession()` recap in the same run recalls a mid-turn steer,
360+
* while the managed `chat.agent` loop denies it. That difference is
361+
* pre-existing and is the surface-specific half of the accumulator gap.
362+
*
363+
* `it.fails` because the managed path does not carry it: turn 2's prompt comes
364+
* back as the original and the following message only, with the injected one
365+
* absent. Held here so the day that changes is noticed, and so the gap has a
366+
* repro that does not need a deployed environment.
367+
*/
368+
describe("chat.agent injected message in the next turn's context", () => {
369+
it.fails(
370+
"carries an injected message into the following turn's prompt",
371+
{ timeout: 30_000 },
372+
async () => {
373+
const chatId = "inject-next-turn";
374+
const toolGate = makeGate();
375+
let toolEntered = false;
376+
const prompts: string[][] = [];
377+
378+
const gateTool = tool({
379+
description: "blocks until the test opens it",
380+
inputSchema: z.object({ q: z.string() }),
381+
execute: async () => {
382+
toolEntered = true;
383+
await toolGate.promise;
384+
return "ok";
385+
},
386+
});
387+
388+
let step = 0;
389+
const recordingModel = new MockLanguageModelV3({
390+
doStream: async ({ prompt }) => {
391+
prompts.push(
392+
prompt
393+
.filter((m) => m.role === "user")
394+
.flatMap((m) =>
395+
Array.isArray(m.content)
396+
? (m.content as { type: string; text?: string }[])
397+
.filter((c) => c.type === "text")
398+
.map((c) => c.text ?? "")
399+
: []
400+
)
401+
);
402+
const isToolStep = step++ % 2 === 0;
403+
return {
404+
stream: simulateReadableStream({
405+
chunks: isToolStep ? toolCallChunks(`tc-${step}`) : textChunks("done"),
406+
initialDelayInMs: 10,
407+
chunkDelayInMs: 2,
408+
}),
409+
};
410+
},
411+
});
412+
413+
const agent = chat.agent({
414+
id: "steering-injection.next-turn",
415+
pendingMessages: { shouldInject: () => true },
416+
run: async ({ messages, signal }) =>
417+
streamText({
418+
model: recordingModel,
419+
messages,
420+
abortSignal: signal,
421+
...chat.toStreamTextOptions(),
422+
tools: { gate: gateTool },
423+
stopWhen: stepCountIs(5),
424+
}),
425+
});
426+
427+
const harness = mockChatAgent(agent, { chatId });
428+
try {
429+
const first = harness.sendMessage(userMessage("m1", "u-1"));
430+
await waitFor(() => toolEntered, "tool entered");
431+
await sendAndLand(harness, chatId, "steer-me", "u-2");
432+
toolGate.open();
433+
await first;
434+
435+
await waitFor(() => turnCompleteCount(harness) >= 1, "turn 1 complete");
436+
const promptsAfterTurn1 = prompts.length;
437+
438+
// A fresh turn. Its prompt is built from accumulated history, so it
439+
// should still contain the message that was injected into turn 1.
440+
await harness.sendMessage(userMessage("m3", "u-3"));
441+
await waitFor(() => prompts.length > promptsAfterTurn1, "turn 2 prompt built");
442+
443+
const turn2Prompt = prompts[promptsAfterTurn1]!;
444+
expect(turn2Prompt).toContain("steer-me");
445+
} finally {
446+
toolGate.open();
447+
await harness.close();
448+
}
449+
}
450+
);
451+
});

0 commit comments

Comments
 (0)