Files
oh-my-pi/packages/ai/test/bedrock-on-payload.test.ts
can1357 3566bd9b41 fix(ai): unified Cursor interaction-query handling after merging #8889 and #8830
- Kept the shared cursor/interaction-query module as the single handler and deleted the duplicate local implementation in cursor.ts
- Added the named webFetchRequestQuery approval case (field 9 is named under the regenerated proto)
- Preserved the deliberate no-fake-VM-success semantics for setupVmEnvironmentArgs (review of #8047)
- Updated the field-9 regression test to assert the named decode of the raw same-field reply, which also pins the LEN-prefix wire framing
2026-08-19 01:47:20 +02:00

81 lines
2.9 KiB
TypeScript

// Regression: amazon-bedrock ignored the onPayload replacement return value
// (fire-and-forget), so the hook could never change the body actually sent
// upstream. The replacement contract matches anthropic / openai-responses /
// google: await the hook and use its non-undefined return as the request body.
import { describe, expect, it, vi } from "bun:test";
import { streamBedrock } from "@oh-my-pi/pi-ai/providers/amazon-bedrock";
import type { Context, Model } from "@oh-my-pi/pi-ai/types";
import { buildModel } from "@oh-my-pi/pi-catalog/build";
function model(): Model<"bedrock-converse-stream"> {
return buildModel({
id: "us.anthropic.claude-haiku-4-5-20251001-v1:0",
name: "haiku",
api: "bedrock-converse-stream",
provider: "amazon-bedrock",
baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
reasoning: false,
input: ["text"],
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
contextWindow: 1_000_000,
maxTokens: 128_000,
});
}
const context: Context = {
messages: [{ role: "user", content: "hi", timestamp: 0 }],
};
// Capture the serialized body the provider sends. The response is an empty
// event stream: the fetch (and thus the body capture) happens before any
// response parsing, and the stream's outcome is irrelevant to the assertion.
async function captureSentBody(
onPayload: (payload: unknown) => unknown | Promise<unknown>,
): Promise<Record<string, any>> {
const { promise, resolve } = Promise.withResolvers<Record<string, any>>();
const fetchMock = vi.fn(async (_input: string | URL | Request, init?: RequestInit) => {
const body = init?.body;
const text = body instanceof Uint8Array ? new TextDecoder().decode(body) : String(body);
resolve(JSON.parse(text));
return new Response(
new ReadableStream<Uint8Array>({
start(controller) {
controller.close();
},
}),
{ status: 200, headers: { "content-type": "application/vnd.amazon.eventstream" } },
);
}) as unknown as typeof fetch;
const stream = streamBedrock(model(), context, { bearerToken: "test-token", fetch: fetchMock, onPayload });
void (async () => {
try {
for await (const _ of stream) {
// ignore events
}
} catch {
// empty event stream: stream errors are expected and irrelevant
}
})();
return promise;
}
describe("bedrock onPayload replacement", () => {
it("sends an async onPayload replacement body", async () => {
const body = await captureSentBody(async payload => ({
...(payload as Record<string, unknown>),
messages: [{ role: "user", content: [{ text: "replacement" }] }],
}));
expect(body.messages).toEqual([{ role: "user", content: [{ text: "replacement" }] }]);
expect(JSON.stringify(body.messages)).not.toContain("hi");
}, 10_000);
it("keeps the original body when onPayload returns undefined", async () => {
const body = await captureSentBody(async () => undefined);
expect(body.messages[0].content[0].text).toBe("hi");
}, 10_000);
});