- Switch conversation agent to xiaomi/mimo-v2.5 (multimodal: text + image) - Render native reasoning parts as live 'Thinking trace' (streaming open, collapsed after completion); inline <think> extraction for streaming models - Image attachments: picker (up to 4, 10MB each), base64 to Flue AgentPromptImage, authenticated blob-URL replay for historical images - Mobile keyboard viewport fix: visual-viewport hook, fixed shell, interactive-widget=resizes-content, header pinned, composer follows keyboard - Conversation to Signal to proposed Work: Convex persistence, Effect validation in @code/work-os, Work cards with exact source provenance - Streamdown markdown + Mermaid chart rendering in chat messages - Flue tool turns hidden, reasoning-containing turns remain visible - Frontend regression tests: keyboard viewport, responsive shell, attachment overflow, authenticated images, reasoning traces, transforms - .env.example updated to xiaomi/mimo-v2.5 config
153 lines
4.9 KiB
TypeScript
153 lines
4.9 KiB
TypeScript
import { createFlueClient } from "@flue/sdk";
|
|
import { describe, expect, test } from "vitest";
|
|
|
|
import { createFlueFetch, generateBrowserRequestId } from "./flue-transport";
|
|
|
|
const BASE_URL = new URL("https://flue.example/api/");
|
|
const GLOBAL_RECEIVER_FETCH = function globalReceiverFetch(this: unknown) {
|
|
if (this !== globalThis) {
|
|
throw new TypeError("fetch receiver must be globalThis");
|
|
}
|
|
return Promise.resolve(new Response(null, { status: 204 }));
|
|
} as typeof fetch;
|
|
|
|
describe("createFlueFetch", () => {
|
|
test("overlapping SDK agent sends keep distinct request IDs", async () => {
|
|
const firstResponse = Promise.withResolvers<Response>();
|
|
const secondResponse = Promise.withResolvers<Response>();
|
|
const bothStarted = Promise.withResolvers<boolean>();
|
|
const capturedHeaders: Headers[] = [];
|
|
let requestCount = 0;
|
|
let requestId = 0;
|
|
|
|
const fetchImpl: typeof fetch = (input, init) => {
|
|
capturedHeaders.push(new Headers(init?.headers));
|
|
requestCount += 1;
|
|
if (requestCount === 2) {
|
|
bothStarted.resolve(true);
|
|
}
|
|
if (requestCount === 1) {
|
|
return firstResponse.promise;
|
|
}
|
|
if (requestCount === 2) {
|
|
return secondResponse.promise;
|
|
}
|
|
throw new Error(`Unexpected request: ${String(input)}`);
|
|
};
|
|
const client = createFlueClient({
|
|
baseUrl: BASE_URL.toString(),
|
|
fetch: createFlueFetch({
|
|
baseUrl: BASE_URL,
|
|
fetchImpl,
|
|
generateRequestId: () => `request-${(requestId += 1)}`,
|
|
}),
|
|
headers: { authorization: "Bearer current-jwt" },
|
|
});
|
|
|
|
const firstSend = client.agents.send("zopu", "org-a", {
|
|
message: "first",
|
|
});
|
|
const secondSend = client.agents.send("zopu", "org-a", {
|
|
message: "second",
|
|
});
|
|
await bothStarted.promise;
|
|
|
|
expect(capturedHeaders).toHaveLength(2);
|
|
expect(capturedHeaders[0]?.get("x-zopu-request-id")).toBe("request-1");
|
|
expect(capturedHeaders[1]?.get("x-zopu-request-id")).toBe("request-2");
|
|
expect(capturedHeaders[0]?.get("authorization")).toBe("Bearer current-jwt");
|
|
expect(capturedHeaders[1]?.get("authorization")).toBe("Bearer current-jwt");
|
|
|
|
firstResponse.resolve(
|
|
Response.json(
|
|
{
|
|
offset: "1",
|
|
streamUrl: "http://internal/streams/first",
|
|
submissionId: "submission-1",
|
|
},
|
|
{ status: 202 }
|
|
)
|
|
);
|
|
secondResponse.resolve(
|
|
Response.json(
|
|
{
|
|
offset: "2",
|
|
streamUrl: "http://internal/streams/second",
|
|
submissionId: "submission-2",
|
|
},
|
|
{ status: 202 }
|
|
)
|
|
);
|
|
|
|
await expect(Promise.all([firstSend, secondSend])).resolves.toEqual([
|
|
{
|
|
offset: "1",
|
|
streamUrl: "https://flue.example/streams/first",
|
|
submissionId: "submission-1",
|
|
},
|
|
{
|
|
offset: "2",
|
|
streamUrl: "https://flue.example/streams/second",
|
|
submissionId: "submission-2",
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("invokes fetch with the browser global receiver", async () => {
|
|
await expect(
|
|
createFlueFetch({ baseUrl: BASE_URL, fetchImpl: GLOBAL_RECEIVER_FETCH })(
|
|
"https://flue.example/api/agents/zopu/org-a?view=history"
|
|
)
|
|
).resolves.toBeInstanceOf(Response);
|
|
});
|
|
|
|
test("uses the default browser request ID generator without losing its receiver", async () => {
|
|
const capturedHeaders: Headers[] = [];
|
|
const fetchImpl: typeof fetch = (_input, init) => {
|
|
capturedHeaders.push(new Headers(init?.headers));
|
|
return Promise.resolve(new Response(null, { status: 204 }));
|
|
};
|
|
|
|
await createFlueFetch({ baseUrl: BASE_URL, fetchImpl })(
|
|
"https://flue.example/api/agents/zopu/org-a",
|
|
{ method: "POST" }
|
|
);
|
|
|
|
expect(capturedHeaders[0]?.get("x-zopu-request-id")).toMatch(
|
|
/^[0-9a-f-]{36}$/u
|
|
);
|
|
});
|
|
|
|
test("generates a UUID without requiring crypto.randomUUID", () => {
|
|
expect(generateBrowserRequestId()).toMatch(
|
|
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u
|
|
);
|
|
});
|
|
|
|
test("history and stream observation requests remain untagged", async () => {
|
|
const capturedHeaders: Headers[] = [];
|
|
const fetchImpl: typeof fetch = (_input, init) => {
|
|
capturedHeaders.push(new Headers(init?.headers));
|
|
return Promise.resolve(new Response(null, { status: 204 }));
|
|
};
|
|
const flueFetch = createFlueFetch({
|
|
baseUrl: BASE_URL,
|
|
fetchImpl,
|
|
generateRequestId: () => "must-not-be-used",
|
|
});
|
|
|
|
await flueFetch("https://flue.example/api/agents/zopu/org-a?view=history", {
|
|
headers: { authorization: "Bearer current-jwt" },
|
|
method: "GET",
|
|
});
|
|
await flueFetch("https://flue.example/api/agents/zopu/org-a?view=updates", {
|
|
headers: { authorization: "Bearer current-jwt" },
|
|
method: "GET",
|
|
});
|
|
|
|
expect(capturedHeaders).toHaveLength(2);
|
|
expect(capturedHeaders[0]?.has("x-zopu-request-id")).toBe(false);
|
|
expect(capturedHeaders[1]?.has("x-zopu-request-id")).toBe(false);
|
|
});
|
|
});
|