feat: release 2.7.0-rc3

This commit is contained in:
2026-10-07 05:10:59 +00:00 Unverified
parent c8de9ddcba
commit 2a24b2ff56
14 changed files with 1302 additions and 249 deletions
+285 -3
View File
@@ -7,6 +7,7 @@ import { initializeProject } from "../../command/init";
import { managedHeader } from "../../lib/scaffold";
import { readTrust, resolveTrustIdentity, updateTrust } from "../../lib/trust";
import { KuberApiError } from "../../lib/api";
import type { ScaffoldQuestion } from "../../lib/scaffold-prompts";
const roots: string[] = [];
const originalConfig = process.env.XDG_CONFIG_HOME;
@@ -24,6 +25,189 @@ async function root() {
}
describe("kuber init", () => {
test("interactive YAML fields finish before login and automatic trust", async () => {
const cwd = await root();
const events: string[] = [];
const session = {
token: "test",
expiresAt: "2099-01-01",
user: { username: "a", roles: [] },
};
const log = spyOn(console, "log").mockImplementation(() => {});
try {
await initializeProject(
cwd,
{},
{
prompt: async (question) => {
events.push(question.name);
expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe(
false,
);
if (question.name === "source") return "image";
if (question.name === "image") return "nginx:stable";
if (question.name === "path") return ".";
expect(question.type).toBe("snippet");
expect(question.template).toStartWith(
"name: ${project}\nservices:\n ${name}:",
);
expect(question.template).not.toContain("managedBy");
const project = question.fields?.find(
({ name }) => name === "project",
);
expect(project?.validate?.("Bad Project")).not.toBe(true);
expect(project?.validate?.("demo")).toBe(true);
return {
values: { project: "demo", name: "web", replicas: "1" },
result: "malicious preview",
};
},
session: async () => {
events.push("session");
return undefined;
},
login: async () => {
events.push("login");
expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe(
false,
);
return session;
},
request: async (_path, init) => {
events.push(init?.method ?? "GET");
return (init?.method ? undefined : { fingerprints: [] }) as never;
},
persistTrust: async () => {
events.push("trust");
},
},
);
expect(events).toEqual([
"source",
"image",
"path",
"compose",
"session",
"login",
"GET",
"POST",
"trust",
]);
const content = await readFile(join(cwd, "compose.yml"), "utf8");
expect(content.split("\n")[0]).toBe(managedHeader);
expect(YAML.parse(content)).toEqual({
managedBy: "kuber",
name: "demo",
services: { web: { image: "nginx:stable", deploy: { replicas: 1 } } },
});
expect(content).not.toContain("malicious");
} finally {
log.mockRestore();
}
});
test("flagged project and configured project bypass editable project field; existing login is reused", async () => {
for (const configured of [false, true]) {
const cwd = await root();
if (configured)
await writeFile(
join(cwd, ".kuberrc.ts"),
'export default { project: "fixed" };\n',
);
let logins = 0;
const questions: ScaffoldQuestion[] = [];
const log = spyOn(console, "log").mockImplementation(() => {});
try {
await initializeProject(
cwd,
{
source: "auto",
cnb: "",
path: ".",
...(configured ? {} : { project: "fixed" }),
},
{
prompt: async (question) => {
questions.push(question);
expect(question.template).toStartWith('name: "fixed"');
expect(
question.fields?.some(({ name }) => name === "project"),
).toBe(false);
return {
values: { project: "ignored", name: "web", replicas: "1" },
};
},
session: async () => ({
token: "test",
expiresAt: "2099-01-01",
user: { username: "a", roles: [] },
}),
login: async () => {
logins++;
throw new Error("unneeded login");
},
request: async (_path, init) =>
(init?.method ? undefined : { fingerprints: [] }) as never,
persistTrust: async () => {},
},
);
expect(questions.map(({ type }) => type)).toEqual(["snippet"]);
expect(logins).toBe(0);
expect(
YAML.parse(await readFile(join(cwd, "compose.yml"), "utf8")),
).toMatchObject({
name: "fixed",
services: { web: { build: "auto" } },
});
} finally {
log.mockRestore();
}
}
});
test("cancel or invalid snippet performs no login, trust, or filesystem writes", async () => {
for (const failure of ["cancel", "invalid"]) {
const cwd = await root();
let downstream = 0;
await expect(
initializeProject(
cwd,
{ source: "template", template: "bun-service", path: "." },
{
prompt: async () => {
if (failure === "cancel") throw new Error("cancelled");
return {
values: {
project: "demo",
name: "web",
cpu: "-1",
replicas: "1",
},
};
},
session: async () => {
downstream++;
return undefined;
},
login: async () => {
downstream++;
throw new Error("unexpected login");
},
request: async () => {
downstream++;
return undefined as never;
},
},
),
).rejects.toThrow(
failure === "cancel" ? "cancelled" : "CPU must be a positive",
);
expect(downstream).toBe(0);
for (const name of ["compose.yml", "Dockerfile", ".dockerignore"])
expect(await Bun.file(join(cwd, name)).exists()).toBe(false);
}
});
test("uses an existing configured project for Compose and trust", async () => {
const cwd = await root();
await writeFile(
@@ -146,14 +330,14 @@ describe("kuber init", () => {
};
},
request: async (_path, init) => {
if (++attempts === 1) throw new KuberApiError("revoked", 401);
if (++attempts === 2) throw new KuberApiError("revoked", 401);
return (
init?.method === "POST" ? undefined : { fingerprints: [] }
) as never;
},
},
);
expect([attempts, logins]).toEqual([3, 1]);
expect([attempts, logins]).toEqual([4, 1]);
expect(await readTrust()).toHaveLength(1);
} finally {
log.mockRestore();
@@ -361,7 +545,7 @@ describe("kuber init", () => {
request: async (_path, init) => {
calls.push(init?.method ?? "GET");
if (init?.method === "POST")
throw new KuberApiError("grant failed", 503);
throw new KuberApiError("grant failed", 400);
return { fingerprints: [] } as never;
},
},
@@ -374,6 +558,104 @@ describe("kuber init", () => {
expect(await Bun.file(join(cwd, ".dockerignore")).exists()).toBe(false);
});
test("committed POST with lost response keeps generated files after reconciliation", async () => {
const cwd = await root();
const identity = await resolveTrustIdentity("demo", cwd);
const trusted = new Set<string>();
const calls: string[] = [];
await expect(
initializeProject(
cwd,
{
nonInteractive: true,
project: "demo",
source: "template",
template: "static-nginx",
},
{
session: async () => ({
token: "test",
expiresAt: "2099-01-01",
user: { username: "a", roles: [] },
}),
request: async (_path, init) => {
calls.push(init?.method ?? "GET");
if (init?.method === "POST") {
trusted.add(identity.fingerprint);
throw new Error("socket closed after commit");
}
return { fingerprints: [...trusted] } as never;
},
},
),
).rejects.toThrow(/registration succeeded.*files were kept/);
expect(calls).toEqual(["GET", "POST", "GET"]);
expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe(true);
expect(await Bun.file(join(cwd, "Dockerfile")).exists()).toBe(true);
expect(await Bun.file(join(cwd, ".dockerignore")).exists()).toBe(true);
});
test("unavailable reconciliation keeps generated files and explains repair", async () => {
const cwd = await root();
const calls: string[] = [];
await expect(
initializeProject(
cwd,
{
nonInteractive: true,
project: "demo",
},
{
session: async () => ({
token: "test",
expiresAt: "2099-01-01",
user: { username: "a", roles: [] },
}),
request: async (_path, init) => {
calls.push(init?.method ?? "GET");
if (init?.method === "POST") throw new Error("response lost");
if (calls.length === 3)
throw new Error("trust service unavailable");
return { fingerprints: [] } as never;
},
},
),
).rejects.toThrow(/Could not confirm.*kuber trust status.*kuber trust/);
expect(calls).toEqual(["GET", "POST", "GET"]);
expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe(true);
});
test("concurrent grant found during reconciliation keeps generated files", async () => {
const cwd = await root();
const identity = await resolveTrustIdentity("demo", cwd);
const calls: string[] = [];
await expect(
initializeProject(
cwd,
{
nonInteractive: true,
project: "demo",
},
{
session: async () => ({
token: "test",
expiresAt: "2099-01-01",
user: { username: "a", roles: [] },
}),
request: async (_path, init) => {
calls.push(init?.method ?? "GET");
if (init?.method === "POST") throw new Error("response lost");
return {
fingerprints: calls.length === 3 ? [identity.fingerprint] : [],
} as never;
},
},
),
).rejects.toThrow(/registration succeeded.*files were kept/);
expect(calls).toEqual(["GET", "POST", "GET"]);
expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe(true);
});
test("existing Compose file is not overwritten and no grant is attempted", async () => {
const cwd = await root();
const composePath = join(cwd, "compose.yml");