feat: release 2.5.1
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
parseLogTailLines,
|
||||
runLogs,
|
||||
type LogApiEvent,
|
||||
type LogEventWriter,
|
||||
@@ -81,4 +82,34 @@ describe("logs API command runner", () => {
|
||||
);
|
||||
expect(receivedSignal).toBe(controller.signal);
|
||||
});
|
||||
|
||||
test.each([false, true])("sends -n with follow=%s", async (follow) => {
|
||||
const paths: string[] = [];
|
||||
const stream: LogsApiStream = async function* (path, _init, options) {
|
||||
paths.push(path);
|
||||
expect(options?.timeoutMs).toBe(follow ? 0 : undefined);
|
||||
yield* [] as never[];
|
||||
};
|
||||
await runLogs("shop", "web", follow, undefined, stream, () => {}, 0);
|
||||
expect(paths).toEqual([
|
||||
`/workspaces/shop/logs?service=web${follow ? "&follow=true" : ""}&tailLines=0`,
|
||||
]);
|
||||
});
|
||||
|
||||
test("accepts only non-negative safe integer line counts", async () => {
|
||||
expect(parseLogTailLines("25")).toBe(25);
|
||||
expect(parseLogTailLines("0")).toBe(0);
|
||||
for (const invalid of ["-1", "1.5", "2abc", "", "1e3", "9007199254740992"])
|
||||
expect(() => parseLogTailLines(invalid)).toThrow("non-negative integer");
|
||||
|
||||
let called = false;
|
||||
const stream: LogsApiStream = async function* () {
|
||||
called = true;
|
||||
yield* [] as never[];
|
||||
};
|
||||
await expect(
|
||||
runLogs("shop", undefined, false, undefined, stream, () => {}, -1),
|
||||
).rejects.toThrow("non-negative integer");
|
||||
expect(called).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { join } from "node:path";
|
||||
|
||||
describe("CLI module graph", () => {
|
||||
test("does not resolve the Kubernetes client during CLI startup", () => {
|
||||
const entry = join(import.meta.dir, "../../index.ts");
|
||||
const probe = Bun.spawnSync([process.execPath, entry, "--help"], {
|
||||
cwd: import.meta.dir,
|
||||
env: process.env,
|
||||
});
|
||||
expect(probe.exitCode).toBe(0);
|
||||
expect(new TextDecoder().decode(probe.stdout)).toContain("USAGE");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { join } from "node:path";
|
||||
|
||||
const entry = join(import.meta.dir, "../../index.ts");
|
||||
|
||||
function cli(...args: string[]) {
|
||||
const result = Bun.spawnSync([process.execPath, entry, ...args], {
|
||||
cwd: import.meta.dir,
|
||||
env: { ...process.env, TERM: "dumb", NO_COLOR: "1" },
|
||||
});
|
||||
return {
|
||||
status: result.exitCode,
|
||||
output: new TextDecoder().decode(result.stdout),
|
||||
error: new TextDecoder().decode(result.stderr),
|
||||
};
|
||||
}
|
||||
|
||||
describe("CLI startup", () => {
|
||||
test("registers all original names as lazy commands", () => {
|
||||
const probe = Bun.spawnSync([
|
||||
process.execPath,
|
||||
"-e",
|
||||
`import { main } from ${JSON.stringify(join(import.meta.dir, "../../command/main.ts"))}; console.log(JSON.stringify(Object.entries(main.subCommands).map(([name, command]) => [name, typeof command])));`,
|
||||
]);
|
||||
expect(probe.exitCode).toBe(0);
|
||||
const commands = JSON.parse(new TextDecoder().decode(probe.stdout)) as [
|
||||
string,
|
||||
string,
|
||||
][];
|
||||
expect(commands.map(([name]) => name)).toEqual([
|
||||
"audit",
|
||||
"ci",
|
||||
"db",
|
||||
"down",
|
||||
"export",
|
||||
"exec",
|
||||
"logs",
|
||||
"maintenance",
|
||||
"login",
|
||||
"logout",
|
||||
"operations",
|
||||
"ps",
|
||||
"restart",
|
||||
"rollback",
|
||||
"s3",
|
||||
"start",
|
||||
"stop",
|
||||
"up",
|
||||
"users",
|
||||
"trust",
|
||||
"whoami",
|
||||
]);
|
||||
expect(commands.every(([, type]) => type === "function")).toBe(true);
|
||||
});
|
||||
|
||||
test("root help retains descriptions, alias, and completion command", () => {
|
||||
const result = cli("--help");
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.output).toContain("rollback, fuck");
|
||||
expect(result.output).toContain("complete");
|
||||
expect(result.output).toContain("Create and start deployments");
|
||||
|
||||
const missingCommand = cli();
|
||||
expect(missingCommand.status).toBe(1);
|
||||
expect(missingCommand.output).toContain("complete");
|
||||
});
|
||||
|
||||
test("alias and named subcommand have the same help", () => {
|
||||
const alias = cli("fuck", "--help");
|
||||
const named = cli("rollback", "--help");
|
||||
expect(alias.status).toBe(0);
|
||||
expect(alias.output).toBe(named.output);
|
||||
expect(alias.output).toContain("DEPLOYMENT");
|
||||
});
|
||||
|
||||
test("completion still resolves flags from nested commands", () => {
|
||||
const result = cli("complete", "--", "logs", "--");
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.output).toContain("--follow");
|
||||
});
|
||||
});
|
||||
@@ -45,9 +45,6 @@ describe("up API pipeline", () => {
|
||||
join(root, ".kuberrc.ts"),
|
||||
'export default { project: "shop" };\n',
|
||||
);
|
||||
const git = Bun.spawn(["git", "init", "-q", root]);
|
||||
expect(await git.exited).toBe(0);
|
||||
|
||||
process.chdir(root);
|
||||
process.env.XDG_CONFIG_HOME = configHome;
|
||||
const identity = await resolveTrustIdentity("shop", root);
|
||||
@@ -106,6 +103,92 @@ describe("up API pipeline", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("adopts before overlapping database and storage reconciliation, then renders merged env", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "kuber-up-api-"));
|
||||
const previousCwd = process.cwd();
|
||||
const order: string[] = [];
|
||||
const started: string[] = [];
|
||||
let bothReady!: () => void;
|
||||
const bothStarted = new Promise<void>((resolve) => {
|
||||
bothReady = resolve;
|
||||
});
|
||||
let releaseDatabase!: () => void;
|
||||
let releaseStorage!: () => void;
|
||||
const database = new Promise<void>((resolve) => {
|
||||
releaseDatabase = resolve;
|
||||
});
|
||||
const storage = new Promise<void>((resolve) => {
|
||||
releaseStorage = resolve;
|
||||
});
|
||||
try {
|
||||
await writeFile(
|
||||
join(root, "compose.yml"),
|
||||
"services:\n app:\n image: nginx\n volumes:\n - postgresql:app\n - s3:assets\n",
|
||||
);
|
||||
await writeFile(
|
||||
join(root, ".kuberrc.ts"),
|
||||
'export default { project: "shop" };\n',
|
||||
);
|
||||
expect(await Bun.spawn(["git", "init", "-q", root]).exited).toBe(0);
|
||||
process.chdir(root);
|
||||
const trust = await resolveTrustIdentity("shop", root);
|
||||
const request: ApiRequester = async <T>(path: string) => {
|
||||
order.push(path);
|
||||
if (path === "/workspaces/shop") throw new KuberApiError("missing", 404);
|
||||
if (path === "/workspaces")
|
||||
return {
|
||||
metadata: { name: "shop", uid: "workspace", resourceVersion: "1" },
|
||||
} as T;
|
||||
if (path.endsWith("/adopt")) return { resourcesAdopted: 0 } as T;
|
||||
if (path.endsWith("/databases")) {
|
||||
started.push("database");
|
||||
if (started.length === 2) bothReady();
|
||||
await database;
|
||||
return { app: { DATABASE_URL: "postgresql://app", SHARED: "database" } } as T;
|
||||
}
|
||||
if (path.endsWith("/storage")) {
|
||||
started.push("storage");
|
||||
if (started.length === 2) bothReady();
|
||||
await storage;
|
||||
return { app: { AWS_ACCESS_KEY_ID: "key", SHARED: "storage" } } as T;
|
||||
}
|
||||
if (path.endsWith("/plan")) return { desired: [], stale: [] } as T;
|
||||
return {} as T;
|
||||
};
|
||||
const run = provideContext(() => runUp(false, request, { trust }));
|
||||
try {
|
||||
await bothStarted;
|
||||
expect(started).toEqual(["database", "storage"]);
|
||||
expect(order.indexOf("/workspaces/shop/adopt")).toBeLessThan(
|
||||
order.indexOf("/workspaces/shop/databases"),
|
||||
);
|
||||
expect(order).not.toContain("/workspaces/shop/resources/plan");
|
||||
releaseStorage();
|
||||
await Promise.resolve();
|
||||
expect(order).not.toContain("/workspaces/shop/resources/plan");
|
||||
releaseDatabase();
|
||||
const result = await run;
|
||||
expect(result.serviceEnv).toEqual({
|
||||
app: {
|
||||
DATABASE_URL: "postgresql://app",
|
||||
SHARED: "storage",
|
||||
AWS_ACCESS_KEY_ID: "key",
|
||||
},
|
||||
});
|
||||
expect(order.indexOf("/workspaces/shop/resources/plan")).toBeGreaterThan(
|
||||
order.indexOf("/workspaces/shop/storage"),
|
||||
);
|
||||
} finally {
|
||||
releaseStorage();
|
||||
releaseDatabase();
|
||||
await run.catch(() => {});
|
||||
}
|
||||
} finally {
|
||||
process.chdir(previousCwd);
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("creates workspace metadata without embedding source blobs", async () => {
|
||||
const calls: Array<{ path: string; init?: ApiRequestInit }> = [];
|
||||
const request: ApiRequester = async <T>(
|
||||
|
||||
Reference in New Issue
Block a user