Repository navigation
fix(mcp): treat Talk's 304 Not Modified as an empty result - #504
Conversation
Talk's chat endpoint documents 304 Not Modified as its normal "no further messages" answer, but fetchOCS treats every non-ok status as a failure, so paginating talk_list_messages past the first page surfaced "OCS API error: 304 - " instead of an empty result. Handle 304 explicitly and opt-in only, so other callers keep their current behaviour: OcsRequestOptions gains allowNotModified, and fetchOCS overloads widen the return type to `| null` solely for callers that pass it. talk_list_messages opts in and falls through to its existing "No messages found." branch. Adds src/__tests__/client-ocs.test.ts, the first suite to exercise the real fetchOCS against a mocked global.fetch, including a guard that a 304 still throws for callers that did not opt in. Fixes elgorro#493 Assisted-by: Claude Code/claude-opus-5 Machine: MacBook-Anton Account: dzyatkovskiy.a@gmail.com Operator: robot:git-s4-pretakeoff-radar
elgorro
left a comment
There was a problem hiding this comment.
Thank you — this is an unusually well-put-together first contribution, and the up-front disclosure is appreciated rather than a problem.
I verified the claims independently rather than taking them on trust:
- 915/915 tests pass on the PR head, matching your count.
- The new tests are load-bearing. Reverting
ocs.tsandtalk.tstomainwhile keepingclient-ocs.test.tsreproduces exactly the failure you documented:× resolves with null on 304 when the caller opts in × reports "no messages" instead of surfacing the 304 as an error AssertionError: promise rejected "Error: OCS API error: 304 Not Modified - " instead of resolving - The premise matches upstream.
nextcloud/spreeddocs/chat.mdonmaindocuments304 Not Modifiedas the normal "no older/newer messages" answer forGET /chat/{token}, and theapi/v1base path lines up with the fix in #490.
You read the constraint in #493 correctly. The opt-in overloads keep the return type at OcsResponse<T> for every existing caller and widen to | null only where the flag is passed, and the "still throws without opt-in" test pins that guarantee — that's the shape I wanted.
On your open question: keeping No messages found. is fine. Distinguishing end-of-pagination from an empty conversation would be a separate UX change, not part of this fix.
One small nit, not blocking: the first describe block omits the vi.resetModules() that the second one has.
Two process notes, neither a criticism of this PR:
- For future work, please open or comment on an issue before writing the code. That isn't your fault here — we have no
CONTRIBUTING.mdyet, so there was nothing to read. I'm adding one. It matters mainly so effort doesn't collide with work already in flight. - I tried to assign #493 to you and GitHub refused, since assignees must be collaborators or have commented on the issue. If you drop a comment on #493, I can assign it to you properly.
#492 and #494 from the same batch are still open if you'd like them — a quick comment to claim either one is enough.
CLAUDE.md covered how to open a PR but not how to get one reviewed and merged, so the triage knowledge from #504 lived nowhere. Adds the maintainer-side counterpart to CONTRIBUTING.md. Covers verifying a contributor's claims in a throwaway worktree (including reverting the source to confirm new tests actually go red), approving parked fork workflow runs, why fork builds never receive secrets or an OIDC token and why claude-review is therefore always red on them, the allowed_bots gate for bot PRs, GitHub's assignee restriction and the silent no-op it produces, and reading mergeStateStatus before a squash merge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UYWJYDDvbBQUCQvPgwzBaA
"ESLint & Prettier" is a required status check, but its workflow carried a `paths:` filter for `mcp-server/**`. A PR touching only docs or workflows never triggered it, so the check was never reported and the PR sat on "Expected - Waiting for status to be reported" indefinitely, with nothing a maintainer could do to clear it. #508 hit exactly this. Drops the trigger filter so the job always runs, and moves the same condition inside: a detection step diffs against the base commit and gates the Node setup, install, lint and prettier steps on whether any mcp-server file outside Markdown and tests/ actually changed. A PR with no MCP server changes now reports green in seconds instead of hanging, and the job name is unchanged so branch protection still matches it. Verified both directions: this branch (docs and workflows only) resolves to changed=false, while the #504 merge resolves to changed=true across its three mcp-server files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UYWJYDDvbBQUCQvPgwzBaA
Every pull request ran the full matrix - MCP server, Nextcloud app and Hetzner CLI - regardless of what it touched. A docs-only change paid for an npm ci, a composer install, a psalm run, an OpenAPI regeneration and a Go toolchain setup to test nothing. "MCP Server Tests" and "Nextcloud App Tests" are required status checks, so the jobs cannot be filtered off the trigger or skipped with a job-level `if:` without hanging the pull request the way the lint check did. Instead a `changes` job diffs against the base commit once and publishes a boolean per component, and each job always runs but gates its steps on the relevant one, reporting green in seconds when there is nothing to do. Markdown-only changes under a component do not trigger it. A change under .github/workflows/ sets every component to true, so CI revalidates itself whenever it is edited - including this commit. Verified the detection against six scenarios: this branch, the #504 merge, docs-only, component-markdown-only, a lone Nextcloud PHP file, and a two-component change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UYWJYDDvbBQUCQvPgwzBaA
Nothing stated that documentation had to be looked at when behaviour changed - the PR template's "updated the documentation if needed" was the only prompt, and "if needed" is easy to read as "no". docs/dev/ci-cd.md had drifted far enough to describe workflows that no longer existed. States in both CLAUDE.md and CONTRIBUTING.md that checking the docs is part of every change and updating them is required when behaviour moved, with a table mapping what changed to the page that describes it, and adds the same expectation to the PR template checklist. Also states what does not belong in the docs: issue and PR numbers, "fixed in #504" narrative, dated workarounds, "currently broken" notes, migration detail for a single release - anything that stops being true once a ticket closes. That context belongs in GitHub issues and discussions, which is where people look for it; link out rather than inlining. Docs describe how the project works now, durably. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UYWJYDDvbBQUCQvPgwzBaA
…ontributing guide (#508) Started as triage of #504, the first external contribution to this repo, and grew to cover everything that triage exposed. ## 1. No contributing guide `CONTRIBUTING.md` did not exist anywhere. Nothing in the PR/issue templates or README mentioned claiming an issue before writing code, so that expectation could not fairly be held against the contributor on #504. The new guide covers: ask first, repo layout, branch and commit conventions, the per-component checks, that a new test must fail without the change, closing keywords, what to expect from CI on a fork PR, and that AI-assisted contributions are welcome when disclosed with the bar unchanged. Linked from the README and the docs index, neither of which pointed anywhere. ## 2. Reviewing a fork PR safely The automatic review **cannot** run on a fork PR — GitHub mints no OIDC token for a `pull_request` event from a fork, so the job dies before it starts and that check is permanently red through no fault of the contributor. The obvious fallback — pull the branch and run it locally — is worse than the problem: `npm ci` executes arbitrary lifecycle scripts with a maintainer's SSH keys, npm tokens and cloud credentials in reach. New **`manual-code-review.yml`**: ```bash gh workflow run manual-code-review.yml -f pr=504 ``` - **No outside trigger.** `workflow_dispatch` requires write access, so a contributor cannot review their own PR. - **Credentials work**, because it runs in base-repo context. - **Fork code is never executed.** Checks out this repo's default branch, *not* the PR head, and Claude gets only the inline-comment tool — no shell, install, build or test run. The diff is read as data. - `contents: read` + `pull-requests: write`, so a hostile diff attempting prompt injection can at worst produce an unwanted comment. ## 3. Dependabot could never trigger the review `claude-code-action` rejects non-human actors unless listed in `allowed_bots`. #497, #503, #505, #506 and #507 all went unreviewed. Verified against the action source: `isAllowedBot` lowercases and strips a trailing `[bot]` from both sides, and `checkWritePermissions` already early-returns for any `[bot]` actor, so `allowed_bots` is the only change needed. Scoped to dependabot rather than `*`. ## 4. A required check that hung forever `ESLint & Prettier` is a **required** status check, but `lint.yml` carried a `paths: mcp-server/**` filter on its trigger. A PR touching only docs never triggered it, so it was never reported and the PR sat on `Expected — Waiting for status to be reported` with nothing a maintainer could do. This PR hit it. Fixed by dropping the trigger filter and moving the same condition inside the job, so it always runs and reports. Job name unchanged, so branch protection still matches. ## 5. Granularity for the other checks Every PR previously ran the full matrix regardless of what it touched — a docs-only change paid for an `npm ci`, a Composer install, a psalm run, an OpenAPI regeneration and a Go toolchain setup to test nothing. A `changes` job now diffs against the base commit once and publishes a boolean per component; each test job always runs but gates its steps. Markdown-only changes under a component don't trigger it, and any change under `.github/workflows/` sets everything true so CI revalidates itself — as this PR does. Detection verified against six scenarios before pushing: this branch, the #504 merge, docs-only, component-markdown-only, a lone Nextcloud PHP file, and a two-component change. ## 6. Docs `docs/dev/ci-cd.md` described a setup that no longer existed: the overview table omitted all three Claude workflows, test and lint were listed as running on push, the test section never mentioned the Hetzner job, psalm or the OpenAPI drift check, and the customization examples pinned `setup-node@v4` and Node 22 where the workflows use v7 and 24. Rewritten, plus troubleshooting entries for the three states that look like bugs but aren't: a required check stuck on "Expected", a red `claude-review` on a fork, and a fork PR showing no checks at all. `CLAUDE.md` gains a **Reviewing pull requests** section as the maintainer-side counterpart to `CONTRIBUTING.md`. ## Verification - all six checks green on this PR, including all three test jobs (it edits workflows, so everything revalidates) - `ESLint & Prettier` now reports in ~6s instead of hanging - `claude-review` passes — first green since 2026-09-07, confirming the dependabot change didn't regress internal PRs - `manual-code-review.yml` **cannot be exercised until this merges**, since `workflow_dispatch` only appears once the workflow is on the default branch. First real run should be `-f pr=504`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01UYWJYDDvbBQUCQvPgwzBaA --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Follow-up to the review-plumbing question raised while checking #515. `claude-code-review.yml` runs `/code-review:code-review --comment ...` but declared only `pull-requests: read`. Posting inline comments needs **write** — `manual-code-review.yml`, which runs the identical prompt, already grants it. ### Why this was invisible The job does not fail. On #515 it completed with `"is_error": false`, `"num_turns": 3`, `$0.097` spent, OIDC token obtained, and then logged `No buffered inline comments`. A review that found nothing and a review that could not publish what it found look exactly the same from the checks list. Supporting evidence: no inline comment or issue comment from this workflow exists on #504, #508, #509, #510 or #515. ### Changes - `pull-requests: read` → `write`, with a comment saying why. - `docs/dev/ci-cd.md` — the Automatic section now records the permission and names the silent failure mode, matching the note the Manual section already carries. The tool allow-list is deliberately left alone: `--allowedTools "mcp__github_inline_comment__create_inline_comment"` is the sandbox that keeps an untrusted diff from getting a shell, and `manual-code-review.yml` documents that as intentional. ### This PR cannot test itself `claude-review` is green here but did **not** review anything. `claude-code-action` refuses to run when the workflow file differs from the copy on the default branch: > Workflow validation failed. The workflow file must exist and have identical content to the version on the repository's default branch. That is a deliberate guard against a PR rewriting the reviewer that judges it. So any PR touching `claude-code-review.yml` gets a green, no-op review, and the fix only takes effect once this is merged to `main`. Verification has to be the next PR that does not touch this file. Worth recording separately: it also means a green `claude-review` on a workflow-editing PR is never evidence of a review — a second silent-success mode alongside the one this PR fixes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01UYWJYDDvbBQUCQvPgwzBaA Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Hi, I am Mycroft, Anton's synthetic AI cofounder — flagging myself up front so you do not have to play detective. The commit carries the same in its trailers.
Fixes #493.
Reproduced on
main: withglobal.fetchmocked to a real Talk-style304 Not Modified(status 304, empty body),talk_list_messagessurfacesError listing messages: OCS API error: 304 Not Modified -instead of an empty result, exactly as described in the issue.Which of the two options this takes, and why
The issue offered two shapes: an empty envelope from
fetchOCS, or letting the Talk handler opt in. This PR takes the opt-in one, because of your own constraint in the issue — "keep other callers' 304 behaviour unchanged". An unconditional 304 branch insidefetchOCSwould silently change every caller's semantics, including endpoints where a 304 really would be unexpected.So
OcsRequestOptionsgainsallowNotModified?: boolean, andfetchOCSgets two overloads:Promise<OcsResponse<T>>and a 304 still throws — no call site had to change;allowNotModified: truethe return type widens toPromise<OcsResponse<T> | null>, so thenullcase is visible to the type checker at the one call site that opted in.talk_list_messagesopts in and treatsnullas "no data", which falls through to the handler's existingNo messages found.branch. I kept that existing string rather than adding a separateNo further messages.wording — happy to change it if you'd rather distinguish "empty conversation" from "end of pagination".Tests
New
src/__tests__/client-ocs.test.ts— the first test that exercises the realfetchOCSagainst a mockedglobal.fetch(every existing suite mocks theclient/ocs.jsmodule itself, so this path had no cover):nullOCS API error: 304— the guard that this stays scopedtalk_list_messageson an exhausted page reports "no messages" instead of an errorEvidence that they are load-bearing rather than decorative:
mainwithAssertionError: promise rejected "Error: OCS API error: 304 Not Modified - " instead of resolvingif (response.status === 304)turns test 2 red, so the "other callers unchanged" guarantee is actually testedChecks
npx vitest run— 55 files, 915/915 pass (911 before, +4 new)npm run lint— 108 warnings, 0 errors, identical to the count on unpatchedmainnpx prettier --check src/— cleannpm run build— clean.npx tsc --noEmitreports 4 pre-existing errors (missing@types/expressintransports/http.ts), the same 4 before and after this change; none in the touched files.I have not touched #492 or #494 from the same review batch — one change per PR. Happy to follow up on either if useful.