fix(server): survive null elements in Forgejo list responses #263

Merged
lucas merged 1 commit from agent/issue-262-null-timeline-entries into main 2026-11-12 13:10:44 +01:00
Collaborator

Closes: #262

issue_label dispatch crashed with null is not an object (evaluating 'c.label') whenever an issue's timeline contained an entry Forgejo could not serialise — those arrive as JSON null. The trigger was recorded failed and no session started, permanently, for every label event on such an issue.

The fix is central, not at the crash site:

  • compactList (new, tools/shared.ts) drops only null/undefined elements and returns [] for a non-array body; jsonList wraps a single array request with it, preserving meta and passing error results through unchanged.
  • fetchAllPages compacts what it accumulates but keeps using the raw page length for the short-page break, so a full page containing a null is not mistaken for the last page (13 call sites, no changes needed).
  • The ten direct client.json<X[]> fetches in dispatch-api.ts, tools/{issues,prs,ci,pr-diff}.ts now use jsonList; the Array.isArray(res.data) ? … : [] follow-ups are gone.
  • ApiCombinedStatus.statuses is compacted in both places it is read.
  • createDispatchApi(injected?) accepts a client, so listLabelEvents can be exercised without the stored config or a database. Existing callers are unaffected.

Tests

  • tools/shared.test.ts: compactList semantics (order, falsy values kept, non-array → []), jsonList compaction/meta/error pass-through, and that fetchAllPages pages on the raw length.
  • dispatch-api.test.ts (new): listLabelEvents over a timeline with a null between two label rows resolves with both events in order instead of throwing, plus a case where the null sits on a full first page to prove paging is not cut short.

Verified: bun test in apps/server (1327 pass, 0 fail) and bun run typecheck at the root.

Closes: #262 `issue_label` dispatch crashed with `null is not an object (evaluating 'c.label')` whenever an issue's timeline contained an entry Forgejo could not serialise — those arrive as JSON `null`. The trigger was recorded `failed` and no session started, permanently, for every label event on such an issue. The fix is central, not at the crash site: - `compactList` (new, `tools/shared.ts`) drops only `null`/`undefined` elements and returns `[]` for a non-array body; `jsonList` wraps a single array request with it, preserving `meta` and passing error results through unchanged. - `fetchAllPages` compacts what it accumulates but keeps using the **raw** page length for the short-page break, so a full page containing a null is not mistaken for the last page (13 call sites, no changes needed). - The ten direct `client.json<X[]>` fetches in `dispatch-api.ts`, `tools/{issues,prs,ci,pr-diff}.ts` now use `jsonList`; the `Array.isArray(res.data) ? … : []` follow-ups are gone. - `ApiCombinedStatus.statuses` is compacted in both places it is read. - `createDispatchApi(injected?)` accepts a client, so `listLabelEvents` can be exercised without the stored config or a database. Existing callers are unaffected. ## Tests - `tools/shared.test.ts`: `compactList` semantics (order, falsy values kept, non-array → `[]`), `jsonList` compaction/meta/error pass-through, and that `fetchAllPages` pages on the raw length. - `dispatch-api.test.ts` (new): `listLabelEvents` over a timeline with a `null` between two label rows resolves with both events in order instead of throwing, plus a case where the null sits on a full first page to prove paging is not cut short. Verified: `bun test` in `apps/server` (1327 pass, 0 fail) and `bun run typecheck` at the root.
Forgejo emits a JSON null for a timeline row it cannot serialise
(deleted comment, review or label). Reading an issue's label history
dereferenced such an element and threw, so every issue_label trigger on
an affected issue failed permanently and no agent session was started.

Every Forgejo list body is now compacted centrally — paged fetches,
unpaged array fetches and a commit's combined statuses — instead of
guarding the one crash site. Paging still advances on the raw server
page length, so a page that contains a null is not mistaken for a short
last page.

createDispatchApi takes an optional client, which lets the dispatch API
be exercised without the stored Forgejo config or a database.

Co-Authored-By: Claude Opus 5 via omp-deck
lucas merged commit 7b00155414 into main 2026-11-12 13:10:44 +01:00
lucas deleted branch agent/issue-262-null-timeline-entries 2026-11-12 13:10:44 +01:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
lucas/omp-deck!263
No description provided.