CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
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 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 | /**
* Tests for src/lib/push-workflow-sync.ts — the fix for the missing
* "push actually triggers CI" wiring (2026-07-15). Before this, nothing
* synced `.gluecron/workflows/*.yml` into the `workflows` table on push,
* so `on: push` triggers never fired automatically — only manual dispatch,
* PR slash commands, and scheduled (cron) runs worked.
*
* git tree/blob reads and enqueueRun are passed in via the injectable
* `deps` param rather than mock.module()'d — those two modules
* (../git/repository, ./workflow-runner) are imported by dozens of
* unrelated test files, and a top-level mock.module() on either leaks
* globally across the whole `bun test` run and breaks them (verified:
* an earlier version of this file did exactly that and broke 15 tests
* in git.test.ts/api-v2/web-routes). Only ../db is mocked, matching the
* established, already-safe pattern in push.test.ts / notify-activity-feed.test.ts.
*/
import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test";
const _realDb = await import("../db");
let _insertedRows: Array<{ id: string; disabled: boolean }> = [];
let _upsertCalls: any[] = [];
mock.module("../db", () => ({
db: {
insert: () => ({
values: (vals: any) => ({
onConflictDoUpdate: (args: any) => {
_upsertCalls.push({ vals, args });
const row = _insertedRows.shift();
return {
returning: async () => (row ? [row] : []),
};
},
}),
}),
},
}));
const { syncAndEnqueuePushWorkflows } = await import("../lib/push-workflow-sync");
afterAll(() => {
mock.module("../db", () => _realDb);
});
let _treeEntries: Array<{ mode: string; type: string; sha: string; size?: number; name: string }> = [];
let _blobs: Record<string, { content: string; size: number; isBinary: boolean }> = {};
let _enqueueCalls: any[] = [];
let _enqueueShouldThrow = false;
const fakeDeps = {
getTree: async () => _treeEntries,
getBlob: async (_owner: string, _repo: string, _ref: string, path: string) =>
_blobs[path] ?? null,
enqueueRun: async (opts: any) => {
if (_enqueueShouldThrow) throw new Error("enqueue boom");
_enqueueCalls.push(opts);
return "run-id-1";
},
} as any;
beforeEach(() => {
_treeEntries = [];
_blobs = {};
_insertedRows = [];
_upsertCalls = [];
_enqueueCalls = [];
_enqueueShouldThrow = false;
});
const CI_YML = `
name: CI
on: push
jobs:
build:
steps:
- run: echo hi
`;
const DISPATCH_ONLY_YML = `
name: Manual only
on: workflow_dispatch
jobs:
build:
steps:
- run: echo manual
`;
const BROKEN_YML = `
jobs:
- this is not valid: [
`;
function baseOpts(overrides: Partial<Parameters<typeof syncAndEnqueuePushWorkflows>[0]> = {}) {
return {
owner: "acme",
repo: "widgets",
repositoryId: "repo-1",
branch: "main",
commitSha: "a".repeat(40),
triggeredBy: "user-1",
...overrides,
};
}
describe("syncAndEnqueuePushWorkflows", () => {
it("returns zeros when .gluecron/workflows is empty/missing", async () => {
_treeEntries = [];
const result = await syncAndEnqueuePushWorkflows(baseOpts(), fakeDeps);
expect(result).toEqual({ synced: 0, enqueued: 0, errors: [] });
expect(_upsertCalls).toHaveLength(0);
expect(_enqueueCalls).toHaveLength(0);
});
it("syncs and enqueues a workflow with an on:push trigger", async () => {
_treeEntries = [{ mode: "100644", type: "blob", sha: "x", name: "ci.yml" }];
_blobs[".gluecron/workflows/ci.yml"] = { content: CI_YML, size: CI_YML.length, isBinary: false };
_insertedRows = [{ id: "wf-1", disabled: false }];
const result = await syncAndEnqueuePushWorkflows(baseOpts(), fakeDeps);
expect(result.synced).toBe(1);
expect(result.enqueued).toBe(1);
expect(result.errors).toEqual([]);
expect(_enqueueCalls[0]).toMatchObject({
workflowId: "wf-1",
repositoryId: "repo-1",
event: "push",
ref: "main",
commitSha: "a".repeat(40),
triggeredBy: "user-1",
});
// Upsert wrote the parsed shape + onEvents.
expect(_upsertCalls[0].vals.path).toBe(".gluecron/workflows/ci.yml");
expect(JSON.parse(_upsertCalls[0].vals.onEvents)).toEqual(["push"]);
});
it("syncs but does NOT enqueue a workflow with no push trigger", async () => {
_treeEntries = [{ mode: "100644", type: "blob", sha: "x", name: "manual.yml" }];
_blobs[".gluecron/workflows/manual.yml"] = {
content: DISPATCH_ONLY_YML,
size: DISPATCH_ONLY_YML.length,
isBinary: false,
};
_insertedRows = [{ id: "wf-2", disabled: false }];
const result = await syncAndEnqueuePushWorkflows(baseOpts(), fakeDeps);
expect(result.synced).toBe(1);
expect(result.enqueued).toBe(0);
expect(_enqueueCalls).toHaveLength(0);
});
it("syncs but does NOT enqueue a disabled workflow", async () => {
_treeEntries = [{ mode: "100644", type: "blob", sha: "x", name: "ci.yml" }];
_blobs[".gluecron/workflows/ci.yml"] = { content: CI_YML, size: CI_YML.length, isBinary: false };
_insertedRows = [{ id: "wf-3", disabled: true }];
const result = await syncAndEnqueuePushWorkflows(baseOpts(), fakeDeps);
expect(result.synced).toBe(1);
expect(result.enqueued).toBe(0);
expect(_enqueueCalls).toHaveLength(0);
});
it("records a parse error per-file without crashing or blocking other files", async () => {
_treeEntries = [
{ mode: "100644", type: "blob", sha: "x", name: "broken.yml" },
{ mode: "100644", type: "blob", sha: "y", name: "ci.yml" },
];
_blobs[".gluecron/workflows/broken.yml"] = { content: BROKEN_YML, size: 1, isBinary: false };
_blobs[".gluecron/workflows/ci.yml"] = { content: CI_YML, size: CI_YML.length, isBinary: false };
_insertedRows = [{ id: "wf-4", disabled: false }];
const result = await syncAndEnqueuePushWorkflows(baseOpts(), fakeDeps);
// The good file still gets synced + enqueued despite the broken one.
expect(result.synced).toBe(1);
expect(result.enqueued).toBe(1);
});
it("ignores non-yaml entries in the workflows directory", async () => {
_treeEntries = [{ mode: "100644", type: "blob", sha: "x", name: "README.md" }];
const result = await syncAndEnqueuePushWorkflows(baseOpts(), fakeDeps);
expect(result.synced).toBe(0);
expect(_upsertCalls).toHaveLength(0);
});
it("records an error but keeps synced count when enqueueRun throws", async () => {
_treeEntries = [{ mode: "100644", type: "blob", sha: "x", name: "ci.yml" }];
_blobs[".gluecron/workflows/ci.yml"] = { content: CI_YML, size: CI_YML.length, isBinary: false };
_insertedRows = [{ id: "wf-5", disabled: false }];
_enqueueShouldThrow = true;
const result = await syncAndEnqueuePushWorkflows(baseOpts(), fakeDeps);
expect(result.synced).toBe(1);
expect(result.enqueued).toBe(0);
expect(result.errors.length).toBe(1);
expect(result.errors[0]).toContain("enqueue");
});
});
|