Release 2.6.2-rc2
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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> = [];
|
||||
|
||||
Reference in New Issue
Block a user