Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
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-sweepfix/audit-sweep-2026-07-26gatetest/auto-fix-1776586424172gatetest/auto-fix-1776586534814gatetest/auto-fix-1776590685143gatetest/auto-fix-1776590808199mainops/redeploy-retriggerstyle/dxt-cta-themeworktree-agent-a3377aad30d55da26worktree-agent-a7ef607b7ee1d6c74
schema-single-source.test.ts6.3 KB · 153 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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/**
 * One Drizzle definition per physical table.
 *
 * `organizations` was declared TWICE — in schema.ts (slug + created_by_id
 * NOT NULL, matching migration 0003 and therefore the real database) and in
 * schema-extensions.ts (display_name/website/location/is_verified, neither
 * NOT NULL column). Both mapped to the same physical table with irreconcilable
 * shapes.
 *
 * drizzle.config.ts includes only schema.ts and deploys run `db:migrate`,
 * never `db:push`, so the schema-extensions version was never migrated: it
 * described a table that has never existed. routes/orgs.tsx imported it, so
 * POST /orgs/new wrote columns Postgres does not have while omitting two that
 * are NOT NULL. Organization creation could never have succeeded, and because
 * each file typechecked against its own definition, tsc could not see it.
 *
 * The duplicate-table check below is the one that matters: it fails the build
 * for ANY table declared in more than one place, so this class cannot recur.
 */

import { describe, expect, it } from "bun:test";
import { readFileSync, readdirSync } from "fs";
import { join } from "path";

const DB_DIR = "src/db";

/**
 * Strip comments before scanning. Without this the check matches its own
 * documentation: these files explain the duplicate-table bug in prose that
 * necessarily contains `pgTable("organizations"`, and a naive scan then
 * reports the very thing the comment says was fixed.
 */
function code(src: string): string {
  return src
    .replace(/\/\*[\s\S]*?\*\//g, "")
    .replace(/^\s*\/\/.*$/gm, "");
}

function schemaFiles(): string[] {
  return readdirSync(DB_DIR)
    .filter((f) => f.endsWith(".ts") && f.includes("schema"))
    .map((f) => join(DB_DIR, f));
}

describe("no physical table is declared twice", () => {
  it("every pgTable name is unique across all schema files", () => {
    const seen = new Map<string, string[]>();
    for (const f of schemaFiles()) {
      const src = code(readFileSync(f, "utf8"));
      for (const m of src.matchAll(/pgTable\(\s*["'`]([^"'`]+)["'`]/g)) {
        const table = m[1];
        if (!seen.has(table)) seen.set(table, []);
        seen.get(table)!.push(f.replace(/\\/g, "/"));
      }
    }
    const dupes = [...seen.entries()]
      .filter(([, files]) => files.length > 1)
      .map(([t, files]) => `${t} declared in ${files.join(" AND ")}`);
    expect(dupes).toEqual([]);
  });

  it("schema-extensions re-exports the org/team tables rather than redeclaring them", () => {
    const src = code(readFileSync("src/db/schema-extensions.ts", "utf8"));
    // Matched loosely on purpose: asserting an exact import string breaks the
    // moment the list wraps across lines, which says nothing about the bug.
    expect(src).toMatch(/from\s+["']\.\/schema["']/);
    for (const t of ["organizations", "teams", "team_members", "team_repos", "org_members", "branch_protection"]) {
      expect(src).not.toMatch(new RegExp(`pgTable\\(\\s*["'\`]${t}["'\`]`));
    }
  });
});

describe("the organizations table matches its migration", () => {
  const schema = readFileSync("src/db/schema.ts", "utf8");
  const table = schema.slice(
    schema.indexOf('export const organizations = pgTable("organizations"'),
    schema.indexOf('export const orgMembers')
  );

  it("keeps the NOT NULL columns migration 0003 created", () => {
    expect(table).toContain('slug: text("slug").notNull()');
    expect(table).toContain('createdById: uuid("created_by_id")');
    expect(table).toContain(".notNull()");
  });

  it("carries the columns migration 0113 added", () => {
    // Added rather than dropped, so the create form's display name and
    // website are not silently discarded.
    for (const col of ["display_name", "website", "location", "is_verified"]) {
      expect(table).toContain(col);
    }
  });

  it("has a migration that adds those columns", () => {
    const sql = readFileSync("drizzle/0113_orgs_reconcile.sql", "utf8");
    for (const col of ["display_name", "website", "location", "is_verified"]) {
      expect(sql).toContain(`ADD COLUMN IF NOT EXISTS "${col}"`);
    }
    // Additive and idempotent — safe against a populated table and a re-run.
    expect(sql).not.toMatch(/DROP COLUMN|DROP TABLE/i);
  });
});

describe("POST /orgs/new supplies every required column", () => {
  const src = readFileSync("src/routes/orgs.tsx", "utf8");
  const insert = src.slice(
    src.indexOf(".insert(organizations)"),
    src.indexOf(".insert(organizations)") + 700
  );

  it("provides slug and createdById", () => {
    expect(insert).toContain("slug:");
    expect(insert).toContain("createdById:");
  });

  it("derives the slug from the validated name", () => {
    // `name` is already constrained to /^[a-zA-Z0-9._-]+$/ and collision-
    // checked against both users and orgs, so lowercasing it is a safe slug.
    expect(insert).toContain("slug: name.toLowerCase()");
  });
});

describe("teams feature matches the database", () => {
  const schema = readFileSync("src/db/schema.ts", "utf8");

  it("teams carries the permission column the UI renders", () => {
    // orgs.tsx renders `team.permission` and the create form collects it, but
    // the column existed only in the rival definition and was never migrated.
    // Anchored with a regex, not a literal newline — these files are CRLF.
    const t = schema.slice(schema.search(/pgTable\(\s*"teams"/), schema.indexOf("export const teamMembers"));
    expect(t).toContain('permission: text("permission")');
  });

  it("team_repos is declared in schema.ts so it gets a migration", () => {
    // It previously lived ONLY in schema-extensions, which drizzle.config.ts
    // does not include — so the table never existed, while orgs.tsx selected
    // and joined it to render a team's repo list.
    expect(schema).toMatch(/pgTable\(\s*"team_repos"/);
  });

  it("migration 0114 creates team_repos and adds teams.permission", () => {
    const sql = readFileSync("drizzle/0114_teams_reconcile.sql", "utf8");
    expect(sql).toContain('CREATE TABLE IF NOT EXISTS "team_repos"');
    expect(sql).toContain('ALTER TABLE "teams" ADD COLUMN IF NOT EXISTS "permission"');
    expect(sql).not.toMatch(/DROP COLUMN|DROP TABLE/i);
  });

  it("team creation supplies the NOT NULL slug", () => {
    const orgs = readFileSync("src/routes/orgs.tsx", "utf8");
    const insert = orgs.slice(orgs.indexOf(".insert(teams)"), orgs.indexOf(".insert(teams)") + 400);
    expect(insert).toContain("slug,");
  });
});