CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
enable-auto-merge.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.
| f764c07 | 1 | /** |
| 2 | * Block N1 — Tests for the auto-merge bootstrap + readiness scripts. | |
| 3 | * | |
| 4 | * The script exports a pure orchestrator (`runEnableAutoMerge`) that | |
| 5 | * takes a DB-shaped dependency and an `audit` callback. We feed it a | |
| 6 | * hand-rolled fake DB that records inserts / updates / selects so every | |
| 7 | * branch (insert vs update vs no-op) is observable without going near | |
| 8 | * Neon. | |
| 9 | * | |
| 10 | * No `mock.module()` here on purpose — the orchestrator was designed | |
| 11 | * with explicit DI so we don't have to poison the module graph for | |
| 12 | * downstream test files. | |
| 13 | */ | |
| 14 | ||
| 15 | import { describe, expect, test, beforeEach, afterAll } from "bun:test"; | |
| 16 | import { | |
| 17 | runEnableAutoMerge, | |
| 18 | renderDiff, | |
| 19 | resolveRepo, | |
| 20 | type DbLike, | |
| 21 | type EnableAutoMergeArgs, | |
| 22 | } from "../../scripts/enable-auto-merge"; | |
| 23 | import { | |
| 24 | checkAnthropicKey, | |
| 25 | checkAutopilotEnabled, | |
| 26 | checkAutoMergeSweepRegistered, | |
| 27 | checkMigration0040, | |
| 28 | } from "../../scripts/check-auto-merge-readiness"; | |
| 29 | import type { BranchProtection } from "../db/schema"; | |
| 30 | ||
| 31 | // --------------------------------------------------------------------------- | |
| 32 | // Fake DB — narrowly scoped to the script's actual queries. | |
| 33 | // --------------------------------------------------------------------------- | |
| 34 | ||
| 35 | interface FakeState { | |
| 36 | users: Array<{ id: string; username: string }>; | |
| 37 | repositories: Array<{ | |
| 38 | id: string; | |
| 39 | ownerId: string; | |
| 40 | name: string; | |
| 41 | defaultBranch: string; | |
| 42 | }>; | |
| 43 | branchProtection: BranchProtection[]; | |
| 44 | inserts: Array<{ table: string; values: any }>; | |
| 45 | updates: Array<{ table: string; set: any }>; | |
| 46 | } | |
| 47 | ||
| 48 | function freshState(): FakeState { | |
| 49 | return { | |
| 50 | users: [], | |
| 51 | repositories: [], | |
| 52 | branchProtection: [], | |
| 53 | inserts: [], | |
| 54 | updates: [], | |
| 55 | }; | |
| 56 | } | |
| 57 | ||
| 58 | /** | |
| 59 | * Inspect a Drizzle pgTable proxy to identify it by a unique-shape | |
| 60 | * column. We mirror the K1 approach: peek at well-known columns rather | |
| 61 | * than importing the schema-internal Symbol. | |
| 62 | */ | |
| 63 | function tableName(t: any): string { | |
| 64 | if (!t || typeof t !== "object") return "?"; | |
| 65 | if ("isPrivate" in t && "defaultBranch" in t) return "repositories"; | |
| 66 | if ("username" in t && "passwordHash" in t) return "users"; | |
| 67 | if ("pattern" in t && "enableAutoMerge" in t) return "branch_protection"; | |
| 68 | if ("action" in t && "userId" in t && "targetType" in t) return "audit_log"; | |
| 69 | return "?"; | |
| 70 | } | |
| 71 | ||
| 72 | /** | |
| 73 | * Build a where-predicate from a Drizzle SQL expression. We can't | |
| 74 | * introspect Drizzle's AST cheaply, so the fake instead records the | |
| 75 | * "select context" (which table is being queried) and the test sets | |
| 76 | * `_filter` when needed. For this script's queries we only need one | |
| 77 | * filter per call so we infer it from the most recent select. | |
| 78 | */ | |
| 79 | function makeDb(state: FakeState): DbLike { | |
| 80 | let _selectTable: string = "?"; | |
| 81 | let _selectCols: any = null; | |
| 82 | let _selectFilter: any = null; | |
| 83 | ||
| 84 | // Drizzle's `eq(col, val)` returns a plain object — we don't need to | |
| 85 | // unwrap it. Instead we capture the *operand* values via wrapped | |
| 86 | // helpers that the script itself uses through drizzle-orm. | |
| 87 | // For our purposes the filter just needs to be applied in `.limit()` | |
| 88 | // or `.where().limit()`, so we approximate: each table only has one | |
| 89 | // representative row that the test wants matched. We match by the | |
| 90 | // first call argument shape. | |
| 91 | ||
| 92 | const selectChain: any = { | |
| 93 | from: (t: any) => { | |
| 94 | _selectTable = tableName(t); | |
| 95 | return selectChain; | |
| 96 | }, | |
| 97 | where: (cond: any) => { | |
| 98 | _selectFilter = cond; | |
| 99 | return selectChain; | |
| 100 | }, | |
| 101 | limit: async () => { | |
| 102 | return resolveSelect(_selectTable, _selectFilter, _selectCols, state); | |
| 103 | }, | |
| 104 | }; | |
| 105 | ||
| 106 | return { | |
| 107 | select: (cols?: any) => { | |
| 108 | _selectCols = cols; | |
| 109 | _selectTable = "?"; | |
| 110 | _selectFilter = null; | |
| 111 | return selectChain; | |
| 112 | }, | |
| 113 | insert: (t: any) => { | |
| 114 | const name = tableName(t); | |
| 115 | return { | |
| 116 | values: (vals: any) => { | |
| 117 | state.inserts.push({ table: name, values: vals }); | |
| 118 | if (name === "branch_protection") { | |
| 119 | // Synthesize a BranchProtection row. | |
| 120 | const row: BranchProtection = { | |
| 121 | id: `bp-${state.branchProtection.length + 1}`, | |
| 122 | repositoryId: vals.repositoryId, | |
| 123 | pattern: vals.pattern, | |
| 124 | requirePullRequest: vals.requirePullRequest ?? true, | |
| 125 | requireGreenGates: vals.requireGreenGates ?? true, | |
| 126 | requireAiApproval: vals.requireAiApproval ?? true, | |
| 127 | requireHumanReview: vals.requireHumanReview ?? false, | |
| 128 | requiredApprovals: vals.requiredApprovals ?? 0, | |
| 129 | allowForcePush: vals.allowForcePush ?? false, | |
| 130 | allowDeletion: vals.allowDeletion ?? false, | |
| 131 | dismissStaleReviews: vals.dismissStaleReviews ?? false, | |
| 132 | enableAutoMerge: vals.enableAutoMerge ?? false, | |
| 133 | createdAt: new Date(), | |
| 134 | updatedAt: new Date(), | |
| 135 | }; | |
| 136 | state.branchProtection.push(row); | |
| 137 | return { | |
| 138 | returning: async () => [row], | |
| 139 | }; | |
| 140 | } | |
| 141 | return { | |
| 142 | returning: async () => [vals], | |
| 143 | }; | |
| 144 | }, | |
| 145 | }; | |
| 146 | }, | |
| 147 | update: (t: any) => { | |
| 148 | const name = tableName(t); | |
| 149 | return { | |
| 150 | set: (vals: any) => { | |
| 151 | state.updates.push({ table: name, set: vals }); | |
| 152 | return { | |
| 153 | where: async () => { | |
| 154 | if (name === "branch_protection") { | |
| 155 | // Apply set values to the single bp row (script only | |
| 156 | // updates by id, and tests only have one bp row at a time). | |
| 157 | for (const r of state.branchProtection) { | |
| 158 | Object.assign(r, vals); | |
| 159 | } | |
| 160 | } | |
| 161 | return undefined; | |
| 162 | }, | |
| 163 | }; | |
| 164 | }, | |
| 165 | }; | |
| 166 | }, | |
| 167 | }; | |
| 168 | } | |
| 169 | ||
| 170 | function resolveSelect( | |
| 171 | table: string, | |
| 172 | _filter: any, | |
| 173 | _cols: any, | |
| 174 | state: FakeState | |
| 175 | ): any[] { | |
| 176 | // Approximation: return the (single) configured row for the table. | |
| 177 | // resolveRepo issues two selects in sequence — first against `users`, | |
| 178 | // then `repositories` — and the script only seeds one of each in | |
| 179 | // these tests, so returning the first row is sufficient. | |
| 180 | if (table === "users") { | |
| 181 | return state.users.length > 0 ? [state.users[0]] : []; | |
| 182 | } | |
| 183 | if (table === "repositories") { | |
| 184 | return state.repositories.length > 0 | |
| 185 | ? [{ | |
| 186 | id: state.repositories[0]!.id, | |
| 187 | ownerId: state.repositories[0]!.ownerId, | |
| 188 | name: state.repositories[0]!.name, | |
| 189 | defaultBranch: state.repositories[0]!.defaultBranch, | |
| 190 | }] | |
| 191 | : []; | |
| 192 | } | |
| 193 | if (table === "branch_protection") { | |
| 194 | return state.branchProtection.length > 0 | |
| 195 | ? [state.branchProtection[0]] | |
| 196 | : []; | |
| 197 | } | |
| 198 | return []; | |
| 199 | } | |
| 200 | ||
| 201 | // --------------------------------------------------------------------------- | |
| 202 | // Audit fake | |
| 203 | // --------------------------------------------------------------------------- | |
| 204 | ||
| 205 | type AuditCall = { | |
| 206 | userId?: string | null; | |
| 207 | repositoryId?: string | null; | |
| 208 | action: string; | |
| 209 | targetType?: string; | |
| 210 | targetId?: string; | |
| 211 | metadata?: Record<string, unknown>; | |
| 212 | }; | |
| 213 | ||
| 214 | function makeAudit(sink: AuditCall[]) { | |
| 215 | return async (opts: AuditCall) => { | |
| 216 | sink.push(opts); | |
| 217 | }; | |
| 218 | } | |
| 219 | ||
| 220 | // --------------------------------------------------------------------------- | |
| 221 | // Fixtures | |
| 222 | // --------------------------------------------------------------------------- | |
| 223 | ||
| 224 | let state: FakeState; | |
| 225 | let audits: AuditCall[]; | |
| 226 | let db: DbLike; | |
| 227 | ||
| 228 | function seedRepo(opts?: { withProtection?: Partial<BranchProtection> }) { | |
| 229 | state.users.push({ id: "user-1", username: "ccantynz" }); | |
| 230 | state.repositories.push({ | |
| 231 | id: "repo-1", | |
| 232 | ownerId: "user-1", | |
| 233 | name: "Gluecron.com", | |
| 234 | defaultBranch: "main", | |
| 235 | }); | |
| 236 | if (opts?.withProtection) { | |
| 237 | state.branchProtection.push({ | |
| 238 | id: "bp-existing", | |
| 239 | repositoryId: "repo-1", | |
| 240 | pattern: "main", | |
| 241 | requirePullRequest: true, | |
| 242 | requireGreenGates: true, | |
| 243 | requireAiApproval: true, | |
| 244 | requireHumanReview: false, | |
| 245 | requiredApprovals: 0, | |
| 246 | allowForcePush: false, | |
| 247 | allowDeletion: false, | |
| 248 | dismissStaleReviews: false, | |
| 249 | enableAutoMerge: false, | |
| 250 | createdAt: new Date("2026-01-01"), | |
| 251 | updatedAt: new Date("2026-01-01"), | |
| 252 | ...opts.withProtection, | |
| 253 | } as BranchProtection); | |
| 254 | } | |
| 255 | } | |
| 256 | ||
| 257 | beforeEach(() => { | |
| 258 | state = freshState(); | |
| 259 | audits = []; | |
| 260 | db = makeDb(state); | |
| 261 | }); | |
| 262 | ||
| 263 | afterAll(() => { | |
| 264 | // No module-level mocks installed — nothing to undo. Reset state for | |
| 265 | // hygiene anyway. | |
| 266 | state = freshState(); | |
| 267 | audits = []; | |
| 268 | }); | |
| 269 | ||
| 270 | // --------------------------------------------------------------------------- | |
| 271 | // resolveRepo | |
| 272 | // --------------------------------------------------------------------------- | |
| 273 | ||
| 274 | describe("resolveRepo", () => { | |
| 275 | test("returns the repo when owner + name match", async () => { | |
| 276 | seedRepo(); | |
| 277 | const r = await resolveRepo(db, "ccantynz/Gluecron.com"); | |
| 278 | expect(r).not.toBeNull(); | |
| 279 | expect(r?.id).toBe("repo-1"); | |
| 280 | }); | |
| 281 | ||
| 282 | test("returns null when the owner doesn't exist", async () => { | |
| 283 | // No seeded users | |
| 284 | const r = await resolveRepo(db, "ghost/repo"); | |
| 285 | expect(r).toBeNull(); | |
| 286 | }); | |
| 287 | ||
| 288 | test("rejects malformed owner/name", async () => { | |
| 289 | seedRepo(); | |
| 290 | expect(await resolveRepo(db, "no-slash")).toBeNull(); | |
| 291 | expect(await resolveRepo(db, "/")).toBeNull(); | |
| 292 | expect(await resolveRepo(db, "owner/")).toBeNull(); | |
| 293 | }); | |
| 294 | }); | |
| 295 | ||
| 296 | // --------------------------------------------------------------------------- | |
| 297 | // runEnableAutoMerge — INSERT path | |
| 298 | // --------------------------------------------------------------------------- | |
| 299 | ||
| 300 | describe("runEnableAutoMerge — fresh insert", () => { | |
| 301 | test("inserts a new branch_protection row with the safety defaults", async () => { | |
| 302 | seedRepo(); // no existing protection | |
| 303 | const args: EnableAutoMergeArgs = { | |
| 304 | ownerSlash: "ccantynz/Gluecron.com", | |
| 305 | pattern: "main", | |
| 306 | }; | |
| 307 | const result = await runEnableAutoMerge(db, args, makeAudit(audits)); | |
| 308 | ||
| 309 | expect(result.action).toBe("inserted"); | |
| 310 | expect(result.before).toBeNull(); | |
| 311 | expect(result.after.enableAutoMerge).toBe(true); | |
| 312 | ||
| 313 | // Safety defaults present on insert. | |
| 314 | const ins = state.inserts.find((i) => i.table === "branch_protection"); | |
| 315 | expect(ins).toBeTruthy(); | |
| 316 | expect(ins!.values.requireGreenGates).toBe(true); | |
| 317 | expect(ins!.values.requireAiApproval).toBe(true); | |
| 318 | expect(ins!.values.requireHumanReview).toBe(false); | |
| 319 | expect(ins!.values.requiredApprovals).toBe(0); | |
| 320 | expect(ins!.values.enableAutoMerge).toBe(true); | |
| 321 | expect(ins!.values.dismissStaleReviews).toBe(false); | |
| 322 | expect(ins!.values.allowForcePush).toBe(false); | |
| 323 | expect(ins!.values.allowDeletion).toBe(false); | |
| 324 | ||
| 325 | // Audit row written. | |
| 326 | expect(audits.length).toBe(1); | |
| 327 | expect(audits[0]!.action).toBe("auto_merge.enabled_on_main"); | |
| 328 | expect(audits[0]!.repositoryId).toBe("repo-1"); | |
| 329 | expect(audits[0]!.targetType).toBe("branch_protection"); | |
| 330 | }); | |
| 331 | ||
| 332 | test("throws a clear error when the repo isn't found", async () => { | |
| 333 | // No seed — empty DB. | |
| 334 | let caught: Error | null = null; | |
| 335 | try { | |
| 336 | await runEnableAutoMerge( | |
| 337 | db, | |
| 338 | { ownerSlash: "ghost/nope", pattern: "main" }, | |
| 339 | makeAudit(audits) | |
| 340 | ); | |
| 341 | } catch (err) { | |
| 342 | caught = err as Error; | |
| 343 | } | |
| 344 | expect(caught).not.toBeNull(); | |
| 345 | expect(caught?.message).toMatch(/Repository not found/); | |
| 346 | // No audit entry written for a failed resolve. | |
| 347 | expect(audits.length).toBe(0); | |
| 348 | }); | |
| 349 | }); | |
| 350 | ||
| 351 | // --------------------------------------------------------------------------- | |
| 352 | // runEnableAutoMerge — UPDATE path | |
| 353 | // --------------------------------------------------------------------------- | |
| 354 | ||
| 355 | describe("runEnableAutoMerge — existing row", () => { | |
| 356 | test("flips enableAutoMerge=true on an existing row, preserves other fields", async () => { | |
| 357 | seedRepo({ | |
| 358 | withProtection: { | |
| 359 | enableAutoMerge: false, | |
| 360 | requireGreenGates: true, | |
| 361 | requireAiApproval: true, | |
| 362 | requireHumanReview: true, // not what the defaults would set | |
| 363 | requiredApprovals: 2, | |
| 364 | }, | |
| 365 | }); | |
| 366 | const result = await runEnableAutoMerge( | |
| 367 | db, | |
| 368 | { ownerSlash: "ccantynz/Gluecron.com", pattern: "main" }, | |
| 369 | makeAudit(audits) | |
| 370 | ); | |
| 371 | ||
| 372 | expect(result.action).toBe("updated"); | |
| 373 | expect(result.before).not.toBeNull(); | |
| 374 | expect(result.before!.enableAutoMerge).toBe(false); | |
| 375 | expect(result.after.enableAutoMerge).toBe(true); | |
| 376 | ||
| 377 | // Other fields preserved on the after row. | |
| 378 | expect(result.after.requireHumanReview).toBe(true); | |
| 379 | expect(result.after.requiredApprovals).toBe(2); | |
| 380 | ||
| 381 | // Only enableAutoMerge + updatedAt should land in the SET payload. | |
| 382 | const upd = state.updates.find((u) => u.table === "branch_protection"); | |
| 383 | expect(upd).toBeTruthy(); | |
| 384 | expect(upd!.set.enableAutoMerge).toBe(true); | |
| 385 | expect("requireHumanReview" in upd!.set).toBe(false); | |
| 386 | expect("requiredApprovals" in upd!.set).toBe(false); | |
| 387 | ||
| 388 | expect(audits.length).toBe(1); | |
| 389 | expect(audits[0]!.action).toBe("auto_merge.enabled_on_main"); | |
| 390 | }); | |
| 391 | ||
| 392 | test("--off flips the bit to false and writes the disable audit action", async () => { | |
| 393 | seedRepo({ withProtection: { enableAutoMerge: true } }); | |
| 394 | const result = await runEnableAutoMerge( | |
| 395 | db, | |
| 396 | { ownerSlash: "ccantynz/Gluecron.com", pattern: "main", off: true }, | |
| 397 | makeAudit(audits) | |
| 398 | ); | |
| 399 | ||
| 400 | expect(result.action).toBe("updated"); | |
| 401 | expect(result.after.enableAutoMerge).toBe(false); | |
| 402 | expect(audits[0]!.action).toBe("auto_merge.disabled_on_main"); | |
| 403 | }); | |
| 404 | ||
| 405 | test("idempotent: second run is a no-op with no second audit entry", async () => { | |
| 406 | seedRepo({ withProtection: { enableAutoMerge: true } }); | |
| 407 | const result = await runEnableAutoMerge( | |
| 408 | db, | |
| 409 | { ownerSlash: "ccantynz/Gluecron.com", pattern: "main" }, | |
| 410 | makeAudit(audits) | |
| 411 | ); | |
| 412 | expect(result.action).toBe("noop"); | |
| 413 | expect(result.auditWritten).toBe(false); | |
| 414 | expect(audits.length).toBe(0); | |
| 415 | // And no UPDATE was issued. | |
| 416 | expect(state.updates.length).toBe(0); | |
| 417 | }); | |
| 418 | }); | |
| 419 | ||
| 420 | // --------------------------------------------------------------------------- | |
| 421 | // renderDiff | |
| 422 | // --------------------------------------------------------------------------- | |
| 423 | ||
| 424 | describe("renderDiff", () => { | |
| 425 | test("INSERT case renders every tracked field as additions", () => { | |
| 426 | const after: BranchProtection = { | |
| 427 | id: "bp-1", | |
| 428 | repositoryId: "repo-1", | |
| 429 | pattern: "main", | |
| 430 | requirePullRequest: true, | |
| 431 | requireGreenGates: true, | |
| 432 | requireAiApproval: true, | |
| 433 | requireHumanReview: false, | |
| 434 | requiredApprovals: 0, | |
| 435 | allowForcePush: false, | |
| 436 | allowDeletion: false, | |
| 437 | dismissStaleReviews: false, | |
| 438 | enableAutoMerge: true, | |
| 439 | createdAt: new Date(), | |
| 440 | updatedAt: new Date(), | |
| 441 | } as BranchProtection; | |
| 442 | const out = renderDiff(null, after); | |
| 443 | expect(out).toContain("no previous"); | |
| 444 | expect(out).toContain("enableAutoMerge = true"); | |
| 445 | expect(out).toContain("requireAiApproval = true"); | |
| 446 | }); | |
| 447 | ||
| 448 | test("UPDATE case renders only the changed fields", () => { | |
| 449 | const before: BranchProtection = { | |
| 450 | id: "bp-1", | |
| 451 | repositoryId: "repo-1", | |
| 452 | pattern: "main", | |
| 453 | requirePullRequest: true, | |
| 454 | requireGreenGates: true, | |
| 455 | requireAiApproval: true, | |
| 456 | requireHumanReview: false, | |
| 457 | requiredApprovals: 0, | |
| 458 | allowForcePush: false, | |
| 459 | allowDeletion: false, | |
| 460 | dismissStaleReviews: false, | |
| 461 | enableAutoMerge: false, | |
| 462 | createdAt: new Date(), | |
| 463 | updatedAt: new Date(), | |
| 464 | } as BranchProtection; | |
| 465 | const after = { ...before, enableAutoMerge: true } as BranchProtection; | |
| 466 | const out = renderDiff(before, after); | |
| 467 | expect(out).toContain("enableAutoMerge"); | |
| 468 | expect(out).toContain("false"); | |
| 469 | expect(out).toContain("true"); | |
| 470 | // No other field should appear. | |
| 471 | expect(out).not.toContain("requireAiApproval:"); | |
| 472 | expect(out).not.toContain("requireGreenGates:"); | |
| 473 | }); | |
| 474 | }); | |
| 475 | ||
| 476 | // --------------------------------------------------------------------------- | |
| 477 | // Readiness checks | |
| 478 | // --------------------------------------------------------------------------- | |
| 479 | ||
| 480 | describe("readiness check helpers", () => { | |
| 481 | test("checkAnthropicKey fails when env var is missing", () => { | |
| 482 | const r = checkAnthropicKey({} as NodeJS.ProcessEnv); | |
| 483 | expect(r.status).toBe("fail"); | |
| 484 | expect(r.reason).toMatch(/ANTHROPIC_API_KEY/); | |
| 485 | }); | |
| 486 | ||
| 487 | test("checkAnthropicKey passes when env var is present", () => { | |
| 488 | const r = checkAnthropicKey({ | |
| 489 | ANTHROPIC_API_KEY: "sk-ant-1234567890", | |
| 490 | } as unknown as NodeJS.ProcessEnv); | |
| 491 | expect(r.status).toBe("pass"); | |
| 492 | }); | |
| 493 | ||
| 494 | test("checkAutopilotEnabled fails when AUTOPILOT_DISABLED=1", () => { | |
| 495 | const r = checkAutopilotEnabled({ | |
| 496 | AUTOPILOT_DISABLED: "1", | |
| 497 | } as unknown as NodeJS.ProcessEnv); | |
| 498 | expect(r.status).toBe("fail"); | |
| 499 | }); | |
| 500 | ||
| 501 | test("checkAutopilotEnabled passes when AUTOPILOT_DISABLED is unset", () => { | |
| 502 | const r = checkAutopilotEnabled({} as NodeJS.ProcessEnv); | |
| 503 | expect(r.status).toBe("pass"); | |
| 504 | }); | |
| 505 | ||
| 506 | test("checkAutoMergeSweepRegistered passes when the task is present", () => { | |
| 507 | const r = checkAutoMergeSweepRegistered([ | |
| 508 | { name: "mirror-sync" }, | |
| 509 | { name: "auto-merge-sweep" }, | |
| 510 | { name: "weekly-digest" }, | |
| 511 | ]); | |
| 512 | expect(r.status).toBe("pass"); | |
| 513 | }); | |
| 514 | ||
| 515 | test("checkAutoMergeSweepRegistered fails when missing", () => { | |
| 516 | const r = checkAutoMergeSweepRegistered([{ name: "mirror-sync" }]); | |
| 517 | expect(r.status).toBe("fail"); | |
| 518 | expect(r.reason).toMatch(/auto-merge-sweep/); | |
| 519 | }); | |
| 520 | ||
| 521 | test("checkMigration0040 fails when column is missing", async () => { | |
| 522 | const r = await checkMigration0040(async () => ({ exists: false })); | |
| 523 | expect(r.status).toBe("fail"); | |
| 524 | expect(r.reason).toMatch(/enable_auto_merge/); | |
| 525 | }); | |
| 526 | ||
| 527 | test("checkMigration0040 fails on runner error", async () => { | |
| 528 | const r = await checkMigration0040(async () => ({ | |
| 529 | exists: false, | |
| 530 | error: "connection refused", | |
| 531 | })); | |
| 532 | expect(r.status).toBe("fail"); | |
| 533 | expect(r.reason).toMatch(/connection refused/); | |
| 534 | }); | |
| 535 | ||
| 536 | test("checkMigration0040 passes when column exists", async () => { | |
| 537 | const r = await checkMigration0040(async () => ({ exists: true })); | |
| 538 | expect(r.status).toBe("pass"); | |
| 539 | }); | |
| 540 | }); | |
| 541 | ||
| 542 | // --------------------------------------------------------------------------- | |
| 543 | // Confirm the real defaultTasks() registers auto-merge-sweep. | |
| 544 | // --------------------------------------------------------------------------- | |
| 545 | ||
| 546 | describe("real defaultTasks() registration", () => { | |
| 547 | test("registers an 'auto-merge-sweep' task (guards against accidental removal)", async () => { | |
| 548 | const { defaultTasks } = await import("../lib/autopilot"); | |
| 549 | const tasks = defaultTasks(); | |
| 550 | expect(tasks.some((t) => t.name === "auto-merge-sweep")).toBe(true); | |
| 551 | }); | |
| 552 | }); |