fix tests, improve coverage, make git more resilient to weird environments
Msrc/services/git.ts
@@ -3,7 +3,18 @@ import { $ as _$ } from "bun";
import { REPOS_DIR } from "../constants.ts";
const $ = _$.env({ ...process.env, LC_ALL: "C", LANG: "C" });
const gitEnv = {
...process.env,
LC_ALL: "C",
LANG: "C",
GIT_CONFIG_GLOBAL: "/dev/null",
GIT_CONFIG_SYSTEM: "/dev/null",
GIT_CONFIG_COUNT: "0",
GIT_ASKPASS: "echo",
GIT_TERMINAL_PROMPT: "0",
};
const $ = _$.env(gitEnv);
// Per-repo mutex: prevents concurrent git write operations on the same repo
// (e.g. two patches being merged simultaneously, which would corrupt the index).
@@ -55,7 +66,6 @@ export async function archiveRepo(
): Promise<void> {
const p = repoPath(repoName);
const base = `${slug}-${commitHash.slice(0, 8)}`;
const env = { ...process.env, LC_ALL: "C", LANG: "C" };
const zip = Bun.spawn(
[
@@ -67,7 +77,7 @@ export async function archiveRepo(
`--output=${path.join(outDir, `${base}.zip`)}`,
commitHash,
],
{ signal, env },
{ signal, env: gitEnv },
);
if ((await zip.exited) !== 0) throw new Error("git archive (zip) failed");
@@ -81,7 +91,7 @@ export async function archiveRepo(
`--output=${path.join(outDir, `${base}.tar.gz`)}`,
commitHash,
],
{ signal, env },
{ signal, env: gitEnv },
);
if ((await tgz.exited) !== 0)
throw new Error("git archive (tar.gz) failed");
@@ -89,11 +99,11 @@ export async function archiveRepo(
try {
const tar = Bun.spawn(
["git", "-C", p, "archive", "--format=tar", commitHash],
{ signal, env, stdout: "pipe" },
{ signal, env: gitEnv, stdout: "pipe" },
);
const zst = Bun.spawn(
["zstd", "-o", path.join(outDir, `${base}.tar.zst`)],
{ signal, env, stdin: tar.stdout },
{ signal, env: gitEnv, stdin: tar.stdout },
);
await Promise.all([tar.exited, zst.exited]);
} catch {
Msrc/views/Settings.tsx
@@ -283,8 +283,7 @@ export function Settings({
{passkeys.map((pk) => (
<div class="passkey-item">
<span class="passkey-date">
Added{" "}
{formatDate(pk.created_at)}
Added {formatDate(pk.created_at)}
</span>
<form
method="POST"
@@ -350,8 +349,7 @@ export function Settings({
{key.fingerprint}
</span>
<span class="passkey-date">
Added{" "}
{formatDate(key.created_at)}
Added {formatDate(key.created_at)}
</span>
</div>
<form
Mtests/e2e.test.ts
@@ -454,6 +454,15 @@ describe('repos', () => {
expect(await alicePage.locator('.repo-tab[href$="/settings"]').count()).toBe(0);
} finally { await alicePage.close(); }
});
test('create repository with invalid name shows error', async () => {
const resp = await adminCtx.request.post(`${BASE}/new`, {
form: { name: 'has spaces!', description: '', default_branch: 'main' },
maxRedirects: 0,
});
expect(resp.status()).toBe(200);
expect(await resp.text()).toContain('Invalid repository name');
});
});
// ─── Issues ───────────────────────────────────────────────────────────────────
@@ -461,6 +470,7 @@ describe('repos', () => {
describe('issues', () => {
let adminCtx: BrowserContext;
let issueUrl: string;
let completedIssueUrl: string;
beforeAll(async () => {
adminCtx = await loggedInContext();
@@ -534,7 +544,7 @@ describe('issues', () => {
const page = await adminCtx.newPage();
try {
await page.goto(issueUrl);
await page.click('.issue-detail-meta-actions button');
await page.click('form[action*="/close"] button');
await page.waitForURL(new RegExp(issueUrl.replace(BASE, '')));
expect(await page.locator('.issue-badge').textContent()).toBe('closed');
} finally { await page.close(); }
@@ -558,6 +568,67 @@ describe('issues', () => {
expect(await page.locator('.issue-badge').textContent()).toBe('open');
} finally { await page.close(); }
});
test('completed button marks issue as completed', async () => {
const page = await adminCtx.newPage();
try {
await page.goto(`${BASE}/my-repo/issues/new`);
await page.fill('[name=title]', 'To be completed');
await page.click('form[action$="/issues"] button[type=submit]');
await page.waitForURL(/\/my-repo\/issues\/\d+/);
completedIssueUrl = page.url();
await page.click('form[action*="/complete"] button');
await page.waitForURL(new RegExp(completedIssueUrl.replace(BASE, '')));
expect(await page.locator('.issue-badge').textContent()).toBe('completed');
} finally { await page.close(); }
});
test('completed issue appears in completed list', async () => {
const page = await adminCtx.newPage();
try {
await page.goto(`${BASE}/my-repo/issues?status=completed`);
const titles = await page.locator('.issue-title').allTextContents();
expect(titles.some(t => t.includes('To be completed'))).toBe(true);
} finally { await page.close(); }
});
test('non-admin cannot complete or close issue', async () => {
const issueNum = issueUrl.split('/issues/')[1];
const ctx = await browser.newContext();
try {
const completeResp = await ctx.request.post(
`${BASE}/my-repo/issues/${issueNum}/complete`,
{ maxRedirects: 0 },
);
expect(completeResp.status()).toBe(302);
expect(completeResp.headers()['location']).toContain('/login');
} finally { await ctx.close(); }
});
test('reacting with same emoji toggles it off', async () => {
const page = await adminCtx.newPage();
try {
await page.goto(issueUrl);
// Reaction was added by the earlier 'react to issue' test
expect(await page.locator('.reaction-btn').count()).toBeGreaterThan(0);
await page.locator('.reaction-btn').first().click();
await page.waitForURL(new RegExp(issueUrl.replace(BASE, '')));
expect(await page.locator('.reaction-btn').count()).toBe(0);
} finally { await page.close(); }
});
test('react to issue comment', async () => {
const page = await adminCtx.newPage();
try {
await page.goto(issueUrl);
const commentItem = page.locator('.timeline-item:not(.timeline-item-new)')
.filter({ hasText: 'A follow-up comment.' });
await commentItem.locator('.reaction-add-btn').click();
await commentItem.locator('.reaction-picker-btn').first().click();
await page.waitForURL(new RegExp(issueUrl.replace(BASE, '')));
expect(await commentItem.locator('.reaction-btn').count()).toBeGreaterThan(0);
} finally { await page.close(); }
});
});
// ─── Patches ──────────────────────────────────────────────────────────────────
@@ -607,6 +678,12 @@ describe('patches', () => {
beforeAll(async () => {
adminCtx = await loggedInContext();
// Git identity is required to submit patches
await adminCtx.request.fetch(`${BASE}/settings/git-identity`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: 'git_name=Admin+User&git_email=admin%40test.com',
});
});
afterAll(async () => { await adminCtx.close(); });
@@ -778,12 +855,17 @@ describe('patches', () => {
const page = await adminCtx.newPage();
try {
await page.goto(conflictPatchUrl);
// Edit title via title form
await page.click('details.title-edit-details summary');
await page.fill('.title-edit-form-area [name=title]', 'Edited Conflict Patch');
await page.click('.title-edit-form-area [type=submit]');
await page.waitForURL(new RegExp(conflictPatchUrl.replace(BASE, '')));
expect(await page.locator('.issue-detail-title').textContent()).toBe('Edited Conflict Patch');
// Edit description via inline form
await page.click('.timeline-author .inline-edit-details summary');
await page.fill('.inline-edit-form-area [name=title]', 'Edited Conflict Patch');
await page.fill('.inline-edit-form-area [name=edit_description]', 'Updated desc');
await page.click('.inline-edit-form-area [type=submit]');
await page.waitForURL(new RegExp(conflictPatchUrl.replace(BASE, '')));
expect(await page.locator('.issue-detail-title').textContent()).toBe('Edited Conflict Patch');
} finally { await page.close(); }
});
@@ -817,6 +899,21 @@ describe('patches', () => {
expect(await page.locator('.reaction-btn').first().textContent()).toMatch(/\d/);
} finally { await page.close(); }
});
test('admin can delete a patch', async () => {
const page = await adminCtx.newPage();
try {
const patchNum = conflictPatchUrl.split('/patches/')[1];
const resp = await page.request.post(`${BASE}/my-repo/patches/${patchNum}/delete`, {
maxRedirects: 0,
});
expect(resp.status()).toBe(302);
expect(resp.headers()['location']).toContain('/patches');
// Patch should no longer be accessible
const checkResp = await page.request.get(conflictPatchUrl);
expect(checkResp.status()).toBe(404);
} finally { await page.close(); }
});
});
// ─── Pagination ───────────────────────────────────────────────────────────────
@@ -826,6 +923,12 @@ describe('pagination', () => {
beforeAll(async () => {
adminCtx = await loggedInContext();
// Git identity is required to submit patches
await adminCtx.request.fetch(`${BASE}/settings/git-identity`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: 'git_name=Admin+User&git_email=admin%40test.com',
});
// Create a repo dedicated to pagination testing
const page = await adminCtx.newPage();
@@ -1792,6 +1895,33 @@ describe('settings', () => {
expect(resp.status()).toBe(302);
expect(resp.headers()['location']).toContain('error');
});
test('git identity: empty name shows error', async () => {
const resp = await adminCtx.request.post(`${BASE}/settings/git-identity`, {
form: { git_name: ' ', git_email: 'test@example.com' },
maxRedirects: 0,
});
expect(resp.status()).toBe(302);
expect(resp.headers()['location']).toContain('error=Git+name+is+required');
});
test('git identity: empty email shows error', async () => {
const resp = await adminCtx.request.post(`${BASE}/settings/git-identity`, {
form: { git_name: 'Test User', git_email: ' ' },
maxRedirects: 0,
});
expect(resp.status()).toBe(302);
expect(resp.headers()['location']).toContain('error=Git+email+is+required');
});
test('git identity: valid values redirect with success', async () => {
const resp = await aliceCtx.request.post(`${BASE}/settings/git-identity`, {
form: { git_name: 'Alice Smith', git_email: 'alice@example.com' },
maxRedirects: 0,
});
expect(resp.status()).toBe(302);
expect(resp.headers()['location']).toContain('success=git_identity');
});
});
// ─── Repository deletion ──────────────────────────────────────────────────────
@@ -1892,6 +2022,22 @@ describe('404 handling', () => {
expect(resp.status()).toBe(404);
} finally { await page.close(); }
});
test('non-existent patch returns 404', async () => {
const page = await adminCtx.newPage();
try {
const resp = await page.request.get(`${BASE}/my-repo/patches/99999`);
expect(resp.status()).toBe(404);
} finally { await page.close(); }
});
test('non-existent release returns 404', async () => {
const page = await adminCtx.newPage();
try {
const resp = await page.request.get(`${BASE}/my-repo/releases/99999`);
expect(resp.status()).toBe(404);
} finally { await page.close(); }
});
});
// ─── Issue editing and deletion ───────────────────────────────────────────────
@@ -1926,12 +2072,25 @@ describe('issue editing', () => {
const page = await adminCtx.newPage();
try {
await page.goto(issueUrl);
await page.locator('.timeline-author .inline-edit-details').first().locator('summary').click();
await page.fill('.inline-edit-form-area [name=title]', 'Edited issue title');
// Edit title via title form
await page.click('details.title-edit-details summary');
await page.fill('.title-edit-form-area [name=title]', 'Edited issue title');
await page.click('.title-edit-form-area [type=submit]');
await page.waitForURL(new RegExp(issueUrl.replace(BASE, '')));
expect(await page.locator('.issue-detail-title').textContent()).toBe('Edited issue title');
// Edit body via inline form
await page.click('.timeline-author .inline-edit-details summary');
await page.fill('.inline-edit-form-area [name=edit_body]', 'Updated body text.');
await page.click('.inline-edit-form-area [type=submit]');
await page.waitForURL(new RegExp(issueUrl.replace(BASE, '')));
expect(await page.locator('.issue-detail-title').textContent()).toBe('Edited issue title');
} finally { await page.close(); }
});
test('edited marker appears after editing', async () => {
const page = await adminCtx.newPage();
try {
await page.goto(issueUrl);
expect(await page.locator('time.edited-indicator').count()).toBeGreaterThan(0);
} finally { await page.close(); }
});