evOo-local/packages/core/src/self-update.test.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

251 lines
8.1 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
DEFAULT_UPDATE_BRANCH,
decideUpdateAvailability,
detectRestartSupervisor,
isOfficialRepoUrl,
normalizeRepoUrl,
normalizeUpdateBranch,
OFFICIAL_REPO_URL,
parseGitStatusPorcelain,
RESTART_SUPERVISOR_ENV,
repoIdentity,
restartSupervisorAdvice,
shortCommit,
updateSteps,
} from "./self-update.js";
describe("normalizeRepoUrl", () => {
it("accepts the official repository and https forks", () => {
expect(normalizeRepoUrl(OFFICIAL_REPO_URL)).toEqual({
url: "https://github.com/elie222/rakazo",
});
expect(normalizeRepoUrl(" https://github.com/me/rakazo.git/ ")).toEqual({
url: "https://github.com/me/rakazo.git",
});
expect(normalizeRepoUrl("https://git.example.com:8443/team/rakazo.git")).toEqual({
url: "https://git.example.com:8443/team/rakazo.git",
});
});
it("accepts ssh remotes in both spellings", () => {
expect(normalizeRepoUrl("git@github.com:me/rakazo.git")).toEqual({
url: "git@github.com:me/rakazo.git",
});
expect(normalizeRepoUrl("ssh://git@github.com/me/rakazo.git")).toEqual({
url: "ssh://git@github.com/me/rakazo.git",
});
});
it("refuses transports and shapes that are not a git remote", () => {
for (const input of [
"",
" ",
"http://github.com/me/rakazo",
"git://github.com/me/rakazo",
"file:///etc/passwd",
"/srv/rakazo",
"https://github.com",
"https://github.com/onlyone",
"https://github.com/me/../../etc",
"https://user:secret@github.com/me/rakazo",
"https://token@github.com/me/rakazo",
"ssh://bad%0auser@github.com/me/rakazo",
"https://github.com/me/rakazo?x=1",
"https://github.com/me/rakazo#frag",
"https://github.com/me/rak azo",
"--upload-pack=touch /tmp/pwned",
"-o ProxyCommand=id",
`https://github.com/me/rakazo${"x".repeat(400)}`,
]) {
expect(normalizeRepoUrl(input), input).toHaveProperty("error");
}
});
it("rejects an embedded newline that could smuggle a second argument", () => {
expect(normalizeRepoUrl("https://github.com/me/rakazo\n--exec=id")).toHaveProperty("error");
});
});
describe("repoIdentity", () => {
it("treats every spelling of the same remote as one repository", () => {
const identity = "github.com/elie222/rakazo";
expect(repoIdentity("https://github.com/elie222/rakazo")).toBe(identity);
expect(repoIdentity("https://github.com/elie222/rakazo.git")).toBe(identity);
expect(repoIdentity("git@github.com:elie222/rakazo.git")).toBe(identity);
expect(repoIdentity("ssh://git@github.com/Elie222/Rakazo")).toBe(identity);
expect(repoIdentity("not a url")).toBeNull();
});
it("only calls the real upstream official", () => {
expect(isOfficialRepoUrl("git@github.com:elie222/rakazo.git")).toBe(true);
expect(isOfficialRepoUrl("https://github.com/attacker/rakazo")).toBe(false);
expect(isOfficialRepoUrl("https://githubb.com/elie222/rakazo")).toBe(false);
});
});
describe("normalizeUpdateBranch", () => {
it("accepts ordinary branch names", () => {
expect(normalizeUpdateBranch(" main ")).toEqual({ branch: "main" });
expect(normalizeUpdateBranch("release/1.2")).toEqual({ branch: "release/1.2" });
});
it("refuses names git would reject or that could be read as a flag", () => {
for (const input of [
"",
"with space",
"-f",
"a..b",
"tip@{1}",
"bad~1",
"x:y",
".hidden",
"feature/.hidden",
"feature//nested",
"trailing.",
"@",
"/lead",
"t/",
"x.lock",
]) {
expect(normalizeUpdateBranch(input), input).toHaveProperty("error");
}
});
});
describe("parseGitStatusPorcelain", () => {
it("reports a clean tree for empty output", () => {
expect(parseGitStatusPorcelain("")).toEqual({ clean: true, changed: [] });
expect(parseGitStatusPorcelain("\n \n")).toEqual({ clean: true, changed: [] });
});
it("lists tracked modifications", () => {
expect(parseGitStatusPorcelain(" M apps/api/src/app.ts\nA packages/core/src/x.ts\n")).toEqual({
clean: false,
changed: ["apps/api/src/app.ts", "packages/core/src/x.ts"],
});
});
});
describe("updateSteps", () => {
it("orders the work so migrations land immediately before the restart", () => {
const steps = updateSteps({
remoteUrl: OFFICIAL_REPO_URL,
branch: DEFAULT_UPDATE_BRANCH,
targetCommit: "2".repeat(40),
});
expect(steps.map((step) => step.id)).toEqual([
"fetch",
"merge",
"install",
"generate",
"build",
"migrate",
]);
});
it("passes every value as its own argument so nothing reaches a shell", () => {
const steps = updateSteps({
remoteUrl: "git@github.com:me/rakazo.git",
branch: "release/1.2",
targetCommit: "2".repeat(40),
repointRemote: true,
});
expect(steps[0]).toEqual({
id: "remote",
label: "Point the checkout at the chosen repository",
command: "git",
args: ["remote", "set-url", "origin", "git@github.com:me/rakazo.git"],
});
expect(steps.find((step) => step.id === "merge")?.args).toEqual([
"merge",
"--ff-only",
"2".repeat(40),
]);
});
it("never rewrites history, so local commits fail the update instead of vanishing", () => {
const args = updateSteps({
remoteUrl: OFFICIAL_REPO_URL,
branch: "main",
targetCommit: "2".repeat(40),
}).flatMap((step) => step.args);
expect(args).not.toContain("--hard");
expect(args).not.toContain("--force");
expect(args).toContain("--ff-only");
});
});
describe("detectRestartSupervisor", () => {
it("takes the operator's declaration first", () => {
expect(detectRestartSupervisor({ [RESTART_SUPERVISOR_ENV]: "docker" })).toEqual({
kind: "declared",
label: "docker",
});
});
it("recognises systemd and pm2 from the variables they set", () => {
expect(detectRestartSupervisor({ INVOCATION_ID: "abc" })).toEqual({
kind: "systemd",
label: "systemd",
});
expect(detectRestartSupervisor({ pm_id: "0" })).toEqual({ kind: "pm2", label: "pm2" });
});
it("reports none for a bare checkout, and says how to fix it", () => {
expect(detectRestartSupervisor({})).toEqual({ kind: "none", label: null });
expect(detectRestartSupervisor({ [RESTART_SUPERVISOR_ENV]: " " })).toEqual({
kind: "none",
label: null,
});
expect(restartSupervisorAdvice(detectRestartSupervisor({}))).toContain(RESTART_SUPERVISOR_ENV);
expect(restartSupervisorAdvice({ kind: "systemd", label: "systemd" })).toContain("systemd");
});
});
describe("decideUpdateAvailability", () => {
it("refuses when the checkout cannot be updated at all", () => {
expect(decideUpdateAvailability({ checkoutReason: "Not a git checkout." })).toEqual({
status: "unavailable",
reason: "Not a git checkout.",
});
expect(decideUpdateAvailability({ commit: "abc" })).toEqual({
status: "unavailable",
reason: "Could not read the current commit.",
});
});
it("refuses a dirty tree before comparing commits", () => {
expect(
decideUpdateAvailability({
commit: "aaa",
targetCommit: "bbb",
status: { clean: false, changed: ["apps/api/src/app.ts"] },
}),
).toEqual({ status: "dirty", changed: ["apps/api/src/app.ts"] });
});
it("separates up to date from an available update", () => {
const clean = { clean: true, changed: [] };
expect(decideUpdateAvailability({ commit: "aaa", targetCommit: "aaa", status: clean })).toEqual(
{ status: "up-to-date", commit: "aaa" },
);
expect(
decideUpdateAvailability({
commit: "aaa",
targetCommit: "bbb",
behindBy: 4,
status: clean,
}),
).toEqual({ status: "available", commit: "aaa", targetCommit: "bbb", behindBy: 4 });
});
});
describe("shortCommit", () => {
it("shortens a sha and names the unknown case", () => {
expect(shortCommit("3c6e2091a2b3c4")).toBe("3c6e209");
expect(shortCommit(null)).toBe("unknown");
expect(shortCommit("")).toBe("unknown");
});
});