feat: prepare 2.6.1-rc5 shared databases and build SSE
This commit is contained in:
@@ -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[] = [];
|
||||
|
||||
Reference in New Issue
Block a user