feat: prepare 2.6.1-rc5 shared databases and build SSE

This commit is contained in:
2026-10-05 11:09:52 +00:00 Unverified
parent f76165603e
commit 828bf3a328
25 changed files with 2557 additions and 196 deletions
+83
View File
@@ -18,6 +18,7 @@ import {
type WorkspaceSnapshot,
} from "../../lib/workspace";
import type { BuildRequest } from "../../shared/build-protocol";
import { ApiStreamUnsupportedError } from "../../lib/api";
const directories: string[] = [];
@@ -582,6 +583,88 @@ describe("authenticated build API pipeline", () => {
expect(calls.some(({ path }) => path.endsWith("/result"))).toBe(true);
});
test("streams one physical build, resumes cursor, and never polls JSON while SSE is active", async () => {
const snapshot = emptySnapshot();
const calls: string[] = [];
const cursors: number[] = [];
const output: string[] = [];
const request: ApiRequester = async <T>(path: string) => {
calls.push(path);
if (path === "/snapshots/negotiate") return { ready: true } as T;
if (path === "/builds") return { state: "queued" } as T;
if (path.endsWith("/result")) return { reference: "image:web", references: { worker: "image:worker" } } as T;
throw new Error(`Unexpected JSON request ${path}`);
};
const result = await buildServices("shop", {
services: { web: { build: "." }, worker: { build: "." } },
}, process.cwd(), { stream: new Writable({ write(chunk, _, done) {
output.push(String(chunk)); done();
} }) }, { request, snapshot, pollIntervalMs: 0, sleep: async () => {},
streamEvents: async function* (_path, after) {
cursors.push(after);
if (after === 0) {
yield { id: 1, event: { type: "log", sequence: 1, id: "build", message: "once\n" } };
yield { event: { type: "status", status: { version: 1, id: "build", state: "running", createdAt: "now" } } };
} else {
yield { id: 1, event: { type: "log", sequence: 1, id: "build", message: "once\n" } };
yield { event: { type: "status", status: { version: 1, id: "build", state: "succeeded", createdAt: "now" } } };
}
},
});
expect(result.images).toEqual({ web: "image:web", worker: "image:worker" });
expect(cursors).toEqual([0, 1]);
expect(output).toEqual(["[web] once\n"]);
expect(calls.filter((path) => path.includes("/events") || path.includes("/reconcile"))).toEqual([]);
});
test("falls back to JSON events/reconcile only after SSE is unsupported", async () => {
const calls: string[] = [];
const request: ApiRequester = async <T>(path: string) => {
calls.push(path);
if (path === "/snapshots/negotiate") return { ready: true } as T;
if (path === "/builds") return { state: "queued" } as T;
if (path.includes("/events")) return [{ type: "log", sequence: 1, message: "fallback\n" }] as T;
if (path.endsWith("/reconcile")) return { state: "succeeded" } as T;
if (path.endsWith("/result")) return { reference: "image:web" } as T;
throw new Error(path);
};
expect((await buildServices("shop", { services: { web: { build: "." } } },
process.cwd(), undefined, { request, snapshot: emptySnapshot(),
streamEvents: async function* () { yield* []; throw new ApiStreamUnsupportedError(); },
})).images).toEqual({ web: "image:web" });
expect(calls.filter((path) => path.includes("/events") || path.includes("/reconcile"))).toEqual([
expect.stringContaining("/events?after=0"),
expect.stringContaining("/reconcile"),
expect.stringContaining("/events?after=1"),
]);
});
test("aborts an active stream and does not issue JSON polling requests", async () => {
const controller = new AbortController();
const reason = new DOMException("Stopped", "AbortError");
let listening!: () => void;
const ready = new Promise<void>((resolve) => { listening = resolve; });
const calls: string[] = [];
const request: ApiRequester = async <T>(path: string) => {
calls.push(path);
if (path === "/snapshots/negotiate") return { ready: true } as T;
if (path === "/builds") return { state: "queued" } as T;
throw new Error(path);
};
const result = buildServices("shop", { services: { web: { build: "." } } },
process.cwd(), undefined, { request, snapshot: emptySnapshot(), signal: controller.signal,
streamEvents: async function* (_path, _after, signal) {
yield* [];
listening();
await new Promise<void>((_resolve, reject) => signal?.addEventListener("abort", () => reject(signal.reason), { once: true }));
},
});
await ready;
controller.abort(reason);
await expect(result).rejects.toBe(reason);
expect(calls).toEqual(["/snapshots/negotiate", "/builds"]);
});
test("builds equivalent services once and attributes events and image results to each", async () => {
const snapshot = emptySnapshot();
const submitted: BuildRequest[] = [];