Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commit9b2d870unknown_key

test(e2e): add PR command center smoke test + missing-git-repo regression

test(e2e): add PR command center smoke test + missing-git-repo regression

Two new Playwright suites in pulls.spec.ts:

1. "/pulls command center" — verifies the global PR dashboard doesn't
   500 for both unauthenticated and authenticated visitors.

2. "PR detail — missing git repo (regression)" — reproduces the exact
   scenario that caused the McCracken/Gluecron.com #1 500: creates a
   real repo + PR via the UI, then renames the bare git directory out
   of place, and asserts the PR detail page still renders (HTTP 200,
   no "Internal Server Error" text).  Restores the directory in
   afterAll so the test environment stays clean.

These tests would have caught the crash fixed in 6f1fd83 before it
reached production.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFjwnPstUAEMzxmM4DrMiQ
Claude committed on June 18, 2026Parent: 6f1fd83
1 file changed+13409b2d8700030449c13c7b25e6633a448894812060
1 changed file+134−0
Modifiede2e/pulls.spec.ts+134−0View fileUnifiedSplit
99 */
1010
1111import { test, expect } from "@playwright/test";
12import * as path from "path";
13import * as fs from "fs/promises";
1214import {
1315 uid,
1416 TEST_PASSWORD,
262264 });
263265 });
264266});
267
268// ---------------------------------------------------------------------------
269// PR command center — /pulls smoke test
270// ---------------------------------------------------------------------------
271
272test.describe("PR command center (/pulls)", () => {
273 test("page loads without 500 when unauthenticated", async ({ page }) => {
274 const res = await page.goto("/pulls");
275 // Unauthenticated users get redirected to login — either way, no 500
276 expect(res?.status()).not.toBe(500);
277 await expect(page.locator("body")).not.toContainText(/500|Internal Server Error/i);
278 });
279
280 test("page loads without 500 when authenticated", async ({ page }) => {
281 const user = uid("pdash");
282 await page.goto("/register");
283 await page.fill('input[name="username"]', user);
284 await page.fill('input[name="email"]', `${user}@test.example`);
285 await page.fill('input[name="password"]', TEST_PASSWORD);
286 await page.click('button[type="submit"]');
287 await page.waitForURL(/\/(dashboard|[a-z])/);
288
289 const res = await page.goto("/pulls");
290 expect(res?.status()).not.toBe(500);
291 await expect(page.locator("body")).not.toContainText(/500|Internal Server Error/i);
292 });
293});
294
295// ---------------------------------------------------------------------------
296// Regression: PR detail must not 500 when git repo dir is missing
297// (Fixes: 500 on McCracken/Gluecron.com #1 — bare repo missing from FS)
298// ---------------------------------------------------------------------------
299
300test.describe("PR detail — missing git repo (regression)", () => {
301 let prUrl: string;
302 let ghostOwner: string;
303 let ghostRepo: string;
304 let gitRepoPath: string;
305 let backupPath: string;
306 let ghostTmpDir: string;
307
308 test.beforeAll(async ({ browser }) => {
309 ghostOwner = uid("ghostuser");
310 ghostRepo = uid("ghostrepo");
311 const branch = uid("ghost-feat-");
312
313 const page = await browser.newPage();
314
315 // Register
316 await page.goto("/register");
317 await page.fill('input[name="username"]', ghostOwner);
318 await page.fill('input[name="email"]', `${ghostOwner}@test.example`);
319 await page.fill('input[name="password"]', TEST_PASSWORD);
320 await page.click('button[type="submit"]');
321 await page.waitForURL(/\/(dashboard|[a-z])/);
322
323 // Create repo
324 await page.goto("/new");
325 await page.fill('input[name="name"]', ghostRepo);
326 await page.click('button[type="submit"]');
327 await page.waitForURL(new RegExp(`/${ghostOwner}/${ghostRepo}`));
328 await page.close();
329
330 // Push initial commit + feature branch
331 ghostTmpDir = await pushTestCommit({
332 owner: ghostOwner,
333 repo: ghostRepo,
334 username: ghostOwner,
335 password: TEST_PASSWORD,
336 fileName: "README.md",
337 fileContent: `# ${ghostRepo}\n`,
338 commitMsg: "Initial commit",
339 });
340 await pushFeatureBranch({
341 repoDir: ghostTmpDir,
342 username: ghostOwner,
343 branchName: branch,
344 fileName: "ghost.md",
345 fileContent: "Ghost feature.\n",
346 commitMsg: "Add ghost feature",
347 });
348
349 // Create the PR via UI
350 const prPage = await browser.newPage();
351 await prPage.goto("/login");
352 await prPage.fill('input[name="username"]', ghostOwner);
353 await prPage.fill('input[name="password"]', TEST_PASSWORD);
354 await prPage.click('button[type="submit"]');
355 await prPage.waitForURL(/\/(dashboard|[a-z])/);
356
357 await prPage.goto(`/${ghostOwner}/${ghostRepo}/compare/main...${branch}`);
358 const titleInput = prPage.locator('input[name="title"]');
359 await titleInput.waitFor({ timeout: 8_000 });
360 await titleInput.fill(`Ghost PR ${uid("gp")}`);
361 await prPage.click('button[type="submit"]');
362 await prPage.waitForURL(new RegExp(`/${ghostOwner}/${ghostRepo}/pulls/\\d+`));
363 prUrl = prPage.url();
364 await prPage.close();
365
366 // Simulate missing git repo directory — rename it out of the way
367 const reposRoot =
368 process.env.GIT_REPOS_PATH ?? path.join(process.cwd(), "repos");
369 gitRepoPath = path.join(reposRoot, ghostOwner, `${ghostRepo}.git`);
370 backupPath = `${gitRepoPath}.missing`;
371 await fs.rename(gitRepoPath, backupPath).catch(() => {
372 // If rename fails (e.g. repo dir doesn't exist for some reason), continue
373 });
374 });
375
376 test.afterAll(async () => {
377 // Restore the git repo directory so the test environment stays clean
378 if (backupPath && gitRepoPath) {
379 await fs.rename(backupPath, gitRepoPath).catch(() => {});
380 }
381 await cleanupDir(ghostTmpDir);
382 });
383
384 test("PR detail renders without 500 when git repo is missing from disk", async ({
385 page,
386 }) => {
387 const res = await page.goto(prUrl);
388 // Must not be a 500
389 expect(res?.status()).not.toBe(500);
390 await expect(page.locator("body")).not.toContainText(
391 /500|Internal Server Error/i
392 );
393 // PR data from DB should still be visible (title / state)
394 await expect(page.locator("body")).toContainText(/open|ghost/i, {
395 timeout: 8_000,
396 });
397 });
398});
265399