Ledger entry
Sequence 4632 · bugstack · action.executed
Cryptographic proof
- Hash
6ed425d2971578a9b8229af85d057e21ac74756dccefa62e627cc2984584409c- Previous hash
6bca22183c004b0e478f65de3be2a6aa690d9466f06d0a634a14c8ea931fc10b- Ed25519 signature
FjUZT2B4pjviknOPiiEiJuO+o9Gr9GIvx2O//H7cEEzCGhEQWrzs1IL3tgP018heHlU4LzxkmRH/W9dSm3sbBg==- Signing public key (SPKI, base64)
MCowBQYDK2VwAyEAxFhLvd6zMtQx3v6VarlyTyW+m8JUK8asT736IYWEsus=- Published at
- 2026-08-17T20:54:09.730Z
Verify independently (no BugStack software)
The signature is Ed25519 over the hash above (as UTF-8). Fetch the org's keys at /org/bugstack/key — or use the key shown on this page — and run:
node -e '
const { createPublicKey, verify } = require("crypto");
const key = createPublicKey({ key: Buffer.from("<signing public key>", "base64"), type: "spki", format: "der" });
console.log(verify(null, Buffer.from("<hash>", "utf8"), key, Buffer.from("<signature>", "base64")));
'Scrubbed payload
{
"kind": "action.executed",
"action": {
"id": "fa48f9c7-196d-4285-8537-6be6f48a33ca",
"type": "open_pull_request",
"orgId": "2e77b814-3c5d-4b48-965f-8e5475b92ba7",
"status": "executed",
"target": {
"repo": {
"branch": "main",
"repoFullName": "bugstackai/bugstack",
"repositoryId": "c15eb17a-bf5f-4ded-8476-c309a3c705e7"
},
"type": "open_pull_request",
"baseBranch": "main",
"headBranch": "bugstack/deploy-fix/b12f7893-1785861871329"
},
"payload": {
"body": "## 🤖 Deploy failure fix\n\nTest mock's zero-arg downloadSnapshot breaks mock.calls[0][0] typing\n\n### What failed\n- **Service**: bugstack-worker-tester (build_ended, deploy `dep-d9p1cjvlk1mc73a4aivg`)\n- **Commit range**: last green `532dbc540c` → failed `11b2b9ff17`\n- **Breaking commit**: `9aa4563bd3`\n\n### Root cause\nThe `github()` test helper in apps/workers/src/__tests__/cadence-scan.test.ts wraps `downloadSnapshot` in `vi.fn(async () => ...)` with no declared parameter, even though the real `downloadSnapshot` port takes an `{owner, repo, ref?, maxFileBytes?}` argument. Because the mocked function's inferred parameter list is empty, TypeScript infers `mock.calls[number]` as the empty tuple type `[]`, so `gh.downloadSnapshot.mock.calls[0]![0]` fails to typecheck (TS2493: tuple `[]` has no element at index 0), which cascades into `call` being typed as possibly `undefined` at every subsequent `call.xxx` access (TS18048) — exactly the 5 errors in the build log (lines 137-141). This is a test-authoring bug in code added by this branch, not an infra/config issue.\n\n### What changed\nGave the `github()` helper's `downloadSnapshot` mock an explicit (unused) parameter matching the real port signature (`{owner, repo, ref?, maxFileBytes?}`) so vi.fn infers a non-empty parameter tuple. This makes `gh.downloadSnapshot.mock.calls[0]` a 1-tuple, so indexing `[0]` type-checks and every subsequent `call.ref` / `call.maxFileBytes` / `call.owner` / `call.repo` access resolves to a defined type, clearing both the TS2493 and TS18048 errors without touching any other test or the module under test.\n\n<details><summary>Build/deploy log excerpt</summary>\n\n```\n#11 10.48 apps/dashboard postinstall: ✔ Generated Prisma Client (v5.22.0) to ./../../node_modules/.pnpm/@[email-redacted]@5.22.0/node_modules/@prisma/client in 126ms\n#11 10.13 apps/dashboard postinstall: Prisma schema loaded from ../../services/error-service/prisma/schema.prisma\n#11 8.196 apps/dashboard postinstall$ prisma generate\n#11 7.995 \n#11 7.995 + vitest 2.1.9\n#11 7.995 + typescript 5.9.3\n#11 7.995 + turbo 2.9.14\n#11 7.995 + @bugstack/cadence 0.0.0 <- packages/cadence\n#11 7.995 devDependencies:\n#11 7.995 \n#11 7.995 + pg 8.21.0\n#11 7.995 dependencies:\n#11 7.995 \n#11 7.387 Progress: resolved 840, reused 0, downloaded 837, added 840, done\n#11 7.241 Progress: resolved 840, reused 0, downloaded 836, added 839\n#11 6.241 Progress: resolved 840, reused 0, downloaded 832, added 834\n#11 5.535 \n#11 5.535 ╰───────────────────────────────────────────────╯\n#11 5.535 │ │\n#11 5.535 │ To update, run: corepack use [email-redacted].0 │\n#11 5.535 │ Changelog: https://pnpm.io/v/11.20.0 │\n#11 5.535 │ Update available! 10.29.3 → 11.20.0. │\n#11 5.535 │ │\n#11 5.535 ╭───────────────────────────────────────────────╮\n#11 5.535 \n#11 5.238 Progress: resolved 840, reused 0, downloaded 824, added 287\n#11 4.238 Progress: resolved 840, reused 0, downloaded 556, added 173\n#11 3.217 Progress: resolved 840, reused 0, downloaded 305, added 79\n#11 2.213 Progress: resolved 840, reused 0, downloaded 0, added 0\n#11 1.618 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n```\n\n</details>\n\n### Confidence: high\n\n---\n_Generated by BugStack's deploy-failure playbook. Review before merging — auto-merge is gated on CI + governance._",
"type": "open_pull_request",
"files": [
{
"path": "apps/workers/src/__tests__/cadence-scan.test.ts",
"content": "// apps/workers — the server-side cadence scan.\n//\n// The distinction these tests exist to protect: \"this repo is CLEAN\" and \"I\n// could not look at this repo\" must never collapse into the same outcome. The\n// whole org rides one member's OAuth token, so a repo that token cannot see is\n// invisible — and reporting invisible as clean is a false all-clear, the most\n// expensive kind of wrong answer a scanner can give.\n\nimport { describe, expect, it, vi } from \"vitest\";\n\nimport { scanOneRepo, type ScanTarget } from \"../cadence-scan\";\n\nconst TARGET: ScanTarget = {\n orgId: \"org-1\",\n repositoryId: \"repo-1\",\n repoFullName: \"acme/svc\",\n defaultBranch: \"main\",\n};\n\nconst OFFENDING = `import { getDb } from \"@bugstack/db\";\nconst REAP_MS = 60 * 1000;\nsetInterval(reap, REAP_MS);\n`;\n\nconst NEON_ENV = \"DATABASE_URL=postgres://[email-redacted]/app\";\n\n/**\n * The mocked `downloadSnapshot` must declare the same parameter the real port\n * takes (`{owner, repo, ref?, maxFileBytes?}`), even though this helper never\n * reads it. `vi.fn(async () => ...)` with a zero-arg function makes\n * TypeScript infer `mock.calls[number]` as the EMPTY tuple `[]`, so\n * `mock.calls[0][0]` fails to typecheck (TS2493) and every downstream\n * `call.xxx` access on that undefined-typed result cascades into TS18048.\n */\nfunction github(files: Record<string, string>) {\n return {\n downloadSnapshot: vi.fn(\n async (_input: { owner: string; repo: string; ref?: string; maxFileBytes?: number }) =>\n Object.entries(files).map(([path, text]) => ({ path, size: text.length, text })),\n ),\n };\n}\n\nfunction deps() {\n const raised: Array<Record<string, unknown>> = [];\n return {\n raised,\n raiseSignal: async (sig: Record<string, unknown>) => {\n raised.push(sig);\n },\n today: () => \"2026-08-04\",\n };\n}\n\ndescribe(\"scanOneRepo\", () => {\n it(\"raises ONE signal for a repo with findings, in the playbook's shape\", async () => {\n const d = deps();\n const out = await scanOneRepo(\n TARGET,\n github({ \".env.example\": NEON_ENV, \"src/jobs.ts\": OFFENDING }),\n d as never,\n );\n\n expect(out).toEqual({ kind: \"raised\", findings: 1 });\n expect(d.raised).toHaveLength(1);\n\n const sig = d.raised[0]!;\n expect(sig.kind).toBe(\"anomaly\");\n // Load-bearing: REPO_REQUIRING_KINDS gates fixer dispatch on a linked\n // repository, so a null here ends the loop at \"tell a human\".\n expect(sig.repositoryId).toBe(\"repo-1\");\n\n const payload = sig.payload as Record<string, unknown>;\n // The SAME discriminator the CI bridge emits — one playbook, two entry\n // points.\n expect(payload.playbook).toBe(\"cadence\");\n expect(payload.source).toBe(\"server_side_cadence_scan\");\n expect(payload.suggestedAgents).toEqual([\"fixer\"]);\n expect(String(payload.summary)).toContain(\"acme/svc\");\n expect(String(payload.summary)).toContain(\"neon\");\n });\n\n it(\"dedups to one signal per repo per DAY\", async () => {\n // The scan is whole-tree with no base ref to diff against, so every pass\n // re-reports every standing violation. Without a stable per-day key the\n // same finding would open a fresh incident daily.\n const files = { \".env.example\": NEON_ENV, \"src/jobs.ts\": OFFENDING };\n const a = deps();\n await scanOneRepo(TARGET, github(files), a as never);\n const b = deps();\n await scanOneRepo(TARGET, github(files), b as never);\n expect(a.raised[0]!.externalId).toBe(b.raised[0]!.externalId);\n expect(a.raised[0]!.externalId).toBe(\"cadence-scan:repo-1:2026-08-04\");\n });\n\n it(\"distinguishes CLEAN from COULD-NOT-LOOK\", async () => {\n // Clean: the token saw the repo and there was nothing to report.\n const clean = await scanOneRepo(\n TARGET,\n github({\n \".env.example\": NEON_ENV,\n \"src/ok.ts\": 'import { getDb } from \"@bugstack/db\";\\nalignedInterval(1800000, t);',\n }),\n deps() as never,\n );\n expect(clean).toEqual({ kind: \"skipped\", reason: \"clean\" });\n\n // Not visible: the org's single OAuth token cannot see this repo. This is\n // NOT an all-clear and must never be reported as one.\n const blind = await scanOneRepo(\n TARGET,\n {\n downloadSnapshot: async () => {\n throw new Error(\"Not Found\");\n },\n },\n deps() as never,\n );\n expect(blind).toMatchObject({ kind: \"skipped\", reason: \"not_visible\" });\n expect((blind as { detail?: string }).detail).toContain(\"Not Found\");\n });\n\n it(\"stays silent on a repo with no idle-suspend database\", async () => {\n const out = await scanOneRepo(\n TARGET,\n github({\n \".env.example\": \"DATABASE_URL=postgres://[email-redacted]:5432/app\",\n \"src/jobs.ts\": OFFENDING,\n }),\n deps() as never,\n );\n expect(out).toEqual({ kind: \"skipped\", reason: \"not_armed\" });\n });\n\n it(\"skips honestly when the org has no GitHub connector\", async () => {\n const out = await scanOneRepo(TARGET, null, deps() as never);\n expect(out).toEqual({ kind: \"skipped\", reason: \"no_connector\" });\n });\n\n it(\"asks for an explicit ref and a per-file cap\", async () => {\n // downloadSnapshot defaults ref to \"\" — an empty path segment, not the\n // default branch. And an uncapped snapshot of a large repo is how this\n // worker OOMs on a 2GB box.\n const gh = github({ \".env.example\": NEON_ENV, \"src/jobs.ts\": OFFENDING });\n await scanOneRepo(TARGET, gh, deps() as never);\n const call = gh.downloadSnapshot.mock.calls[0]![0];\n expect(call.ref).toBe(\"main\");\n expect(call.maxFileBytes).toBeGreaterThan(0);\n expect(call.owner).toBe(\"acme\");\n expect(call.repo).toBe(\"svc\");\n });\n\n it(\"tolerates null file text without throwing\", async () => {\n // Anything past maxFileBytes comes back as text:null.\n const gh = {\n downloadSnapshot: async () => [\n { path: \"src/big.ts\", size: 999_999, text: null },\n { path: \".env.example\", size: 40, text: NEON_ENV },\n { path: \"src/jobs.ts\", size: OFFENDING.length, text: OFFENDING },\n ],\n };\n const out = await scanOneRepo(TARGET, gh, deps() as never);\n expect(out).toEqual({ kind: \"raised\", findings: 1 });\n });\n});\n"
}
],
"title": "🤖 Deploy fix: Test mock's zero-arg downloadSnapshot breaks mock.calls[0][0] typing"
},
"reasoning": "The `github()` test helper in apps/workers/src/__tests__/cadence-scan.test.ts wraps `downloadSnapshot` in `vi.fn(async () => ...)` with no declared parameter, even though the real `downloadSnapshot` port takes an `{owner, repo, ref?, maxFileBytes?}` argument. Because the mocked function's inferred parameter list is empty, TypeScript infers `mock.calls[number]` as the empty tuple type `[]`, so `gh.downloadSnapshot.mock.calls[0]![0]` fails to typecheck (TS2493: tuple `[]` has no element at index 0), which cascades into `call` being typed as possibly `undefined` at every subsequent `call.xxx` access (TS18048) — exactly the 5 errors in the build log (lines 137-141). This is a test-authoring bug in code added by this branch, not an infra/config issue.\n\nGave the `github()` helper's `downloadSnapshot` mock an explicit (unused) parameter matching the real port signature (`{owner, repo, ref?, maxFileBytes?}`) so vi.fn infers a non-empty parameter tuple. This makes `gh.downloadSnapshot.mock.calls[0]` a 1-tuple, so indexing `[0]` type-checks and every subsequent `call.ref` / `call.maxFileBytes` / `call.owner` / `call.repo` access resolves to a defined type, clearing both the TS2493 and TS18048 errors without touching any other test or the module under test.",
"approvedAt": null,
"executedAt": "2026-08-04T16:44:48.471Z",
"incidentId": "3ef9f0c3-46fb-4882-bb1d-3a63ffe644a0",
"proposedAt": "2026-08-04T16:44:31.335Z",
"autonomyRuleId": null,
"governanceDecision": "propose_and_auto_merge_on_test_pass",
"proposedByAgentRunId": "6cec2fc8-d087-4455-9418-c223e839a0f6"
},
"attempts": 1,
"artifacts": [
{
"data": {
"url": "https://github.com/Bugstackai/BugStack/pull/11",
"repo": "bugstack",
"owner": "bugstackai",
"prNumber": 11,
"baseBranch": "main",
"headBranch": "bugstack/deploy-fix/b12f7893-1785861871329"
},
"kind": "pr_url"
}
],
"evaluatorResults": [
{
"verdict": {
"result": "pass"
},
"structured": {
"fileWarn": 20,
"lineWarn": 500,
"fileCount": 1,
"fileLimit": 50,
"lineCount": 166,
"lineLimit": 2000
},
"evaluatorId": "diff_size"
},
{
"verdict": {
"result": "pass"
},
"structured": {
"note": "no riskClass on input",
"riskClass": null,
"environment": "prod"
},
"evaluatorId": "risk_class"
},
{
"verdict": {
"result": "pass"
},
"structured": {
"override": false,
"actionType": "open_pull_request",
"environment": "prod"
},
"evaluatorId": "environment_floor"
},
{
"verdict": {
"result": "pass"
},
"structured": {
"actionType": "open_pull_request",
"environment": "prod"
},
"evaluatorId": "action_kind"
},
{
"verdict": {
"result": "pass"
},
"structured": {
"confidence": null
},
"evaluatorId": "confidence"
},
{
"verdict": {
"result": "pass"
},
"structured": {
"intact": true
},
"evaluatorId": "signature_chain"
},
{
"verdict": {
"result": "pass"
},
"structured": {
"note": "no tests metadata; type has a downstream gate"
},
"evaluatorId": "test_coverage"
},
{
"verdict": {
"result": "pass"
},
"structured": {
"note": "reproduction contract gates fixer merges only",
"applicable": false
},
"evaluatorId": "reproduction"
},
{
"verdict": {
"result": "pass"
},
"structured": {
"note": "no blastRadius metadata",
"stub": true
},
"evaluatorId": "blast_radius"
},
{
"verdict": {
"result": "pass"
},
"structured": {
"note": "no licenses metadata",
"stub": true
},
"evaluatorId": "license_compliance"
},
{
"verdict": {
"result": "pass"
},
"structured": {
"note": "no branchProtection metadata",
"stub": true
},
"evaluatorId": "branch_protection"
}
],
"governanceDecision": {
"reason": "Per-repo autonomy level: autopilot.",
"decision": "propose_and_auto_merge_on_test_pass",
"matchedRuleId": null,
"matchedRulePriority": null
}
}Redaction metadata
{
"patternMatches": [
"email",
"email"
],
"redactedFields": [],
"scrubberVersion": "1.0.0"
}