Release 2.6.2-rc2

This commit is contained in:
2026-10-06 01:58:22 +00:00 Unverified
parent 198b7866ef
commit cd9fb512cd
7 changed files with 124 additions and 11 deletions
+26
View File
@@ -1,4 +1,6 @@
import { describe, expect, test } from "bun:test";
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
describe("CLI module graph", () => {
@@ -11,4 +13,28 @@ describe("CLI module graph", () => {
expect(probe.exitCode).toBe(0);
expect(new TextDecoder().decode(probe.stdout)).toContain("USAGE");
});
test("production build retains the passive updater's runtime environment guard", () => {
const root = join(import.meta.dir, "../..");
const { scripts } = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
const args = (scripts.build as string).split(/\s+/);
expect(args).toContain("--env");
expect(args[args.indexOf("--env") + 1]).toBe("disable");
const outdir = mkdtempSync(join(tmpdir(), "kuber-cli-build-"));
try {
args[args.indexOf("--outdir") + 1] = outdir;
const build = Bun.spawnSync(args, {
cwd: root,
env: { ...process.env, NODE_ENV: "production" },
});
expect(new TextDecoder().decode(build.stderr)).toBe("");
expect(build.exitCode).toBe(0);
const bundle = readFileSync(join(outdir, "index.js"), "utf8");
expect(bundle).toMatch(/process\.env\.NODE_ENV\s*===\s*["']test["']/);
expect(bundle).toMatch(/process\.env\.NODE_ENV\s*===\s*["']development["']/);
} finally {
rmSync(outdir, { recursive: true, force: true });
}
}, 20_000);
});
+68
View File
@@ -288,6 +288,11 @@ describe("up API pipeline", () => {
expect(
new Headers(build.init?.headers).get("x-kuber-trust-project"),
).toBe("shop");
const plans = calls.filter(({ path }) => path === "/workspaces/shop/resources/plan");
expect(plans).toHaveLength(1);
expect(plans[0]?.options).toEqual({ timeoutMs: 120_000 });
expect(plans[0]?.init?.signal).toBeInstanceOf(AbortSignal);
expect(new Headers(plans[0]?.init?.headers).get("x-kuber-trust-project")).toBe("shop");
} finally {
process.chdir(previousCwd);
if (previousConfigHome === undefined) delete process.env.XDG_CONFIG_HOME;
@@ -714,6 +719,69 @@ describe("up API pipeline", () => {
expect(order).toEqual(["plan", "apply", "hook", "wait", "delete"]);
});
test("allows a slow plan beyond the default deadline before applying once", async () => {
const calls: string[] = [];
const controller = new AbortController();
const request: ApiRequester = async <T>(
path: string,
init?: ApiRequestInit,
options?: ApiRequestOptions,
) => {
calls.push(path.split("/").at(-1)!);
if (path.endsWith("/plan")) {
expect(init?.method).toBe("POST");
expect(init?.signal).toBe(controller.signal);
// Scale 1 simulated second to 1ms: the plan completes at 35s.
const deadline = options?.timeoutMs ?? 30_000;
if (deadline < 35_000) throw new DOMException("Timed out", "TimeoutError");
await Bun.sleep(35);
return { desired: [], stale: [] } as T;
}
if (path.endsWith("/apply")) return {} as T;
throw new Error(`Unexpected request: ${path}`);
};
await reconcileResources("shop", [], 1, undefined, request, {
signal: controller.signal,
});
expect(calls).toEqual(["plan", "apply"]);
});
test("cancels a pending plan without starting a mutation", async () => {
const controller = new AbortController();
const cancelled = new DOMException("Cancelled", "AbortError");
const calls: string[] = [];
let planStarted!: () => void;
const started = new Promise<void>((resolve) => { planStarted = resolve; });
const request: ApiRequester = async <T>(path: string, init?: ApiRequestInit) => {
calls.push(path);
if (!path.endsWith("/plan")) throw new Error(`Unexpected request: ${path}`);
expect(init?.signal).toBe(controller.signal);
return new Promise<T>((_resolve, reject) => {
init?.signal?.addEventListener("abort", () => reject(init.signal?.reason), { once: true });
planStarted();
});
};
const run = reconcileResources("shop", [], 1, undefined, request, {
signal: controller.signal,
});
await started;
controller.abort(cancelled);
await expect(run).rejects.toBe(cancelled);
expect(calls).toEqual(["/workspaces/shop/resources/plan"]);
});
test("does not retry an HTTP 500 plan or start an apply", async () => {
const calls: string[] = [];
const failure = new KuberApiError("plan failed", 500);
const request: ApiRequester = async <T>(path: string) => {
calls.push(path);
throw failure;
};
await expect(reconcileResources("shop", [], 1, undefined, request)).rejects.toBe(failure);
expect(calls).toEqual(["/workspaces/shop/resources/plan"]);
});
test("resumes an interrupted reconcile operation by its persisted ID", async () => {
const calls: string[] = [];
const applyIdempotencyKeys: Array<string | null> = [];