Files
paperclip/server
Lempkey e1e2cef928 fix(issues): accept array-form ?status= filter and stop crashing on repeated keys (#4628) (#4890)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Boards, agents, and the public REST API all read issue lists via
`GET /api/companies/:cid/issues`, with `?status=` as the most-common
filter
> - Express's default `qs` parser binds repeated keys to a `string[]` —
the conventional URL form `?status=todo&status=in_progress` is therefore
valid input
> - The service layer treated `filters.status` as a string and called
`.split(",")` unconditionally, returning HTTP 500 with `TypeError:
filters.status.split is not a function`. The same buggy pattern lived at
a second call site in the same file
> - This PR adds a small `parseStatusFilter` helper that normalizes all
four shapes the route can receive, routes both service-layer call sites
through it, and widens the `IssueFilters.status` type so the contract
stops lying about runtime reality
> - The benefit is a passive 500 disappears for any client (curl, board,
agent code) that builds `?status=` with array-style binding, and the
type system now forces every future caller to handle both shapes
correctly

## Linked Issues or Issue Description

- Refs #4628
- Closes #4084
- Related earlier attempt: #1964

## What Changed

- **`server/src/services/issues.ts`** — Added exported helper
`parseStatusFilter(input: string | readonly string[] | undefined):
string[]` that normalizes single strings, CSV
(`?status=todo,in_progress`), array (`?status=todo&status=in_progress`),
and mixed array+CSV; trims and filters empties. Widened
`IssueFilters.status` from `string` to `string | readonly string[]`.
Replaced inline `.split` call sites in `list()`, blocked-count
filtering, `count()`, and `countUnreadTouchedByUser()` with
helper-driven branching.
- **`server/src/routes/issues.ts`** — Replaced dishonest
`req.query.status` casts with `string | string[] | undefined` at both
issue-list and blocked-count entry points so the route contract matches
Express `qs` runtime behavior.
- **`server/src/__tests__/parse-status-filter.test.ts`** (new) — 10 unit
cases: undefined, empty string, single, CSV, array, mixed array+CSV,
whitespace trim, trailing/extra commas, no-mutation guarantee, hostile
non-string entry guard.
- **`server/src/__tests__/issues-list-query-parsing.test.ts`** (new) — 5
supertest cases against a minimal Express app whose handler mirrors the
route cast/forwarding pattern: single, CSV, repeated-key array, mixed
array+CSV, and no `?status` param.
- **`server/src/__tests__/issues-service.test.ts`** — Added
embedded-Postgres service coverage for array-form status filters through
`list`, `count`, and `countUnreadTouchedByUser` on current master.

**Why service-layer, not route-layer:** the bug is the service contract.
Fixing only at the route would leave other service-layer call sites
latent, keep `IssueFilters.status` inaccurate, and let future internal
callers reintroduce the same crash. Widening the type is the forcing
function that prevents recurrence.

**Why `parseStatusFilter` is exported, not file-local:** the helper has
direct unit coverage and keeps the normalization logic colocated with
its only current call sites.

## Coordination with prior work

- Supersedes **#4084** (thanks to @adlai88 for the original
report-and-fix). This PR additionally fixes the extra current-master
service call sites, widens `IssueFilters.status` so the type contract is
honest, replaces the incorrect route casts, and ships direct regression
coverage.
- **#1964** bundles unrelated route/service changes; this PR keeps scope
tight per CONTRIBUTING.md's one-PR-one-change guidance.

## Out-of-scope finding

While verifying all query-string status parsing sites, I found a sibling
bug in `server/src/services/execution-workspaces.ts:409` reachable from
`routes/execution-workspaces.ts:48`, where repeated `?status=` keys can
still hit the same `.split(",")` assumption. I left that out of this PR
to keep the review surface small.

## Verification

```bash
pnpm --filter @paperclipai/server exec vitest run \
  src/__tests__/parse-status-filter.test.ts \
  src/__tests__/issues-list-query-parsing.test.ts \
  src/__tests__/issues-service.test.ts \
  --testNamePattern='parseStatusFilter|issue list status query parsing|accepts array-form status filters in list and count|excludes plugin operation issues from unread inbox counts'
```

Result on the rebased head: `3` files passed, `17` tests passed, `72`
skipped.

GitHub CI on PR `#4890` is green on the rebased head
`805731d3270783d0b80b33ee1dccdc6771febef6`, including `verify`,
`Typecheck + Release Registry`, `Build`, `e2e`, general tests,
serialized suites, Socket checks, Snyk, and `Greptile Review`.

Local workspace typecheck commands still encounter unrelated
current-master baseline errors under `packages/plugins/sdk` and
`server/src/services/company-skills.ts`; no failures were produced from
the `issues` files changed in this PR.

## Risks

- **Type widening blast radius:** `IssueFilters.status` widens from
`string` to `string | readonly string[]`. Any direct caller that still
assumes `.split()` on the input now gets a useful typecheck failure
instead of a latent runtime crash.
- **Behavior change:** `?status=todo,in_progress&status=done` previously
returned HTTP 500; it now returns HTTP 200 with the union of matching
statuses. Single-string and CSV behavior remain unchanged.
- **No migration. No breaking API changes. No new deps. No UI changes.**

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. — Confirmed: this is a bug
fix, not a feature. ROADMAP.md grep showed no overlap.

## Model Used

- **Claude Opus 4.7** (Anthropic), `claude-opus-4-7`, 1M context,
extended thinking. Used for problem scoping, implementation, and test
authoring; the final rebasing, PR prep, and verification updates were
handled in the maintainer workflow.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

## Cross-references and status (maintainer)

- Closes #4084

---------

Co-authored-by: Devin Foley <devin@paperclip.ing>
2026-06-11 21:52:43 -07:00
..