syntaxai/tdd.md · main · e2e / git-native-forgejo-down.spec.ts
// E2E DRAMATIC PROOF: kill Forgejo, the CMS still commits.
//
// Stops the forgejo service on p620, performs an admin web-edit,
// asserts that the commit lands in our local bare repo, then brings
// Forgejo back up. If Forgejo were on tdd.md's edit path this would
// fail — the test passes only because c14_git owns the commit path
// and Forgejo is genuinely unused for syntaxai/tdd.md.
//
// Run this only when you can tolerate a few seconds of Forgejo
// downtime (agent kata pushes will fail during the test). It's a
// separate spec file so you can opt in: bunx playwright test
// e2e/git-native-forgejo-down.spec.ts.
import { test, expect } from "@playwright/test";
import * as fs from "fs";
import * as path from "path";
import { execSync } from "child_process";
const SCREENSHOT_DIR = "test-results/git-native-forgejo-down";
// When run inside a Flatpak sandbox (e.g. the VS Code .flatpak), `ssh` lives
// on the host so we have to hop through flatpak-spawn. When run on the host
// directly, flatpak-spawn isn't on PATH — call ssh straight. Detect by env.
const SSH_HOP = process.env.FLATPAK_ID ? "flatpak-spawn --host ssh" : "ssh";
const sshExec = (cmd: string): string =>
execSync(`${SSH_HOP} p620 '${cmd}'`, { encoding: "utf8" }).trim();
const sshGit = (args: string): string =>
sshExec(`git --git-dir=/home/scri/repos/tdd.md.git ${args}`);
const forgejoIsUp = (): boolean => {
try {
// The container's HTTP API answers when Forgejo is up; returns
// empty/error when stopped. We check the published port from the
// host's perspective via SSH — same network path the tdd-md
// container would use.
const out = sshExec(
"curl -s -o /dev/null -w '%{http_code}' --max-time 2 http://localhost:44400/api/v1/version || echo 000",
);
return out === "200";
} catch {
return false;
}
};
test.beforeAll(() => {
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
});
const ADMIN_AUTH_FILE = ".auth/admin.json";
test.describe("CMS works with Forgejo stopped", () => {
// Opt-in via env: this test stops the live forgejo container briefly to
// prove the CMS is git-native. Don't fire it from a casual `bun x
// playwright test` — only when the operator is OK with a few seconds of
// forgejo downtime.
test.skip(
process.env.RUN_DESTRUCTIVE_E2E !== "1",
"set RUN_DESTRUCTIVE_E2E=1 to run — this stops the live forgejo container",
);
test.skip(!fs.existsSync(ADMIN_AUTH_FILE), `no ${ADMIN_AUTH_FILE} found`);
test.use({ storageState: ADMIN_AUTH_FILE });
// The teardown re-starts forgejo even if the test fails halfway.
test.afterAll(async () => {
try {
sshExec("systemctl --user start forgejo.service 2>/dev/null || true");
sshExec("sleep 3");
} catch {
// Best effort.
}
});
test("admin edit succeeds while forgejo container is stopped", async ({ page, request }) => {
// Confirm forgejo is currently up before we start.
expect(forgejoIsUp()).toBe(true);
// STOP FORGEJO via systemd — the .service has Restart=always, so
// `podman stop` would just trigger a restart. Stopping the unit
// keeps it down for the duration of the test.
sshExec("systemctl --user stop forgejo.service");
sshExec("sleep 2");
expect(forgejoIsUp()).toBe(false);
try {
const headBefore = sshGit("rev-parse refs/heads/main");
const before = await (await request.get("/content/sama/skill.md")).text();
const marker = `<!-- forgejo-down proof @ ${new Date().toISOString()} -->`;
const newBody = before + "\n" + marker + "\n";
await page.goto("/edit/sama/skill");
const textarea = page.locator("textarea[name='body']");
await expect(textarea).toBeVisible();
await textarea.fill(newBody);
await page.locator("button[type='submit']").click();
// The save must succeed despite Forgejo being down. If it
// didn't, something in the edit path is still calling Forgejo
// and we have not actually decoupled.
await expect(page.getByRole("heading", { name: /applied live/i })).toBeVisible({ timeout: 8000 });
const commitLink = page.locator('a[href^="/GIT/syntaxai/tdd.md/commit/"]');
await expect(commitLink).toBeVisible();
const newSha = (await commitLink.getAttribute("href"))!
.replace("/GIT/syntaxai/tdd.md/commit/", "");
await page.screenshot({
path: path.join(SCREENSHOT_DIR, "1-applied-live-while-forgejo-down.png"),
fullPage: true,
});
// Bare repo advanced.
const headAfter = sshGit("rev-parse refs/heads/main");
expect(headAfter).toBe(newSha);
expect(headAfter).not.toBe(headBefore);
// /GIT/.../commit/<sha> must also still render — read path is
// also c14_git, no Forgejo involvement.
const commitPage = await page.goto(`/GIT/syntaxai/tdd.md/commit/${newSha}`);
expect(commitPage?.status()).toBe(200);
await page.screenshot({
path: path.join(SCREENSHOT_DIR, "2-commit-view-while-forgejo-down.png"),
fullPage: true,
});
} finally {
// Restart forgejo so agent kata pushes work again.
sshExec("systemctl --user start forgejo.service");
}
});
});