Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
claude/adoring-hopper-5x74bqclaude/affectionate-feynman-ykrf1hclaude/architecture-audit-design-wxprenclaude/build-status-update-3MXsfclaude/charming-meitner-mllb5rclaude/compare-gate-gluecron-s4mFQclaude/confident-faraday-tikcwbclaude/continue-work-XMTlIclaude/crontech-gluecron-deploy-7MIECclaude/crontech-platform-setup-SeKfwclaude/design-2026claude/ecstatic-ptolemy-jMdigclaude/enhance-github-integration-QNHdGclaude/fix-aa-loop-issue-PonMQclaude/fix-actions-and-processclaude/fix-desktop-errors-XqoW8claude/fix-red-workflowsclaude/fix-website-access-6FKJNclaude/gatetest-integration-hardeningclaude/github-audit-improvements-bDFr9claude/gluecron-launch-status-FoMRlclaude/hopeful-lamport-olfCTclaude/issue-to-pr-and-protectionsclaude/jolly-heisenberg-2sg1Qclaude/launch-preparation-QmTb6claude/new-session-xk1l7claude/plan-platform-architecture-kkN4yclaude/platform-analysis-roadmap-1nUGLclaude/platform-launch-assessment-8dWV8claude/polish-platform-release-AeDrUclaude/resume-previous-work-KzyLwclaude/review-crontech-handoff-qYEVqclaude/review-project-completeness-lHhS2claude/review-readme-docs-ulqPKclaude/serene-edison-rj87weclaude/setup-multi-repo-dev-BCwNQclaude/ship-fixes-and-tests-Jvz1cclaude/site-audit-competitive-pctlwgclaude/site-migration-vercel-XstpKclaude/standalone-product-repos-XHFTDcopilot/feat-smart-empty-states-keyboard-first-enhancementcopilot/feat-smart-morning-digest-review-context-restorecopilot/fix-and-process-workflowscopilot/update-ai-powered-code-reviewfeat/debt-mapfeat/push-policy-codeowners-hardeningfeat/smart-digest-contextfeat/stage-impactfeat/t1-secret-migrationfeat/u-polishfeat/w-self-hostfeat/w2-claude-configfix/agent-journey-orphan-sweepgatetest/auto-fix-1776586424172gatetest/auto-fix-1776586534814gatetest/auto-fix-1776590685143gatetest/auto-fix-1776590808199mainops/redeploy-retriggerstyle/dxt-cta-themeworktree-agent-a3377aad30d55da26worktree-agent-a7ef607b7ee1d6c74
ai-changelog.test.ts4.5 KB · 134 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
/**
 * Block D7 — AI changelog route tests.
 *
 * The route depends on a real git repo on disk, so these tests drive the
 * route via the exported Hono app directly. A temporary bare repository is
 * spun up per test run under a unique GIT_REPOS_PATH so we don't collide
 * with other tests or the developer's local `./repos` tree.
 *
 * Tests here are deliberately scoped to the guarantees the Block D7 spec
 * calls out:
 *   - Module imports cleanly.
 *   - GET without query params renders the picker form (HTML).
 *   - GET with nonsense from/to strings renders an error banner, NOT a 500.
 *   - GET for a non-existent repo returns 404.
 */

import { describe, it, expect, beforeAll, afterAll } from "bun:test";
import { mkdtemp, rm, mkdir, writeFile } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";

// Pin GIT_REPOS_PATH BEFORE importing the route so `config.gitReposPath`
// (which is a lazy getter) reads our scratch dir on first access.
const SCRATCH = await mkdtemp(join(tmpdir(), "gluecron-ai-changelog-"));
process.env.GIT_REPOS_PATH = SCRATCH;

// eslint-disable-next-line import/first
const { default: aiChangelog } = await import("../routes/ai-changelog");

const OWNER = "alice";
const REPO = "demo";

async function run(cmd: string[], cwd: string) {
  const proc = Bun.spawn(cmd, { cwd, stdout: "pipe", stderr: "pipe" });
  await proc.exited;
}

async function seedRepo(): Promise<void> {
  // Bare repo lives where getRepoPath expects: <root>/<owner>/<repo>.git
  const bareDir = join(SCRATCH, OWNER, `${REPO}.git`);
  await mkdir(bareDir, { recursive: true });
  await run(["git", "init", "--bare", bareDir], SCRATCH);
  await run(
    ["git", "symbolic-ref", "HEAD", "refs/heads/main"],
    bareDir
  );

  // Push one real commit from a scratch working tree so branches/refs exist.
  const workDir = await mkdtemp(join(tmpdir(), "gluecron-ai-changelog-work-"));
  await run(["git", "init", workDir], SCRATCH);
  await run(
    ["git", "-C", workDir, "config", "user.email", "test@example.com"],
    SCRATCH
  );
  await run(
    ["git", "-C", workDir, "config", "user.name", "Test"],
    SCRATCH
  );
  await writeFile(join(workDir, "README.md"), "# hello\n");
  await run(["git", "-C", workDir, "add", "README.md"], SCRATCH);
  await run(
    ["git", "-C", workDir, "commit", "-m", "initial commit"],
    SCRATCH
  );
  // Ensure the local branch is called main regardless of git defaults.
  await run(
    ["git", "-C", workDir, "branch", "-M", "main"],
    SCRATCH
  );
  await run(
    ["git", "-C", workDir, "remote", "add", "origin", bareDir],
    SCRATCH
  );
  await run(
    ["git", "-C", workDir, "push", "origin", "main"],
    SCRATCH
  );

  await rm(workDir, { recursive: true, force: true });
}

describe("routes/ai-changelog — module", () => {
  it("imports cleanly and exports a Hono app", () => {
    expect(aiChangelog).toBeDefined();
    expect(typeof aiChangelog.request).toBe("function");
  });
});

describe("routes/ai-changelog — repo present", () => {
  beforeAll(async () => {
    await seedRepo();
  });

  afterAll(async () => {
    await rm(SCRATCH, { recursive: true, force: true }).catch(() => {});
  });

  it("GET without query params renders the picker form", async () => {
    const res = await aiChangelog.request(
      `/${OWNER}/${REPO}/ai/changelog`
    );
    expect(res.status).toBe(200);
    const body = await res.text();
    expect(body).toContain("AI Changelog");
    // The form exposes from/to inputs and a submit button.
    expect(body).toContain('name="from"');
    expect(body).toContain('name="to"');
    expect(body.toLowerCase()).toContain("generate");
  });

  it("GET with nonsense from/to renders an error banner, not a 500", async () => {
    const res = await aiChangelog.request(
      `/${OWNER}/${REPO}/ai/changelog?from=not-a-real-ref-xyz&to=also-not-real-abc`
    );
    // Must NOT be a 500 — the route should handle unresolvable refs gracefully.
    expect(res.status).toBe(200);
    const body = await res.text();
    // Error banner class used by the Layout for form errors.
    expect(body).toContain("auth-error");
    // And mentions the offending ref(s).
    expect(body).toMatch(/Could not resolve/i);
  });
});

describe("routes/ai-changelog — missing repo", () => {
  it("returns 404 for a non-existent repo", async () => {
    const res = await aiChangelog.request(
      "/nobody-xyz/nothing-xyz/ai/changelog"
    );
    expect(res.status).toBe(404);
    const body = await res.text();
    expect(body.toLowerCase()).toContain("repository not found");
  });
});