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
+77 -37
View File
@@ -4,6 +4,7 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Writable } from "node:stream";
import { stripVTControlCharacters } from "node:util";
import type { ApiRequestInit, ApiRequestOptions } from "../../lib/api";
import { KuberApiError } from "../../lib/api";
import type { ApiRequester } from "../../lib/build";
@@ -25,8 +26,21 @@ const snapshot = {
blobs: [],
};
// DefaultRenderer.create returns a complete TTY frame before the cursor redraw.
function frameRows(frame: string): string[] {
return stripVTControlCharacters(frame).split("\n");
}
function taskRow(rows: string[], title: string): string {
return rows.find((row) => row.includes(title)) ?? "";
}
function depth(row: string): number {
return row.match(/^\s*/)?.[0].length ?? 0;
}
describe("up API pipeline", () => {
test("keeps queued builds in the TTY render tree without duplicate rows", async () => {
test("renders build lifecycle in child titles without duplicate phase output", async () => {
const root = await mkdtemp(join(tmpdir(), "kuber-up-api-"));
const previousCwd = process.cwd();
const tty = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
@@ -37,9 +51,7 @@ describe("up API pipeline", () => {
frames.push(frame);
return frame;
});
const writes = spyOn(process.stdout, "write").mockImplementation(((chunk: string | Uint8Array) => {
return true;
}) as typeof process.stdout.write);
const writes = spyOn(process.stdout, "write").mockImplementation((() => true) as typeof process.stdout.write);
const polls = new Map<string, number>();
const builds = new Map<string, string>();
try {
@@ -60,13 +72,13 @@ describe("up API pipeline", () => {
if (path.includes("/events"))
return [{ type: "status", status: {
state: "queued",
phase: ["queued", "preparing", "waiting", "waiting"][polls.get(service) ?? 0],
phase: ["queued", "creating", "starting", "running", "done"][polls.get(service) ?? 0],
} }] as T;
if (path.endsWith("/reconcile")) {
const count = (polls.get(service) ?? 0) + 1;
polls.set(service, count);
await Bun.sleep(110);
return count < 3
return count < 4
? { state: "queued" } as T
: service === "client"
? { state: "failed", error: "build stopped\nstack detail" } as T
@@ -76,29 +88,27 @@ describe("up API pipeline", () => {
throw new Error(path);
};
await expect(provideContext(() => runUp(true, request, { trust }))).rejects.toThrow("build stopped");
expect(polls.get("client")).toBe(3);
const active = frames.filter((frame) =>
frame.includes("Build images") && frame.includes("Build app") && frame.includes("Build client") &&
frame.split("\n").filter((row) => row.includes("Build queued")).length === 2);
expect(polls.get("client")).toBe(4);
const active = frames.map(frameRows).filter((rows) =>
taskRow(rows, "Build images") && taskRow(rows, "Build app") && taskRow(rows, "Build client"));
expect(active.length).toBeGreaterThan(0);
for (const frame of frames.filter((frame) => frame.includes("Build images") && frame.includes("Build app") && frame.includes("Build client"))) {
const rows = frame.split("\n");
const parent = rows.find((row) => row.includes("Build images"))!;
for (const rows of active) {
const parent = taskRow(rows, "Build images");
for (const name of ["app", "client"]) {
const children = rows.filter((row) => row.includes(`Build ${name}`));
expect(children).toHaveLength(1);
expect(children[0]!.search(/\S/)).toBeGreaterThan(parent.search(/\S/));
expect(depth(children[0]!)).toBeGreaterThan(depth(parent));
}
expect(rows.filter((row) => row.includes("Build queued")).length).toBeLessThanOrEqual(2);
expect(rows.filter((row) => /^\s*› Build (?:queued|creating|starting|running|done)/.test(row))).toHaveLength(0);
}
expect(active[0]).toMatch(/Build app\n {2,}.*Build queued[\s\S]*Build client\n {2,}.*Build queued/);
const final = frames.at(-1)!;
expect(final).toMatch(/Build images[\s\S]* .*Build app[\s\S]* .*Build client/);
expect(final).toMatch(/✔.*Build app/);
expect(final).toMatch(/✖.*Build client/);
expect(frames.some((frame) => frame.includes("Build preparing") && frame.includes("Build waiting"))).toBe(true);
expect(final).toContain("build stopped");
expect(final).toContain("stack detail");
for (const phase of ["queued", "creating", "starting", "running", "done"])
expect(frames.some((frame) => frame.includes(`Build app: ${phase}`))).toBe(true);
const final = frameRows(frames.at(-1)!);
expect(taskRow(final, "Build app: done")).toMatch(/✔/);
expect(taskRow(final, "Build client: failed")).toMatch(/✖/);
expect(final.join("\n")).toContain("build stopped");
expect(final.join("\n")).toContain("stack detail");
expect(final.join("\n").split("build stopped")).toHaveLength(2);
} finally {
renderSpy.mockRestore();
writes.mockRestore();
@@ -292,11 +302,18 @@ describe("up API pipeline", () => {
const order: string[] = [];
const started: string[] = [];
let rootTasks: Listr["tasks"] | undefined;
let backingTasks: Listr["tasks"] | undefined;
const originalRun = Listr.prototype.run;
const frames: string[] = [];
const create = DefaultRenderer.prototype.create;
const renderSpy = spyOn(DefaultRenderer.prototype, "create").mockImplementation(function (this: DefaultRenderer, options) {
const frame = create.call(this, options);
frames.push(frame);
return frame;
});
const tty = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
const writes = spyOn(process.stdout, "write").mockImplementation((() => true) as typeof process.stdout.write);
const runSpy = spyOn(Listr.prototype, "run").mockImplementation(function (this: Listr) {
if (this.tasks[0]?.title === "Read compose") rootTasks = this.tasks;
if (this.tasks[0]?.title === "Reconcile databases") backingTasks = this.tasks;
return originalRun.call(this);
});
let bothReady!: () => void;
@@ -312,6 +329,7 @@ describe("up API pipeline", () => {
releaseStorage = resolve;
});
try {
Object.defineProperty(process.stdout, "isTTY", { configurable: true, value: true });
await writeFile(
join(root, "compose.yml"),
"services:\n app:\n image: nginx\n volumes:\n - postgresql:app\n - s3:assets\n",
@@ -356,10 +374,21 @@ describe("up API pipeline", () => {
expect(titles.indexOf("Render manifests")).toBeLessThan(
titles.indexOf("Reconcile resources"),
);
expect(backingTasks!.map(({ title }) => title)).toEqual([
const backing = rootTasks!.find(({ title }) => title === "Reconcile backing services")!;
expect(backing.subtasks.map(({ title }) => title)).toEqual([
"Reconcile databases",
"Reconcile S3 storage",
]);
const active = frames.map(frameRows).filter((rows) =>
taskRow(rows, "Reconcile backing services") &&
taskRow(rows, "Reconcile databases") &&
taskRow(rows, "Reconcile S3 storage"));
expect(active.length).toBeGreaterThan(0);
for (const rows of active) {
const parent = taskRow(rows, "Reconcile backing services");
expect(depth(taskRow(rows, "Reconcile databases"))).toBeGreaterThan(depth(parent));
expect(depth(taskRow(rows, "Reconcile S3 storage"))).toBeGreaterThan(depth(parent));
}
expect(rootTasks!.filter(({ title }) => title === "Reconcile databases")).toEqual([]);
expect(started).toEqual(["database", "storage"]);
expect(order.indexOf("/workspaces/shop/adopt")).toBeLessThan(
@@ -388,6 +417,10 @@ describe("up API pipeline", () => {
}
} finally {
runSpy.mockRestore();
renderSpy.mockRestore();
writes.mockRestore();
if (tty) Object.defineProperty(process.stdout, "isTTY", tty);
else Reflect.deleteProperty(process.stdout, "isTTY");
process.chdir(previousCwd);
await rm(root, { recursive: true, force: true });
}
@@ -396,17 +429,18 @@ describe("up API pipeline", () => {
test("shows database API problem details on the database task without starting resources", async () => {
const root = await mkdtemp(join(tmpdir(), "kuber-up-api-"));
const previousCwd = process.cwd();
const rendered: string[] = [];
const writes = spyOn(process.stdout, "write").mockImplementation(((chunk: string | Uint8Array) => {
rendered.push(String(chunk));
return true;
}) as typeof process.stdout.write);
const errors = spyOn(process.stderr, "write").mockImplementation(((chunk: string | Uint8Array) => {
rendered.push(String(chunk));
return true;
}) as typeof process.stderr.write);
const frames: string[] = [];
const create = DefaultRenderer.prototype.create;
const renderSpy = spyOn(DefaultRenderer.prototype, "create").mockImplementation(function (this: DefaultRenderer, options) {
const frame = create.call(this, options);
frames.push(frame);
return frame;
});
const tty = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
const writes = spyOn(process.stdout, "write").mockImplementation((() => true) as typeof process.stdout.write);
const calls: string[] = [];
try {
Object.defineProperty(process.stdout, "isTTY", { configurable: true, value: true });
await writeFile(join(root, "compose.yml"), "services:\n web:\n image: nginx\n volumes:\n - postgresql:web_db\n");
await writeFile(join(root, ".kuberrc.ts"), 'export default { project: "shop" };\n');
process.chdir(root);
@@ -423,11 +457,17 @@ describe("up API pipeline", () => {
throw new Error(`Unexpected request: ${path}`);
};
await expect(provideContext(() => runUp(false, request, { trust }))).rejects.toThrow(detail);
expect(rendered.join("")).toContain("database apply for database web_db");
const rows = frameRows(frames.at(-1)!);
expect(depth(taskRow(rows, "Reconcile databases"))).toBeGreaterThan(depth(taskRow(rows, "Reconcile backing services")));
expect(taskRow(rows, "Reconcile databases")).toMatch(/✖/);
const visible = rows.join(" ").replace(/\s+/g, " ");
expect(visible.split(detail)).toHaveLength(2);
expect(calls).not.toContain("/workspaces/shop/resources/plan");
} finally {
errors.mockRestore();
renderSpy.mockRestore();
writes.mockRestore();
if (tty) Object.defineProperty(process.stdout, "isTTY", tty);
else Reflect.deleteProperty(process.stdout, "isTTY");
process.chdir(previousCwd);
await rm(root, { recursive: true, force: true });
}
+67
View File
@@ -0,0 +1,67 @@
import { afterEach, expect, test } from "bun:test";
import { apiStreamEvents, ApiStreamUnsupportedError } from "../../lib/api";
const originalFetch = globalThis.fetch;
afterEach(() => {
globalThis.fetch = originalFetch;
});
test("parses split UTF-8 SSE frames, comments and ids with authenticated request", async () => {
let headers: Headers | undefined;
const versions: Array<string | null> = [];
globalThis.fetch = (async (
_url: string | URL | Request,
init?: RequestInit,
) => {
headers = new Headers(init?.headers);
const bytes = new TextEncoder().encode(
': ping\r\nid: 3\r\nevent: log\r\ndata: {"type":"log","message":"hé"}\r\n\r\nevent: status\ndata: {"type":"status"}\n\n',
);
return new Response(
new ReadableStream({
start(controller) {
for (let i = 0; i < bytes.length; i += 2)
controller.enqueue(bytes.slice(i, i + 2));
controller.close();
},
}),
{
headers: {
"content-type": "text/event-stream",
"x-kuber-version": "2.6.1-rc5",
},
},
);
}) as unknown as typeof fetch;
const events = [];
for await (const event of apiStreamEvents("/builds/a/events", 2, undefined, {
baseUrl: "https://test",
session: { token: "secret" } as never,
onServerVersion: (value) => versions.push(value),
}))
events.push(event);
expect(headers?.get("authorization")).toBe("Bearer secret");
expect(headers?.get("last-event-id")).toBe("2");
expect(versions).toEqual(["2.6.1-rc5"]);
expect(events).toEqual([
{ id: 3, event: { type: "log", message: "hé" } },
{ id: undefined, event: { type: "status" } },
]);
});
test("identifies existing JSON-only servers without parsing their response as SSE", async () => {
globalThis.fetch = (async () =>
new Response("[]", {
headers: { "content-type": "application/json" },
})) as unknown as typeof fetch;
await expect(
(async () => {
for await (const _ of apiStreamEvents("/builds/a/events", 0, undefined, {
baseUrl: "https://test",
session: { token: "secret" } as never,
})) {
/* consume */
}
})(),
).rejects.toBeInstanceOf(ApiStreamUnsupportedError);
});
+68
View File
@@ -7,6 +7,7 @@ import {
apiUpload,
} from "../../lib/api";
import type { KuberSession } from "../../lib/session";
import { createVersionObserver } from "../../lib/version-update";
const originalFetch = globalThis.fetch;
const session: KuberSession = {
@@ -19,6 +20,73 @@ afterEach(() => {
globalThis.fetch = originalFetch;
});
test("observes the response header on JSON, errors, uploads and streaming without delaying payloads", async () => {
const seen: Array<string | null> = [];
const onServerVersion = (version: string | null) => {
seen.push(version);
};
const options = {
authenticated: false,
baseUrl: "https://api.test",
onServerVersion,
};
const encoder = new TextEncoder();
globalThis.fetch = mock(async (input) => {
const path = new URL(String(input)).pathname;
const headers = { "x-kuber-version": "2.7.0" };
if (path === "/error")
return Response.json({ detail: "failure" }, { status: 503, headers });
if (path === "/upload") return new Response(null, { status: 204, headers });
if (path === "/stream")
return new Response(
new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode('{"n":1}\n'));
},
}),
{ headers },
);
return Response.json({ ok: true }, { headers });
}) as unknown as typeof fetch;
expect(await apiRequest<{ ok: boolean }>("/json", {}, options)).toEqual({
ok: true,
});
await expect(apiRequest("/error", {}, options)).rejects.toBeInstanceOf(
KuberApiError,
);
await apiUpload("/upload", new Uint8Array([1]), { ...options, offset: 0 });
let complete!: (success: boolean) => void;
const pending = new Promise<boolean>((resolve) => {
complete = resolve;
});
const lines: string[] = [];
const observer = createVersionObserver({
currentVersion: "2.6.1",
runner: () => pending,
report: (line) => lines.push(line),
});
const records = apiStreamNdjson<{ n: number }>(
"/stream",
{},
{
...options,
onServerVersion: (version) => {
onServerVersion(version);
observer(version);
},
},
);
expect((await records.next()).value).toEqual({ n: 1 });
expect(seen).toEqual(["2.7.0", "2.7.0", "2.7.0", "2.7.0"]);
expect(lines).toEqual([]);
await records.return();
complete(false);
await new Promise<void>((resolve) => setTimeout(resolve, 0));
expect(lines).toEqual(["\x1b[90m+ New version available: 2.7.0\x1b[0m\n"]);
});
describe("authenticated API transport", () => {
test("sends authentication and serializes JSON", async () => {
const fetchMock = mock(
+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[] = [];
+158 -3
View File
@@ -8,6 +8,7 @@ import {
getServicePostgresClaim,
isPostgresVolumeEntry,
reconcilePostgresClaim,
reconcilePostgresClaims,
} from "../../lib/database";
import { objectApi } from "../../lib/k8s";
@@ -127,6 +128,7 @@ describe("managed PostgreSQL claims", () => {
secretName: "postgres-app",
};
spyOn(objectApi, "read").mockImplementation(async (resource) => {
if (resource.kind === "Database") throw { code: 404 };
if (resource.kind === "Secret") {
return {
...resource,
@@ -141,6 +143,7 @@ describe("managed PostgreSQL claims", () => {
const patch = spyOn(objectApi, "patch").mockImplementation(
async (resource) => resource as never,
);
const create = spyOn(objectApi, "create").mockImplementation(async (resource) => resource as never);
expect(await reconcilePostgresClaim("project", claim)).toEqual({
username: "app",
@@ -149,8 +152,8 @@ describe("managed PostgreSQL claims", () => {
expect(patch.mock.calls.map(([resource]) => resource.kind)).toEqual([
"Secret",
"Cluster",
"Database",
]);
expect(create.mock.calls.map(([resource]) => resource.kind)).toEqual(["Database"]);
});
test("reports database apply phase and claim without exposing provider credentials", async () => {
@@ -161,12 +164,14 @@ describe("managed PostgreSQL claims", () => {
secretName: "postgres-app_role",
};
spyOn(objectApi, "read").mockImplementation(async (resource) => {
if (resource.kind === "Database") throw { code: 404 };
if (resource.kind === "Secret") {
return { ...resource, data: { password: Buffer.from("private-value").toString("base64") } } as never;
}
return { ...resource, spec: { managed: { roles: [] } } } as never;
});
spyOn(objectApi, "patch").mockImplementation(async (resource) => {
spyOn(objectApi, "patch").mockImplementation(async (resource) => resource as never);
spyOn(objectApi, "create").mockImplementation(async (resource) => {
if (resource.kind === "Database") {
throw Object.assign(new Error("Forbidden: password=private-value"), { code: 403 });
}
@@ -180,11 +185,78 @@ describe("managed PostgreSQL claims", () => {
}
expect(failure).toBeInstanceOf(DatabaseReconciliationError);
expect((failure as Error).message).toBe(
"Database reconciliation failed during database apply for database app_db (service app, role app_role): Forbidden (HTTP 403)",
"Database reconciliation failed during database apply for requested database claim: Forbidden (HTTP 403)",
);
expect((failure as Error).message).not.toContain("private-value");
});
test.each(["read", "patch"])("distinguishes role secret %s failure", async (method) => {
const secretLike = "DB_PASSWORD_private123";
spyOn(objectApi, "read").mockImplementation(async (resource) => {
if (resource.kind === "Database") throw { code: 404 };
if (method === "read") throw new Error(`token=${secretLike}`);
return undefined as never;
});
spyOn(objectApi, "patch").mockImplementation(async () => {
throw new Error(`token=${secretLike}`);
});
let failure: unknown;
try {
await reconcilePostgresClaim("project", {
service: secretLike, username: secretLike, database: secretLike, secretName: `postgres-${secretLike}`,
});
} catch (error) { failure = error; }
expect(failure).toBeInstanceOf(DatabaseReconciliationError);
expect((failure as DatabaseReconciliationError).phase).toBe(`role secret ${method === "read" ? "lookup" : "apply"}`);
expect((failure as Error).message).not.toContain(secretLike);
});
test("uses safe provider status and reason without exposing request bodies or unsafe claim identifiers", () => {
const cause = Object.assign(new Error("request body DATABASE_URL=postgresql://admin:[email protected]/app"), {
statusCode: 422,
body: { reason: "Invalid", code: 422, message: "token=private" },
});
const failure = new DatabaseReconciliationError("managed role update", cause, {
service: "web", database: "postgresql://admin:[email protected]/app", username: "web_role", secretName: "secret",
});
expect(failure.message).toBe(
"Database reconciliation failed during managed role update for requested database claim: Invalid (HTTP 422)",
);
expect(failure.cause).toBe(cause);
expect(failure.message).not.toContain("private");
});
test("rejects an unrecognized phase containing a syntactically valid secret-like name", () => {
const failure = new DatabaseReconciliationError("DB_PASSWORD_private123", new Error("private"));
expect(failure.phase).toBe("operation execution");
expect(failure.message).not.toContain("DB_PASSWORD_private123");
});
test("identifies failure while preparing malformed managed roles before a Cluster write", async () => {
spyOn(objectApi, "read").mockImplementation(async (resource) => resource.kind === "Secret"
? { ...resource, data: { password: Buffer.from("private").toString("base64") } } as never
: resource.kind === "Database" ? Promise.reject({ code: 404 }) : { ...resource, spec: { managed: { roles: {} } } } as never);
const patch = spyOn(objectApi, "patch").mockImplementation(async (resource) => resource as never);
let failure: unknown;
try {
await reconcilePostgresClaim("project", {
service: "DB_PASSWORD_private123", username: "role", database: "db", secretName: "postgres-role",
});
} catch (error) { failure = error; }
expect(failure).toBeInstanceOf(DatabaseReconciliationError);
expect((failure as DatabaseReconciliationError).phase).toBe("managed role preparation");
expect((failure as Error).message).not.toContain("DB_PASSWORD_private123");
expect(patch.mock.calls.map(([resource]) => resource.kind)).toEqual(["Secret"]);
});
test("reports malformed claims as claim discovery failures without echoing compose input", async () => {
await expect(reconcilePostgresClaims("project", {
services: { web: { volumes: ["postgresql:user:password=private"] } },
} as ComposeSpecification)).rejects.toThrow(
"Database reconciliation failed during claim discovery: Check PostgreSQL claim declarations",
);
});
test("reconciles a CNPG cluster returned with managed fields without sending them back", async () => {
const claim = {
service: "app",
@@ -193,6 +265,7 @@ describe("managed PostgreSQL claims", () => {
secretName: "postgres-app",
};
spyOn(objectApi, "read").mockImplementation(async (resource) => {
if (resource.kind === "Database") throw { code: 404 };
if (resource.kind === "Secret") {
return {
...resource,
@@ -223,6 +296,7 @@ describe("managed PostgreSQL claims", () => {
return resource as never;
},
);
spyOn(objectApi, "create").mockImplementation(async (resource) => resource as never);
await reconcilePostgresClaim("project", claim);
@@ -239,4 +313,85 @@ describe("managed PostgreSQL claims", () => {
expect(cluster?.metadata).not.toHaveProperty("resourceVersion");
expect(cluster).not.toHaveProperty("status");
});
test("two projects reuse a foreign database without applying its metadata", async () => {
const claim = { service: "web", username: "sastify", database: "sastify-store", secretName: "postgres-sastify" };
const live = {
apiVersion: "postgresql.cnpg.io/v1", kind: "Database",
metadata: { name: claim.database, namespace: "database", labels: {
"kuber.dev/project": "sastify-api", "kuber.dev/workspace-uid": "foreign-uid",
} },
spec: { owner: claim.username, cluster: { name: "postgres" } },
};
const original = structuredClone(live);
spyOn(objectApi, "read").mockImplementation(async (resource) => resource.kind === "Database"
? live as never
: resource.kind === "Secret"
? { ...resource, data: { password: Buffer.from("shared-password").toString("base64") } } as never
: { ...resource, spec: { managed: { roles: [] } } } as never);
const patch = spyOn(objectApi, "patch").mockImplementation(async (resource) => resource as never);
const create = spyOn(objectApi, "create").mockImplementation(async (resource) => resource as never);
const compose = { services: { web: { volumes: ["postgresql:sastify/sastify-store"] } } } as ComposeSpecification;
for (const project of ["sastify-api", "another-project"]) {
const env = await reconcilePostgresClaims(project, compose);
expect(env.web?.DATABASE_URL).toBe("postgresql://sastify:[email protected]:5432/sastify-store");
}
expect(live).toEqual(original);
expect(patch.mock.calls.map(([resource]) => resource.kind)).toEqual(["Secret", "Cluster", "Secret", "Cluster"]);
expect(create).not.toHaveBeenCalled();
});
test("validates all databases in a compose file before writing the first role", async () => {
const read = spyOn(objectApi, "read").mockImplementation(async (resource) => {
if (resource.kind === "Database" && resource.metadata?.name === "first") throw { code: 404 };
if (resource.kind === "Database") return { ...resource, spec: {
owner: "another-role", cluster: { name: "postgres" },
} } as never;
throw new Error("Role lookup must not run");
});
const patch = spyOn(objectApi, "patch").mockImplementation(async (resource) => resource as never);
const create = spyOn(objectApi, "create").mockImplementation(async (resource) => resource as never);
await expect(reconcilePostgresClaims("project", { services: {
first: { volumes: ["postgresql:first"] },
second: { volumes: ["postgresql:second"] },
} } as ComposeSpecification)).rejects.toMatchObject({ phase: "database ownership" });
expect(read.mock.calls.map(([resource]) => resource.kind)).toEqual(["Database", "Database"]);
expect(patch).not.toHaveBeenCalled();
expect(create).not.toHaveBeenCalled();
});
test.each([
["wrong role", "other", "postgres"],
["wrong cluster", "sastify", "other"],
])("rejects existing database with %s before any writes", async (_case, owner, cluster) => {
const claim = { service: "web", username: "sastify", database: "sastify-store", secretName: "postgres-sastify" };
spyOn(objectApi, "read").mockImplementation(async (resource) => resource.kind === "Database"
? { ...resource, spec: { owner, cluster: { name: cluster } } } as never
: undefined as never);
const patch = spyOn(objectApi, "patch").mockImplementation(async (resource) => resource as never);
const create = spyOn(objectApi, "create").mockImplementation(async (resource) => resource as never);
await expect(reconcilePostgresClaim("project", claim)).rejects.toMatchObject({ phase: "database ownership" });
expect(patch).not.toHaveBeenCalled();
expect(create).not.toHaveBeenCalled();
});
test("creates missing databases only once and fails closed on create races", async () => {
spyOn(objectApi, "read").mockImplementation(async (resource) => {
if (resource.kind === "Database") throw { code: 404 };
return { ...resource, spec: { managed: { roles: [] } } } as never;
});
spyOn(objectApi, "patch").mockImplementation(async (resource) => resource as never);
const create = spyOn(objectApi, "create").mockImplementation(async () => {
throw Object.assign(new Error("Conflict"), { code: 409 });
});
await expect(reconcilePostgresClaims("project", { services: {
web: { volumes: ["postgresql:sastify/sastify-store"] },
worker: { volumes: ["postgresql:sastify/sastify-store"] },
} } as ComposeSpecification)).rejects.toMatchObject({ phase: "database apply" });
expect(create).toHaveBeenCalledTimes(1);
expect(create.mock.calls[0]?.[0]).toMatchObject({
metadata: { name: "sastify-store", labels: { "kuber.dev/project": "project" } },
spec: { owner: "sastify", cluster: { name: "postgres" } },
});
});
});
+51
View File
@@ -0,0 +1,51 @@
import { expect, test } from "bun:test";
test("a short CLI exits while its version updater remains pending", async () => {
const moduleUrl = new URL("../../lib/version-update.ts", import.meta.url)
.href;
const script = `
const { createVersionObserver } = await import(${JSON.stringify(moduleUrl)});
createVersionObserver({
currentVersion: "2.6.1-rc3",
runner: () => new Promise(() => {}),
})("2.7.0");
`;
const child = Bun.spawn([process.execPath, "-e", script], {
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
});
const exit = await Promise.race([
child.exited,
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("short CLI stayed alive")), 1_000),
),
]);
expect(exit).toBe(0);
expect(await new Response(child.stderr).text()).toBe("");
});
test.each([
["success", "return true", "+ Updated to 2.7.0"],
["failure", "throw new Error('install failed')", "+ New version available: 2.7.0"],
])("reports exactly one %s outcome when it settles", async (_name, outcome, message) => {
const moduleUrl = new URL("../../lib/version-update.ts", import.meta.url).href;
const script = `
const { createVersionObserver } = await import(${JSON.stringify(moduleUrl)});
createVersionObserver({
currentVersion: "2.6.1-rc3",
runner: async () => { ${outcome}; },
})("2.7.0");
await new Promise((resolve) => setTimeout(resolve, 25));
`;
const child = Bun.spawn([process.execPath, "-e", script], {
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
});
expect(await child.exited).toBe(0);
const stderr = await new Response(child.stderr).text();
expect(stderr.split("\n").filter((line) => line.includes(message))).toHaveLength(1);
});
+201
View File
@@ -0,0 +1,201 @@
import { describe, expect, test } from "bun:test";
import {
compareVersions,
createVersionObserver,
installVersion,
} from "../../lib/version-update";
const tick = () => new Promise<void>((resolve) => setTimeout(resolve, 0));
describe("server version updates", () => {
test("compares strict SemVer precedence including prereleases and ignores build metadata", () => {
expect(compareVersions("2.6.1", "2.6.1-rc3")).toBe(1);
expect(compareVersions("2.6.1-rc.10", "2.6.1-rc.3")).toBe(1);
expect(compareVersions("2.6.1-alpha.1", "2.6.1-alpha.beta")).toBe(-1);
expect(compareVersions("2.6.1-rc3.1", "2.6.1-rc3")).toBe(1);
expect(compareVersions("2.6.1+new", "2.6.1+old")).toBe(0);
expect(compareVersions("3.0.0", "2.6.1")).toBe(1);
for (const invalid of [
"",
"v3.0.0",
"3.0",
"03.0.0",
"3.0.0-rc.01",
"3.0.0-",
"3.0.0+",
"3.0.0;touch /tmp/pwned",
"3.0.0\n",
"3.0.0-💥",
"9".repeat(129) + ".0.0",
]) {
expect(compareVersions(invalid, "2.6.1-rc3")).toBeUndefined();
}
});
test("rejects invalid install arguments before spawning", async () => {
expect(await installVersion("2.7.0;echo hacked")).toBe(false);
});
test("uses a quiet detached timeout argument vector", async () => {
const calls: string[][] = [];
let unrefs = 0;
const spawn = (
argv: string[],
options: {
stdin: "ignore";
stdout: "ignore";
stderr: "ignore";
detached: true;
},
) => {
calls.push(argv);
expect(options).toEqual({
stdin: "ignore",
stdout: "ignore",
stderr: "ignore",
detached: true,
});
return {
exited: Promise.resolve(0),
unref: () => {
unrefs++;
},
};
};
expect(await installVersion("2.7.0-rc.1+build.2", spawn, 1)).toBe(true);
expect(calls).toEqual([
[
"timeout",
"--signal=TERM",
"--kill-after=2s",
"1s",
"bun",
"i",
"-g",
"--no-cache",
"@dmgnr/[email protected]+build.2",
],
]);
expect(unrefs).toBe(1);
});
test("ignores malformed, equal, and older headers without attempting an install", async () => {
const runs: string[] = [];
const lines: string[] = [];
const observe = createVersionObserver({
currentVersion: "2.6.1-rc3",
runner: async (version) => {
runs.push(version);
return true;
},
report: (line) => lines.push(line),
});
for (const version of [
null,
"garbage",
"2.6.1-rc3",
"2.6.0",
"2.6.1-rc2",
"2.6.1-rc3+build",
])
observe(version);
await tick();
expect(runs).toEqual([]);
expect(lines).toEqual([]);
});
test("starts once for concurrent newer responses, reports success in gray on stderr", async () => {
let finish!: (value: boolean) => void;
const pending = new Promise<boolean>((resolve) => {
finish = resolve;
});
const runs: string[] = [];
const lines: string[] = [];
const observe = createVersionObserver({
currentVersion: "2.6.1-rc3",
runner: (version) => {
runs.push(version);
return pending;
},
report: (line) => lines.push(line),
});
observe("2.6.1");
observe("9.0.0");
expect(runs).toEqual([]);
await tick();
expect(runs).toEqual(["2.6.1"]);
expect(lines).toEqual([]);
finish(true);
await tick();
expect(lines).toEqual(["\x1b[90m+ Updated to 2.6.1\x1b[0m\n"]);
observe("10.0.0");
await tick();
expect(runs).toHaveLength(1);
});
test("reports failure or rejection once without throwing into requests", async () => {
for (const runner of [
async () => false,
async () => {
throw new Error("offline");
},
]) {
const lines: string[] = [];
const observe = createVersionObserver({
currentVersion: "2.6.1-rc3",
runner,
report: (line) => lines.push(line),
});
observe("2.7.0");
observe("2.8.0");
await tick();
expect(lines).toEqual([
"\x1b[90m+ New version available: 2.7.0\x1b[0m\n",
]);
}
});
test("a detached pending install does not delay a short CLI or pollute its streams", async () => {
const url = new URL("../../lib/version-update.ts", import.meta.url).href;
const script = `
const { createVersionObserver } = await import(${JSON.stringify(url)});
createVersionObserver({ currentVersion: '2.6.1',
runner: () => new Promise(() => {}) })('2.7.0');
`;
const child = Bun.spawn([process.execPath, "-e", script], {
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
});
const timeout = setTimeout(() => child.kill(), 1_000);
try {
expect(await child.exited).toBe(0);
expect(await new Response(child.stdout).text()).toBe("");
expect(await new Response(child.stderr).text()).toBe("");
} finally {
clearTimeout(timeout);
}
});
test("a short subprocess emits one outcome to stderr when its runner settles", async () => {
const url = new URL("../../lib/version-update.ts", import.meta.url).href;
for (const result of ["true", "false", "reject"]) {
const script = `
const { createVersionObserver } = await import(${JSON.stringify(url)});
createVersionObserver({ currentVersion: '2.6.1',
runner: async () => ${result === "reject" ? "Promise.reject(Error('offline'))" : result} })('2.7.0');
await new Promise(resolve => setTimeout(resolve, 20));
`;
const child = Bun.spawn([process.execPath, "-e", script], {
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
});
expect(await child.exited).toBe(0);
expect(await new Response(child.stdout).text()).toBe("");
expect(await new Response(child.stderr).text()).toBe(
`\x1b[90m+ ${result === "true" ? "Updated to " : "New version available: "}2.7.0\x1b[0m\n`,
);
}
});
});
+24 -20
View File
@@ -7,7 +7,11 @@ import type { ManagementService } from "../../server/management";
import { MemoryTrustStore } from "../../server/trust-store";
import { MemoryWorkspaceStore } from "../../server/workspace-store";
const now = Date.parse("2026-09-05T00:00:00.000Z");
const now = Date.parse("2030-09-05T00:00:00.000Z");
const finiteExpiry = new Date(now + 30 * 24 * 60 * 60 * 1000).toISOString();
const delegatedLaterExpiry = new Date(
now + 31 * 24 * 60 * 60 * 1000,
).toISOString();
function request(path: string, init: RequestInit = {}, token = "admin-token") {
const headers = new Headers(init.headers);
@@ -33,7 +37,7 @@ async function setup() {
tokenHash: hashToken("admin-token"),
username: "admin",
authVersion: 1,
expiresAt: "2026-10-05T00:00:00.000Z",
expiresAt: finiteExpiry,
});
return {
store,
@@ -86,7 +90,7 @@ describe("API keys", () => {
tokenHash: hashToken("ci-key"),
username: "ci",
capabilities: ["kubernetes:read"],
expiresAt: "2026-10-05T00:00:00.000Z",
expiresAt: finiteExpiry,
});
expect((await app(request("/api/v2/users", {}, "ci-key"))).status).toBe(
403,
@@ -104,7 +108,7 @@ describe("API keys", () => {
username: "ci",
capabilities: ["users:write", "kubernetes:read"],
workspace: "shop",
expiresAt: "2026-10-05T00:00:00.000Z",
expiresAt: finiteExpiry,
});
const create = (body: unknown) =>
app(
@@ -149,14 +153,14 @@ describe("API keys", () => {
const laterExpiry = await create({
capabilities: ["kubernetes:read"],
workspace: "shop",
expiresAt: "2026-10-06T00:00:00.000Z",
expiresAt: delegatedLaterExpiry,
});
expect(laterExpiry.status).toBe(403);
const subset = await create({
capabilities: ["kubernetes:read"],
workspace: "shop",
expiresAt: "2026-10-05T00:00:00.000Z",
expiresAt: finiteExpiry,
});
expect(subset.status).toBe(201);
expect(
@@ -194,7 +198,7 @@ describe("API keys", () => {
tokenHash: hashToken("unscoped-delegation"),
username: "ci",
capabilities: ["users:write", "kubernetes:read"],
expiresAt: "2026-10-05T00:00:00.000Z",
expiresAt: finiteExpiry,
});
const unscopedSubset = await app(
request(
@@ -204,7 +208,7 @@ describe("API keys", () => {
headers: { "content-type": "application/json" },
body: JSON.stringify({
capabilities: ["kubernetes:read"],
expiresAt: "2026-09-20T00:00:00.000Z",
expiresAt: new Date(now + 15 * 24 * 60 * 60 * 1000).toISOString(),
}),
},
"unscoped-delegation",
@@ -248,7 +252,7 @@ describe("API keys", () => {
tokenHash: hashToken("expired-key"),
username: "ci",
capabilities: ["kubernetes:read"],
expiresAt: "2026-09-04T00:00:00.000Z",
expiresAt: new Date(now - 24 * 60 * 60 * 1000).toISOString(),
});
expect((await app(request("/api/v2/me", {}, "expired-key"))).status).toBe(
401,
@@ -261,7 +265,7 @@ describe("API keys", () => {
headers: { "content-type": "application/json" },
body: JSON.stringify({
capabilities: ["kubernetes:read"],
expiresAt: "2027-09-06T00:00:00.000Z",
expiresAt: new Date(now + 366 * 24 * 60 * 60 * 1000).toISOString(),
}),
}),
)
@@ -291,7 +295,7 @@ describe("API keys", () => {
headers: { "content-type": "application/json" },
body: JSON.stringify({
capabilities: ["kubernetes:read"],
expiresAt: "2026-10-05T00:00:00.000Z",
expiresAt: finiteExpiry,
}),
}),
);
@@ -300,7 +304,7 @@ describe("API keys", () => {
token: string;
expiresAt: string;
};
expect(finiteKey.expiresAt).toBe("2026-10-05T00:00:00.000Z");
expect(finiteKey.expiresAt).toBe(finiteExpiry);
expect(
(await app(request("/api/v2/logout", { method: "POST" }, key.token)))
.status,
@@ -326,7 +330,7 @@ describe("API keys", () => {
username: "ci",
capabilities: ["kubernetes:read"],
workspace: "shop",
expiresAt: "2026-10-05T00:00:00.000Z",
expiresAt: finiteExpiry,
});
expect(
(await app(request("/api/v2/workspaces/other", {}, "scoped-key"))).status,
@@ -366,7 +370,7 @@ describe("API keys", () => {
username: "ci",
capabilities: ["kubernetes:write"],
workspace: "shop",
expiresAt: "2026-10-05T00:00:00.000Z",
expiresAt: finiteExpiry,
});
const apply = (workspace: string) =>
app(
@@ -402,7 +406,7 @@ describe("API keys", () => {
username: "ci",
capabilities: ["users:read"],
workspace: "shop",
expiresAt: "2026-10-05T00:00:00.000Z",
expiresAt: finiteExpiry,
});
await auditStore.append({
actor: { username: "admin" },
@@ -459,7 +463,7 @@ describe("API keys", () => {
username: "ci",
capabilities: ["kubernetes:read"],
workspace: "shop",
expiresAt: "2026-10-05T00:00:00.000Z",
expiresAt: finiteExpiry,
});
const unfiltered = await app(
@@ -525,7 +529,7 @@ describe("API keys", () => {
username: "admin",
capabilities: ["kubernetes:write"],
workspace: "kuber-system",
expiresAt: "2026-10-05T00:00:00.000Z",
expiresAt: finiteExpiry,
});
await store.createApiKey({
id: "key_platform_scope",
@@ -533,7 +537,7 @@ describe("API keys", () => {
username: "admin",
capabilities: ["platform:adopt"],
workspace: "shop",
expiresAt: "2026-10-05T00:00:00.000Z",
expiresAt: finiteExpiry,
});
await store.createApiKey({
id: "key_platform_allowed",
@@ -541,7 +545,7 @@ describe("API keys", () => {
username: "admin",
capabilities: ["platform:adopt"],
workspace: "kuber-system",
expiresAt: "2026-10-05T00:00:00.000Z",
expiresAt: finiteExpiry,
});
const adopt = (token: string) =>
app(
@@ -569,7 +573,7 @@ describe("API keys", () => {
username: "ci",
capabilities: ["kubernetes:write"],
workspace: "shop",
expiresAt: "2026-10-05T00:00:00.000Z",
expiresAt: finiteExpiry,
});
const matching = await app(
request(
+220 -3
View File
@@ -9,6 +9,7 @@ import {
} from "../../server/operation-store";
import { MemoryWorkspaceStore } from "../../server/workspace-store";
import { MemoryTrustStore } from "../../server/trust-store";
import type { BuildController } from "../../server/build-controller";
function request(
path: string,
@@ -340,7 +341,8 @@ describe("operation response safety", () => {
expect(immediate.status).toBe(500);
expect(immediateBody).not.toContain("database-password");
expect(immediateBody).not.toContain("kube-secret");
expect(immediateBody).toContain("OPERATION_FAILED");
expect(immediateBody).toContain("DATABASE_RECONCILE_FAILED");
expect(immediateBody).toContain("during operation execution: Unexpected failure; check server logs using the operation ID");
const operationId = (await operationStore.list())[0]!.metadata.name;
const retrieved = await app(
@@ -529,6 +531,98 @@ async function authenticatedStore(role: "viewer" | "operator" | "admin") {
}
describe("kuber v2 HTTP routes", () => {
test("streams build status and logs, supports resume cursors, and keeps version headers", async () => {
const controller = {
getBuildProject: async () => "demo",
getBuildStatus: async () => ({ state: "running", phase: "building" }),
getBuildEvents: async (_id: string, after = 0) => [
{ type: "log", sequence: 4, message: "build output" },
].filter((event) => event.sequence > after),
reconcileBuild: async () => ({ state: "running", phase: "building" }),
} as unknown as BuildController;
const app = createApp({ store: await authenticatedStore("operator"), builds: controller });
const abort = new AbortController();
const response = await app(request(
"/api/v2/builds/build-1/events?after=2",
{ headers: { accept: "text/event-stream" }, signal: abort.signal },
"token",
));
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain("text/event-stream");
expect(response.headers.get("x-kuber-version")).toBeTruthy();
const reader = response.body!.getReader();
let body = "";
while (!body.includes("event: status")) {
const { done, value } = await reader.read();
if (done) break;
body += new TextDecoder().decode(value);
}
expect(body).toContain("id: 4\nevent: log");
expect(body).toContain("build output");
expect(body).toContain("event: status");
abort.abort();
await reader.cancel();
const resumedAbort = new AbortController();
const resumed = await app(request(
"/api/v2/builds/build-1/events",
{ headers: { accept: "text/event-stream", "last-event-id": "4" }, signal: resumedAbort.signal },
"token",
));
const resumedReader = resumed.body!.getReader();
const resumedChunk = await resumedReader.read();
expect(new TextDecoder().decode(resumedChunk.value)).not.toContain("id: 4");
resumedAbort.abort();
await resumedReader.cancel();
});
test("checks authentication and workspace scope before opening a build stream", async () => {
const controller = {
getBuildProject: async () => "other",
getBuildStatus: async () => ({ state: "running", phase: "building" }),
getBuildEvents: async () => [],
reconcileBuild: async () => ({ state: "running", phase: "building" }),
} as unknown as BuildController;
const app = createApp({ store: await authenticatedStore("operator"), builds: controller });
const unauthenticated = await app(request(
"/api/v2/builds/build-1/events",
{ headers: { accept: "text/event-stream" } },
));
expect(unauthenticated.status).toBe(401);
const store = new MemoryAuthStore();
await store.putUser({ username: "scoped", passwordHash: "hash", roles: ["operator"] });
await store.createApiKey({
id: "scoped-key-12345678", username: "scoped", tokenHash: hashToken("scoped-token"),
capabilities: ["kubernetes:write"], workspace: "demo",
});
const scopedApp = createApp({ store, builds: controller });
const forbidden = await scopedApp(request(
"/api/v2/builds/build-1/events",
{ headers: { accept: "text/event-stream" } },
"scoped-token",
));
expect(forbidden.status).toBe(403);
expect(forbidden.headers.get("content-type")).not.toContain("text/event-stream");
});
test("rejects malformed SSE cursors before opening the stream", async () => {
const controller = {
getBuildProject: async () => "demo",
getBuildStatus: async () => ({ state: "running", phase: "building" }),
getBuildEvents: async () => [],
reconcileBuild: async () => ({ state: "running", phase: "building" }),
} as unknown as BuildController;
const app = createApp({ store: await authenticatedStore("operator"), builds: controller });
const response = await app(request(
"/api/v2/builds/build-1/events?after=1.5",
{ headers: { accept: "text/event-stream" } },
"token",
));
expect(response.status).toBe(400);
expect(response.headers.get("content-type")).toContain("application/problem+json");
});
test("grants, lists, revokes, and enforces namespace trust for applies", async () => {
const workspaceStore = new MemoryWorkspaceStore({
uid: () => "workspace-uid",
@@ -1788,11 +1882,134 @@ describe("kuber v2 HTTP routes", () => {
expect(result.status).toBe(500);
const problem = await result.json() as { code: string; detail: string };
expect(problem.code).toBe("DATABASE_RECONCILE_FAILED");
expect(problem.detail).toContain("database apply for database web_db (service web, role web_role): Forbidden");
expect(problem.detail).toContain("database apply for requested database claim: Forbidden");
expect(JSON.stringify(problem)).not.toContain("private-value");
}
expect((await operationStore.get("operation-db-failure"))?.status.error?.message)
.toContain("database apply for database web_db");
.toContain("database apply for requested database claim");
});
test("never exposes syntactically valid secret-like database claim identifiers", async () => {
const workspaceStore = new MemoryWorkspaceStore({ uid: () => "workspace-uid" });
await workspaceStore.create({
id: "demo",
source: { uri: "oci://example/demo", digest: "sha256:abc" },
});
const operationStore = new MemoryOperationStore(undefined, () => "db-secret-identifier");
const { DatabaseReconciliationError } = await import("../../lib/database");
const secretLike = ["DB_PASSWORD_private123", "svc_api_token_abc", "role_secret_key_123"];
const app = createApp({
store: await authenticatedStore("operator"),
workspaceStore,
operationStore,
management: {
reconcileDatabases: async () => {
throw new DatabaseReconciliationError(
"database apply",
Object.assign(new Error("Forbidden"), { statusCode: 403 }),
{ service: secretLike[1]!, username: secretLike[2]!, database: secretLike[0]!, secretName: "safe-secret-name" },
);
},
} as unknown as ManagementService,
});
const response = await app(request(
"/api/v2/workspaces/demo/databases",
{ method: "POST", headers: { "idempotency-key": "db-secret-identifier" }, body: JSON.stringify({ compose: {} }) },
"token",
));
const responseBody = JSON.stringify(await response.json());
const operation = await operationStore.get("operation-db-secret-identifier");
expect(response.status).toBe(500);
expect(responseBody).toContain("database apply for requested database claim: Forbidden (HTTP 403)");
expect(operation?.status.error?.message).toContain("database apply for requested database claim");
for (const identifier of secretLike) {
expect(responseBody).not.toContain(identifier);
expect(JSON.stringify(operation)).not.toContain(identifier);
}
});
test("logs allowlisted database failure metadata with the operation ID, never provider data", async () => {
const workspaceStore = new MemoryWorkspaceStore({ uid: () => "workspace-uid" });
await workspaceStore.create({ id: "demo", source: { uri: "oci://example/demo", digest: "sha256:abc" } });
const operationStore = new MemoryOperationStore(undefined, () => "diagnostic");
const logs: Record<string, unknown>[] = [];
const { DatabaseReconciliationError } = await import("../../lib/database");
const secretLike = "DB_PASSWORD_private123";
const cause = Object.assign(new Error(`token=${secretLike}`), {
name: secretLike,
code: "ECONNREFUSED",
statusCode: 503,
body: { message: secretLike, headers: { authorization: secretLike } },
stack: `Error: ${secretLike}\n at ${secretLike} (/tmp/${secretLike}/lib/database.ts:22:7)`,
});
const app = createApp({
store: await authenticatedStore("operator"), workspaceStore, operationStore,
logger: { error: (entry) => logs.push(entry) },
management: { reconcileDatabases: async () => {
throw new DatabaseReconciliationError("database apply", cause, {
service: secretLike, username: secretLike, database: secretLike, secretName: secretLike,
});
} } as unknown as ManagementService,
});
const response = await app(request("/api/v2/workspaces/demo/databases", {
method: "POST", body: JSON.stringify({ compose: {} }),
}, "token"));
expect(response.status).toBe(500);
expect(await response.json()).toMatchObject({
code: "DATABASE_RECONCILE_FAILED", operationId: "operation-diagnostic",
});
const diagnostic = logs.filter((entry) => entry.event === "operation.database_reconcile.failed");
expect(diagnostic).toHaveLength(1);
expect(diagnostic[0]).toMatchObject({
operationId: "operation-diagnostic", phase: "database apply",
errorClass: "UnknownError", providerCode: "ECONNREFUSED", providerStatus: 503,
topFrame: "lib/database.ts:22:7",
});
expect(JSON.stringify(diagnostic)).not.toContain(secretLike);
expect(JSON.stringify(await operationStore.get("operation-diagnostic"))).not.toContain(secretLike);
});
test("retains a safe diagnosis for an ordinary database provider error through HTTP, persistence and polling", async () => {
const workspaceStore = new MemoryWorkspaceStore({ uid: () => "workspace-uid" });
await workspaceStore.create({
id: "demo",
source: { uri: "oci://example/demo", digest: "sha256:abc" },
});
const operationStore = new MemoryOperationStore(undefined, () => "ordinary-db-error");
const app = createApp({
store: await authenticatedStore("operator"),
workspaceStore,
operationStore,
management: {
reconcileDatabases: async () => {
throw Object.assign(new Error("provider failed: DATABASE_URL=postgresql://admin:[email protected]/app"), {
statusCode: 403,
body: { reason: "Forbidden", message: "password=private", code: 403 },
});
},
} as unknown as ManagementService,
});
const reconcile = () => app(request(
"/api/v2/workspaces/demo/databases",
{ method: "POST", headers: { "idempotency-key": "ordinary-db-error" }, body: JSON.stringify({ compose: {} }) },
"token",
));
const expected = "Database reconciliation failed during operation execution: Forbidden (HTTP 403)";
for (const result of [await reconcile(), await reconcile()]) {
expect(result.status).toBe(500);
expect(await result.json()).toMatchObject({
code: "DATABASE_RECONCILE_FAILED",
detail: expected,
operationId: "operation-ordinary-db-error",
});
}
const persisted = await operationStore.get("operation-ordinary-db-error");
expect(persisted?.status.error).toEqual({ code: "DATABASE_RECONCILE_FAILED", message: expected });
const polled = await app(request("/api/v2/operations/operation-ordinary-db-error", {}, "token"));
expect(polled.status).toBe(200);
expect(await polled.json()).toMatchObject({ status: { error: { message: expected } } });
expect(JSON.stringify(persisted)).not.toContain("private");
});
test("routes workspace adoption and keeps platform adoption admin-only", async () => {
+330
View File
@@ -0,0 +1,330 @@
import { describe, expect, test } from "bun:test";
import { BuildEventStreamHub } from "../../server/build-event-stream";
import type { BuildStatus } from "../../shared/build-protocol";
const base: BuildStatus = {
version: 1,
id: "a",
state: "running",
createdAt: "now",
};
const tick = () => Bun.sleep(10);
describe("build SSE hub", () => {
test("reports an explicit gap when the resume cursor predates retained logs", async () => {
const abort = new AbortController();
const hub = new BuildEventStreamHub(
{
getBuildStatus: async () => base,
reconcileBuild: async () => base,
getBuildEvents: async (_id, after) => [
{ type: "status" as const, status: base },
...[4, 5]
.filter((sequence) => sequence > (after ?? 0))
.map((sequence) => ({
type: "log" as const,
id: "a",
sequence,
message: `log ${sequence}\n`,
})),
],
},
{ pollMs: 100, heartbeatMs: 1_000 },
);
const response = await hub.open(
"a",
new Request("https://test/builds/a/events", {
headers: { "last-event-id": "1" },
signal: abort.signal,
}),
);
const reader = response.body!.getReader();
const decoder = new TextDecoder();
const first = decoder.decode((await reader.read()).value);
expect(first).toContain("event: gap\n");
expect(first).toContain('"missing":2');
expect(first).toContain("id: 3\n");
expect(decoder.decode((await reader.read()).value)).toContain("id: 4\nevent: log");
abort.abort();
await reader.cancel();
});
test("coalesces reconciliation, resumes logs, dedupes status and drains terminal", async () => {
let calls = 0;
let status = base;
const hub = new BuildEventStreamHub(
{
getBuildStatus: async () => status,
reconcileBuild: async () => {
calls++;
return status;
},
getBuildEvents: async (_id, after) => [
{ type: "status" as const, status: base },
...[1, 2]
.filter((sequence) => sequence > (after ?? 0))
.map((sequence) => ({
type: "log" as const,
id: "a",
sequence,
message: `log ${sequence}\n`,
})),
],
},
{ pollMs: 20, heartbeatMs: 40 },
);
const a = new AbortController();
const b = new AbortController();
const request = (signal: AbortSignal, after: string) =>
new Request(`https://test/builds/a/events?after=${after}`, { signal });
const first = await hub.open("a", request(a.signal, "1"));
const second = await hub.open("a", request(b.signal, "2"));
expect(first.headers.get("content-type")).toContain("text/event-stream");
const reader1 = first.body!.getReader();
const reader2 = second.body!.getReader();
const text = new TextDecoder();
expect(text.decode((await reader1.read()).value)).toContain(
"id: 2\nevent: log\n",
);
expect(text.decode((await reader1.read()).value)).toContain(
"event: status\n",
);
expect(text.decode((await reader2.read()).value)).toContain(
"event: status\n",
);
await tick();
expect(calls).toBe(1);
status = { ...base, state: "succeeded" };
const remaining = async (reader: typeof reader1) => {
let output = "";
for (;;) {
const { value, done } = await reader.read();
if (done) return output;
output += text.decode(value);
}
};
expect(await remaining(reader1)).toContain('"state":"succeeded"');
expect(await remaining(reader2)).toContain('"state":"succeeded"');
a.abort();
b.abort();
});
test("validates cursor, stops aborted subscribers and emits heartbeat comments", async () => {
const hub = new BuildEventStreamHub(
{
getBuildStatus: async () => base,
reconcileBuild: async () => base,
getBuildEvents: async () => [],
},
{ pollMs: 5, heartbeatMs: 5 },
);
await expect(
hub.open("a", new Request("https://test/events?after=-1")),
).rejects.toThrow(RangeError);
const abort = new AbortController();
const response = await hub.open(
"a",
new Request("https://test/events", { signal: abort.signal }),
);
const reader = response.body!.getReader();
expect(new TextDecoder().decode((await reader.read()).value)).toContain(
"event: status",
);
expect(new TextDecoder().decode((await reader.read()).value)).toBe(
": heartbeat\n\n",
);
abort.abort();
expect((await reader.read()).done).toBe(true);
});
test("reads persisted progress when another replica owns reconciliation", async () => {
let status = base;
let sequence = 0;
let reconciles = 0;
const hub = new BuildEventStreamHub(
{
getBuildStatus: async () => status,
reconcileBuild: async () => {
reconciles++;
throw new Error("build lease held by another replica");
},
getBuildEvents: async (_id, after) =>
sequence > (after ?? 0)
? [
{
type: "log" as const,
id: "a",
sequence,
message: "remote log\n",
},
]
: [],
},
{ pollMs: 5, heartbeatMs: 50 },
);
const abort = new AbortController();
const response = await hub.open(
"a",
new Request("https://test/events", { signal: abort.signal }),
);
const reader = response.body!.getReader();
const decoder = new TextDecoder();
expect(decoder.decode((await reader.read()).value)).toContain(
'"state":"running"',
);
sequence = 1;
expect(decoder.decode((await reader.read()).value)).toContain(
"id: 1\nevent: log",
);
status = { ...base, state: "succeeded" };
let terminalFrame = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
terminalFrame += decoder.decode(value);
}
expect(terminalFrame).toContain('"state":"succeeded"');
expect(reconciles).toBeGreaterThan(0);
abort.abort();
});
test("heartbeats during slow reconciliation and tears down on abort", async () => {
const blocked = new Promise<BuildStatus>(() => {});
const signals: AbortSignal[] = [];
let observations = 0;
const hub = new BuildEventStreamHub(
{
getBuildStatus: async () => base,
reconcileBuild: async (_id, options) => {
if (options?.signal) signals.push(options.signal);
return blocked;
},
getBuildEvents: async () => {
observations++;
return [];
},
},
{ pollMs: 5, heartbeatMs: 5 },
);
const abort = new AbortController();
const response = await hub.open(
"a",
new Request("https://test/events", { signal: abort.signal }),
);
const reader = response.body!.getReader();
expect(new TextDecoder().decode((await reader.read()).value)).toContain(
"event: status",
);
expect(new TextDecoder().decode((await reader.read()).value)).toBe(
": heartbeat\n\n",
);
await Bun.sleep(25);
expect(observations).toBeGreaterThan(1);
expect(signals).toHaveLength(1);
abort.abort();
for (;;) {
if ((await reader.read()).done) break;
}
expect(signals[0]?.aborted).toBe(true);
const stoppedAt = observations;
await Bun.sleep(20);
expect(observations).toBe(stoppedAt);
const next = new AbortController();
const reopened = await hub.open(
"a",
new Request("https://test/events", { signal: next.signal }),
);
const nextReader = reopened.body!.getReader();
expect(new TextDecoder().decode((await nextReader.read()).value)).toContain(
"event: status",
);
expect(signals).toHaveLength(2);
expect(signals[1]?.aborted).toBe(false);
next.abort();
await nextReader.cancel();
});
test("bounds shared reconciliation writes while observing two subscribers independently", async () => {
let reconciles = 0;
let observations = 0;
const hub = new BuildEventStreamHub(
{
getBuildStatus: async () => base,
reconcileBuild: async () => {
reconciles++;
return base;
},
getBuildEvents: async () => {
observations++;
return [];
},
},
{ pollMs: 10, reconcileMs: 80, heartbeatMs: 1_000 },
);
const a = new AbortController();
const b = new AbortController();
const first = await hub.open(
"a",
new Request("https://test/events", { signal: a.signal }),
);
const second = await hub.open(
"a",
new Request("https://test/events", { signal: b.signal }),
);
expect(
new TextDecoder().decode((await first.body!.getReader().read()).value),
).toContain("event: status");
expect(
new TextDecoder().decode((await second.body!.getReader().read()).value),
).toContain("event: status");
await Bun.sleep(45);
expect(reconciles).toBe(1); // One lease/write attempt, not one per poll or subscriber.
expect(observations).toBeGreaterThanOrEqual(3);
for (let i = 0; i < 15 && reconciles < 2; i++) await tick();
expect(reconciles).toBe(2);
a.abort();
b.abort();
});
test("closes a slow subscriber at the bounded queue limit for resumable logs", async () => {
const hub = new BuildEventStreamHub(
{
getBuildStatus: async () => base,
reconcileBuild: async () => base,
getBuildEvents: async (_id, after) =>
Array.from({ length: 40 }, (_, index) => index + 1)
.filter((sequence) => sequence > (after ?? 0))
.map((sequence) => ({
type: "log" as const,
id: "a",
sequence,
message: `log ${sequence}\n`,
})),
},
{ pollMs: 5, heartbeatMs: 50 },
);
const response = await hub.open("a", new Request("https://test/events"));
await tick(); // Allow the producer to fill its queue before consumption.
const reader = response.body!.getReader();
const frames: string[] = [];
for (;;) {
const { value, done } = await reader.read();
if (done) break;
frames.push(new TextDecoder().decode(value));
}
expect(frames).toHaveLength(16);
expect(frames[0]).toContain("id: 1\nevent: log");
expect(frames[15]).toContain("id: 16\nevent: log");
const resumed = await hub.open(
"a",
new Request("https://test/events", {
headers: { "last-event-id": "16" },
}),
);
const resumedReader = resumed.body!.getReader();
expect(
new TextDecoder().decode((await resumedReader.read()).value),
).toContain("id: 17\nevent: log");
await resumedReader.cancel();
});
});
+111 -4
View File
@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test";
import type { KubernetesObject, V1Deployment } from "@kubernetes/client-node";
import { DatabaseReconciliationError } from "../../lib/database";
import {
createManagementService,
type ManagementDependencies,
@@ -67,6 +68,27 @@ function dependencies(
}
describe("server management service", () => {
test.each([
["namespace precheck", { readNamespace: async () => { throw new Error("token=private"); } }],
["database dependency", { reconcileDatabases: async () => { throw new Error("token=private"); } }],
["database resource listing", { listDatabaseResources: async () => { throw new Error("token=private"); } }],
["database resource ownership", { applyResource: async () => { throw new Error("token=private"); }, listDatabaseResources: async () => [object("Database", "DB_PASSWORD_private123", "db-uid")] }],
] as const)("labels database %s failures without leaking details", async (phase, overrides) => {
const service = createManagementService(dependencies(overrides));
let failure: unknown;
try { await service.reconcileDatabases(workspace, { services: {} }); }
catch (error) { failure = error; }
expect(failure).toBeInstanceOf(DatabaseReconciliationError);
expect((failure as DatabaseReconciliationError).phase).toBe(phase);
expect((failure as Error).message).not.toContain("private");
});
test("preserves an existing database substep rather than masking it", async () => {
const service = createManagementService(dependencies({
reconcileDatabases: async () => { throw new DatabaseReconciliationError("database apply", new Error("private")); },
}));
await expect(service.reconcileDatabases(workspace, { services: {} })).rejects.toMatchObject({ phase: "database apply" });
});
test("waits on deployments together and cancels siblings after a crash", async () => {
const started: string[] = [];
let siblingCancelled = false;
@@ -344,14 +366,18 @@ describe("server management service", () => {
namespace: "database",
},
};
const service = createManagementService(
dependencies({ listDatabaseResources: async () => [database] }),
);
const deleted: ResourceIdentity[] = [];
const service = createManagementService(dependencies({
listDatabaseResources: async () => [database],
deleteResource: async (identity) => { deleted.push(identity); },
}));
const plan = await service.planDown(workspace, true);
expect(plan.delete.map(({ kind, uid }) => [kind, uid])).toEqual([
["Database", "database-uid"],
["Namespace", "namespace-uid"],
]);
expect(plan.retained.map(({ kind, uid }) => [kind, uid])).toEqual([["Database", "database-uid"]]);
await service.down(workspace, true);
expect(deleted.map(({ kind }) => kind)).toEqual(["Namespace"]);
const missingUid = createManagementService(
dependencies({
@@ -368,6 +394,87 @@ describe("server management service", () => {
);
});
test("preserves foreign and owned database CRs on full down while deleting other resources", async () => {
const shared = { ...object("Database", "sastify-store", "shared-uid", {
"app.kubernetes.io/managed-by": "kuber", [WORKSPACE_PROJECT_LABEL]: workspace.project,
[WORKSPACE_UID_LABEL]: "other-workspace",
}), metadata: { ...object("Database", "sastify-store", "shared-uid").metadata,
namespace: "database", labels: {
"app.kubernetes.io/managed-by": "kuber", [WORKSPACE_PROJECT_LABEL]: workspace.project,
[WORKSPACE_UID_LABEL]: "other-workspace",
} } };
const owned = { ...object("Database", "local", "local-uid"), metadata: {
...object("Database", "local", "local-uid").metadata, namespace: "database",
} };
const deleted: string[] = [];
const service = createManagementService(dependencies({
listProjectResources: async () => [object("Deployment", "api", "deployment-uid")],
listDatabaseResources: async () => [shared, owned],
deleteResource: async (identity) => { deleted.push(identity.kind); },
}));
const plan = await service.down(workspace, true);
expect(plan.delete.map(({ kind }) => kind)).toEqual(["Deployment", "Namespace"]);
expect(plan.retained.map(({ name }) => name)).toEqual(["local"]);
expect(deleted).toEqual(["Deployment", "Namespace"]);
});
test("reuses declared foreign database without taking its workspace UID", async () => {
const db = { ...object("Database", "sastify-store", "shared-uid", {
"app.kubernetes.io/managed-by": "kuber", [WORKSPACE_PROJECT_LABEL]: workspace.project,
[WORKSPACE_UID_LABEL]: "other-workspace",
}), metadata: {
...object("Database", "sastify-store", "shared-uid").metadata,
namespace: "database",
labels: { "app.kubernetes.io/managed-by": "kuber",
[WORKSPACE_PROJECT_LABEL]: workspace.project, [WORKSPACE_UID_LABEL]: "other-workspace" },
}, spec: { owner: "sastify", cluster: { name: "postgres" } } };
const before = structuredClone(db);
const applied: KubernetesObject[] = [];
const service = createManagementService(dependencies({
readNamespace: async (project) => ({ uid: "namespace-uid", labels: {
"app.kubernetes.io/managed-by": "kuber",
[WORKSPACE_UID_LABEL]: project === workspace.project ? workspace.uid : "second-uid",
} }),
listDatabaseResources: async () => [db],
applyResource: async (resource) => { applied.push(resource); return resource; },
}));
const compose = { services: { web: { volumes: ["postgresql:sastify/sastify-store"] } } } as never;
for (const project of [workspace, { project: "second-project", uid: "second-uid" }]) {
await service.reconcileDatabases(project, compose);
}
expect(applied).toEqual([]);
expect(db).toEqual(before);
await expect(service.reconcileDatabases(workspace, { services: {} })).rejects.toMatchObject({ phase: "database resource ownership" });
expect(applied).toEqual([]);
});
test("adopts unowned and same-workspace databases and rejects foreign conflicting specs", async () => {
const resources: KubernetesObject[] = [
{ ...object("Database", "new-db", "new-uid"), metadata: {
...object("Database", "new-db", "new-uid").metadata,
namespace: "database", labels: { "app.kubernetes.io/managed-by": "kuber" },
} },
{ ...object("Database", "owned-db", "owned-uid"), metadata: {
...object("Database", "owned-db", "owned-uid").metadata, namespace: "database",
} },
];
const applied: KubernetesObject[] = [];
const service = createManagementService(dependencies({
listDatabaseResources: async () => resources,
applyResource: async (resource) => { applied.push(resource); return resource; },
}));
await service.reconcileDatabases(workspace, { services: {} });
expect(applied.map((resource) => resource.metadata?.labels?.[WORKSPACE_UID_LABEL])).toEqual([workspace.uid, workspace.uid]);
resources[0] = { ...resources[0]!, metadata: { ...resources[0]!.metadata,
labels: { [WORKSPACE_UID_LABEL]: "other-workspace" } }, spec: {
owner: "wrong", cluster: { name: "postgres" },
} } as KubernetesObject;
await expect(service.reconcileDatabases(workspace, { services: { web: {
volumes: ["postgresql:sastify/new-db"],
} } } as never)).rejects.toMatchObject({ phase: "database resource ownership" });
expect(applied).toHaveLength(2);
});
test("stop deletes matching HPAs before scaling selected deployments", async () => {
const calls: string[] = [];
const service = createManagementService(
+7
View File
@@ -121,6 +121,13 @@ describe("operation store", () => {
code: "RECONCILE_FAILED",
message: "Database reconciliation failed",
});
expect(sanitizeOperationError({
code: "DATABASE_RECONCILE_FAILED",
message: "Database reconciliation failed during database apply: Forbidden (HTTP 403) DB_PASSWORD=private",
}, "databases.reconcile")).toEqual({
code: "DATABASE_RECONCILE_FAILED",
message: "Database reconciliation failed during database apply: Forbidden (HTTP 403) DB_PASSWORD=[REDACTED]",
});
});
test("enforces the operation state machine", async () => {
+74
View File
@@ -0,0 +1,74 @@
import { expect, test } from "bun:test";
import { createApp, execProblem } from "../../server/app";
import { hashToken, MemoryAuthStore } from "../../server/auth";
import { MemoryWorkspaceStore } from "../../server/workspace-store";
import type { LogService } from "../../server/log-service";
import { KUBER_VERSION, KUBER_VERSION_HEADER } from "../../shared/version";
test("returns the bundled version on success, errors, preflight, empty and streaming responses", async () => {
const store = new MemoryAuthStore();
await store.putUser({
username: "viewer",
passwordHash: "hash",
roles: ["viewer"],
});
await store.putSession({
tokenHash: hashToken("token"),
username: "viewer",
authVersion: 1,
expiresAt: "2030-01-01T00:00:00.000Z",
});
const workspaceStore = new MemoryWorkspaceStore({
uid: () => "workspace-uid",
});
await workspaceStore.create({
id: "demo",
source: { uri: "oci://example/demo", digest: "sha256:abc" },
});
const logs = {
async collect() {
return [{ type: "log", message: "hello" }];
},
} as unknown as LogService;
const app = createApp({ store, workspaceStore, logs });
const request = (path: string, init: RequestInit = {}) =>
new Request(`https://kuber.astrxl.dev${path}`, init);
const authenticated = (path: string, init: RequestInit = {}) =>
request(path, {
...init,
headers: {
authorization: "Bearer token",
...Object.fromEntries(new Headers(init.headers)),
},
});
const success = await app(request("/api/v2/health"));
const error = await app(request("/api/v2/me"));
const preflight = await app(
request("/api/v2/health", {
method: "OPTIONS",
headers: { origin: "https://kuber.astrxl.dev" },
}),
);
const stream = await app(authenticated("/api/v2/workspaces/demo/logs"));
const empty = await app(authenticated("/api/v2/logout", { method: "POST" }));
for (const response of [success, error, preflight, empty, stream]) {
expect(response.headers.get(KUBER_VERSION_HEADER)).toBe(KUBER_VERSION);
}
expect([
success.status,
error.status,
preflight.status,
empty.status,
stream.status,
]).toEqual([200, 401, 204, 204, 200]);
expect(stream.headers.get("content-type")).toContain("application/x-ndjson");
expect(await stream.text()).toContain("hello");
});
test("includes the version on exec upgrade errors outside the ordinary API router", () => {
const response = execProblem(new Error("upgrade failed"), "request-id");
expect(response.status).toBe(500);
expect(response.headers.get(KUBER_VERSION_HEADER)).toBe(KUBER_VERSION);
});