Blame · Line-by-line history
intelligence.test.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 2c34075 | 1 | import { describe, it, expect, beforeAll, afterAll } from "bun:test"; |
| 2 | import { join } from "path"; | |
| 3 | import { rm, mkdir } from "fs/promises"; | |
| 4 | import { initBareRepo, getRepoPath } from "../git/repository"; | |
| 5 | import { | |
| 6 | computeHealthScore, | |
| 7 | detectCIConfig, | |
| 8 | analyzePush, | |
| 9 | } from "../lib/intelligence"; | |
| 10 | import { autoRepair } from "../lib/autorepair"; | |
| 11 | ||
| 12 | const TEST_REPOS = join(import.meta.dir, "../../.test-repos-intel-" + Date.now()); | |
| 13 | ||
| 14 | beforeAll(async () => { | |
| 15 | await rm(TEST_REPOS, { recursive: true, force: true }); | |
| 16 | await mkdir(TEST_REPOS, { recursive: true }); | |
| 17 | process.env.GIT_REPOS_PATH = TEST_REPOS; | |
| 18 | ||
| 19 | // Create repo with various files | |
| 20 | await initBareRepo("dev", "myapp"); | |
| 21 | const cloneDir = join(TEST_REPOS, "_clone"); | |
| 22 | const workDir = join(cloneDir, "work"); | |
| 23 | ||
| 779c681 | 24 | // Must throw on failure — see the note in week3.test.ts. A silently failed |
| 25 | // clone leaves `cwd` inside the real repository and the subsequent git | |
| 26 | // config/add/commit calls then mutate it. | |
| 2c34075 | 27 | const run = async (cmd: string[], cwd: string) => { |
| 28 | const proc = Bun.spawn(cmd, { cwd, stdout: "pipe", stderr: "pipe" }); | |
| 779c681 | 29 | const [exitCode, stderr] = await Promise.all([ |
| 30 | proc.exited, | |
| 31 | new Response(proc.stderr).text(), | |
| 32 | ]); | |
| 33 | if (exitCode !== 0) { | |
| 34 | throw new Error( | |
| 35 | `[intelligence fixture] ${cmd.join(" ")} failed in ${cwd} (exit ${exitCode}): ${stderr}` | |
| 36 | ); | |
| 37 | } | |
| 2c34075 | 38 | }; |
| 39 | ||
| 40 | await run(["git", "clone", getRepoPath("dev", "myapp"), workDir], TEST_REPOS); | |
| 41 | await run(["git", "config", "user.email", "dev@test.com"], workDir); | |
| 42 | await run(["git", "config", "user.name", "Dev"], workDir); | |
| 43 | ||
| 44 | await mkdir(join(workDir, "src"), { recursive: true }); | |
| 45 | await mkdir(join(workDir, "src/__tests__"), { recursive: true }); | |
| 46 | ||
| 47 | // package.json | |
| 48 | await Bun.write( | |
| 49 | join(workDir, "package.json"), | |
| 50 | JSON.stringify( | |
| 51 | { | |
| 52 | name: "myapp", | |
| 53 | version: "1.0.0", | |
| 54 | scripts: { test: "bun test", lint: "eslint .", build: "tsc" }, | |
| 55 | dependencies: { hono: "^4.0.0" }, | |
| 56 | devDependencies: { typescript: "^5.0.0", "@types/bun": "^1.0.0" }, | |
| 57 | }, | |
| 58 | null, | |
| 59 | 2 | |
| 60 | ) | |
| 61 | ); | |
| 62 | ||
| 63 | // tsconfig | |
| 64 | await Bun.write( | |
| 65 | join(workDir, "tsconfig.json"), | |
| 66 | JSON.stringify({ compilerOptions: { strict: true } }, null, 2) | |
| 67 | ); | |
| 68 | ||
| 69 | // README | |
| 70 | await Bun.write(join(workDir, "README.md"), "# My App\n\nA test project.\n"); | |
| 71 | ||
| 72 | // Source files | |
| 73 | await Bun.write( | |
| 74 | join(workDir, "src/index.ts"), | |
| 75 | 'const greeting = "hello world";\nconsole.log(greeting);\n' | |
| 76 | ); | |
| 77 | await Bun.write( | |
| 78 | join(workDir, "src/util.ts"), | |
| 79 | 'export function add(a: number, b: number): number {\n return a + b;\n}\n' | |
| 80 | ); | |
| 81 | ||
| 82 | // Test file | |
| 83 | await Bun.write( | |
| 84 | join(workDir, "src/__tests__/util.test.ts"), | |
| 85 | 'import { expect, test } from "bun:test";\nimport { add } from "../util";\n\ntest("add", () => {\n expect(add(1, 2)).toBe(3);\n});\n' | |
| 86 | ); | |
| 87 | ||
| 88 | // Trailing whitespace in a file (for auto-repair test) | |
| 89 | await Bun.write( | |
| 90 | join(workDir, "src/messy.ts"), | |
| 91 | 'const x = 1; \nconst y = 2;\t\t\n// no newline at end' | |
| 92 | ); | |
| 93 | ||
| 94 | // bun.lock (just a dummy) | |
| 95 | await Bun.write(join(workDir, "bun.lock"), "{}"); | |
| 96 | ||
| 97 | // .gitignore with some entries | |
| 98 | await Bun.write(join(workDir, ".gitignore"), "node_modules/\ndist/\n"); | |
| 99 | ||
| 100 | await run(["git", "add", "-A"], workDir); | |
| 101 | await run(["git", "commit", "-m", "Initial commit"], workDir); | |
| 102 | await run(["git", "branch", "-M", "main"], workDir); | |
| 103 | await run(["git", "push", "-u", "origin", "main"], workDir); | |
| 104 | ||
| 105 | await rm(cloneDir, { recursive: true, force: true }); | |
| 106 | }); | |
| 107 | ||
| 108 | afterAll(async () => { | |
| 109 | await rm(TEST_REPOS, { recursive: true, force: true }); | |
| 110 | }); | |
| 111 | ||
| 112 | describe("health score", () => { | |
| 113 | it("computes a health report", async () => { | |
| 114 | const report = await computeHealthScore("dev", "myapp"); | |
| 115 | ||
| 116 | expect(report.score).toBeGreaterThan(0); | |
| 117 | expect(report.score).toBeLessThanOrEqual(100); | |
| 118 | expect(["A+", "A", "B", "C", "D", "F"]).toContain(report.grade); | |
| 119 | expect(report.breakdown.security).toBeDefined(); | |
| 120 | expect(report.breakdown.testing).toBeDefined(); | |
| 121 | expect(report.breakdown.complexity).toBeDefined(); | |
| 122 | expect(report.breakdown.dependencies).toBeDefined(); | |
| 123 | expect(report.breakdown.documentation).toBeDefined(); | |
| 124 | expect(report.breakdown.activity).toBeDefined(); | |
| 125 | expect(report.insights.length).toBeGreaterThan(0); | |
| 126 | }); | |
| 127 | ||
| 128 | it("detects tests exist", async () => { | |
| 129 | const report = await computeHealthScore("dev", "myapp"); | |
| 130 | expect(report.breakdown.testing.hasTests).toBe(true); | |
| 131 | expect(report.breakdown.testing.testFileCount).toBeGreaterThan(0); | |
| 132 | }); | |
| 133 | ||
| 134 | it("detects README", async () => { | |
| 135 | const report = await computeHealthScore("dev", "myapp"); | |
| 136 | expect(report.breakdown.documentation.hasReadme).toBe(true); | |
| 137 | }); | |
| 138 | ||
| 139 | it("detects dependencies", async () => { | |
| 140 | const report = await computeHealthScore("dev", "myapp"); | |
| 141 | expect(report.breakdown.dependencies.total).toBeGreaterThan(0); | |
| 142 | expect(report.breakdown.dependencies.lockfileExists).toBe(true); | |
| 143 | }); | |
| 144 | ||
| 145 | it("has at least 1 contributor", async () => { | |
| 146 | const report = await computeHealthScore("dev", "myapp"); | |
| 147 | expect(report.breakdown.activity.uniqueContributors).toBeGreaterThanOrEqual(1); | |
| 148 | }); | |
| 149 | }); | |
| 150 | ||
| 151 | describe("zero-config CI detection", () => { | |
| 152 | it("detects Bun + TypeScript project", async () => { | |
| 153 | const ci = await detectCIConfig("dev", "myapp", "main"); | |
| 154 | ||
| 155 | expect(ci.projectType).toBe("typescript"); | |
| 156 | expect(ci.runtime).toBe("bun"); | |
| 157 | expect(ci.detected).toContain("Bun project detected"); | |
| 158 | expect(ci.detected).toContain("TypeScript detected"); | |
| 159 | }); | |
| 160 | ||
| 161 | it("detects test, lint, and build commands", async () => { | |
| 162 | const ci = await detectCIConfig("dev", "myapp", "main"); | |
| 163 | ||
| 164 | const names = ci.commands.map((c) => c.name); | |
| 165 | expect(names).toContain("Test"); | |
| 166 | expect(names).toContain("Lint"); | |
| 167 | }); | |
| 168 | ||
| 169 | it("detects Hono framework", async () => { | |
| 170 | const ci = await detectCIConfig("dev", "myapp", "main"); | |
| 171 | expect(ci.detected).toContain("Hono framework"); | |
| 172 | }); | |
| 173 | }); | |
| 174 | ||
| 175 | describe("auto-repair", () => { | |
| 176 | it("fixes whitespace issues", async () => { | |
| 177 | const result = await autoRepair("dev", "myapp", "main"); | |
| 178 | ||
| 179 | expect(result.repaired).toBe(true); | |
| 180 | expect(result.repairs.length).toBeGreaterThan(0); | |
| 181 | ||
| 182 | // Should have fixed trailing whitespace | |
| 183 | const whitespaceRepairs = result.repairs.filter( | |
| 184 | (r) => r.type === "whitespace" | |
| 185 | ); | |
| 186 | expect(whitespaceRepairs.length).toBeGreaterThan(0); | |
| 187 | }); | |
| 188 | ||
| 189 | it("adds missing .gitignore entries", async () => { | |
| 190 | const result = await autoRepair("dev", "myapp", "main"); | |
| 191 | ||
| 192 | const gitignoreRepairs = result.repairs.filter( | |
| 193 | (r) => r.type === "gitignore" | |
| 194 | ); | |
| 195 | // May or may not need repair depending on current state | |
| 196 | // (first run may have already fixed it) | |
| 197 | expect(result.repaired).toBeDefined(); | |
| 198 | }); | |
| 199 | ||
| 200 | it("subsequent runs have fewer repairs", async () => { | |
| 201 | // Run once to fix everything | |
| 202 | const first = await autoRepair("dev", "myapp", "main"); | |
| 203 | // Run again — should have fewer or no repairs | |
| 204 | const second = await autoRepair("dev", "myapp", "main"); | |
| 205 | expect(second.repairs.length).toBeLessThanOrEqual(first.repairs.length); | |
| 206 | }); | |
| 207 | }); | |
| 208 | ||
| 209 | describe("health dashboard route", () => { | |
| 210 | it("GET /:owner/:repo/health returns health page", async () => { | |
| 211 | const app = (await import("../app")).default; | |
| 212 | const res = await app.request("/dev/myapp/health"); | |
| 213 | expect(res.status).toBe(200); | |
| 214 | const html = await res.text(); | |
| 215 | expect(html).toContain("Health Score"); | |
| 216 | expect(html).toContain("Security"); | |
| 217 | expect(html).toContain("Testing"); | |
| 218 | expect(html).toContain("Zero-Config CI"); | |
| 219 | }); | |
| 220 | }); |