Skip to content

feat(services): migrate status-report domain (PR 1/N)#2101

Merged
mxkaske merged 8 commits intomainfrom
feat/services-status-report
Apr 23, 2026
Merged

feat(services): migrate status-report domain (PR 1/N)#2101
mxkaske merged 8 commits intomainfrom
feat/services-status-report

Conversation

@mxkaske
Copy link
Copy Markdown
Member

@mxkaske mxkaske commented Apr 23, 2026

Summary

Stacked on #2100. First domain migration onto the service layer. Moves every status-report write and read onto packages/services/src/status-report/ and makes tRPC / Connect / Slack thin adapters. Freezes the ServiceContext shape and the single-object { ctx, input } argument convention for every subsequent domain PR.

What lands here

Services (packages/services/src/status-report/)

  • createStatusReport, addStatusReportUpdate, resolveStatusReport (convenience wrapper over add-update with status=resolved), updateStatusReport (metadata + associations), updateStatusReportUpdate (single-update edit), deleteStatusReport, deleteStatusReportUpdate, listStatusReports, getStatusReport, notifyStatusReport.
  • All mutations wrapped in withTransaction, emit emitAudit before returning, and throw the typed ServiceError hierarchy (NotFound / Forbidden / Conflict / …).
  • Zod schemas exported per operation are the contract — .parse() runs at every entry as defence-in-depth.
  • Internal helpers: validatePageComponentIds, updatePageComponentAssociations, getReportInWorkspace, getReportUpdateInWorkspace, getUpdatesForReport, getPageComponentIdsForReport.
  • Notification is a separate service call. notifyStatusReport({ ctx, input }) wraps dispatchStatusReportUpdate with workspace-scope + status-subscribers plan check, and is invoked as a second awaited call after the mutation — preserving the dashboard's two-call pattern (mandatory under the Edge runtime).

Surfaces — external contracts preserved

  • tRPC (packages/api/src/router/statusReport.ts): every procedure now calls a service. create still returns { ...initialUpdate, notifySubscribers }. list and get still return the enriched { updates, pageComponents, page } shape the dashboard consumes. emailRouter.sendStatusReport becomes a thin wrapper over notifyStatusReport.
  • Connect RPC (apps/server/src/routes/rpc/services/status-report/): handler is now proto → parse → service call → convert-back. External Connect API unchanged. Helpers previously exported from this folder (consumed by Slack) are gone; callers go through services.
  • Slack (apps/server/src/routes/slack/interactions.ts): the four status-report branches (create / add-update / update / resolve) go through services. The maintenance branch still writes to db directly — it migrates in PR 2.

Adapters

  • packages/api/src/service-adapter.tstoServiceCtx(ctx) + toTRPCError(err) (ServiceError → TRPCError with matching codes; ZodError maps to BAD_REQUEST).
  • apps/server/src/routes/rpc/adapter.tstoServiceCtx(rpcCtx) + toConnectError(err). Synthesises actor.keyId = "ws:<workspaceId>" until the auth interceptor captures the real API key id (open question in the plan).
  • apps/server/src/routes/slack/service-adapter.ts — refetches the full workspace for the pending-action's workspaceId, builds a Slack actor context, and renders ServiceError → human-readable Slack messages.

Enforcement

  • Biome noRestrictedImports scoped to packages/api/src/router/statusReport.ts + apps/server/src/routes/rpc/services/status-report/**. Bans @openstatus/db, @openstatus/db/src/schema, drizzle-orm, and the three plan/* subpaths. Tests are ignored (setup/cleanup legitimately touch db).
  • Subpath export @openstatus/services/status-report.
  • packages/api / apps/server now depend on @openstatus/services.

Tests

  • packages/services/src/status-report/__tests__/status-report.test.ts — happy paths, workspace isolation, cascade deletes, cross-workspace ForbiddenError / NotFoundError, the slack-actor audit branch, and enriched-relations for list / get.

Deliberate behaviour changes (small)

  • Zod throws on invalid input where the old Connect silently coerced (e.g. out-of-range limit). Callers that need clamping can catch.
  • updateStatusReport with pageComponentIds: [] now clears associations (same as old Connect's updatePageComponentIds: true). Passing undefined leaves them untouched.

Open questions deferred to later PRs

  • Connect auth interceptor does not capture API key id today — surfaced as actor.keyId = "ws:<workspaceId>". Fix comes with a small interceptor change in a later PR.
  • Slack LLM tool schemas still hand-written; a follow-up can try feeding services' Zod schemas directly through the AI SDK if round-trip is clean.

Test plan

  • CI runs the new services integration tests (seeded turso) alongside existing Connect + tRPC suites
  • Dashboard statusReport.createemailRouter.sendStatusReport two-call path still works (no frontend change)
  • Connect CreateStatusReport / AddStatusReportUpdate preserve notify semantics (single RPC call externally, two service calls internally)
  • Slack create / add-update / update / resolve actions (approve / approve_notify / cancel) still round-trip
  • Biome override fires if a new import from @openstatus/db sneaks into a scoped file

Stack

🤖 Generated with Claude Code

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openstatus-dashboard Ready Ready Preview, Comment Apr 23, 2026 1:02pm
openstatus-status-page Ready Ready Preview, Comment Apr 23, 2026 1:02pm
openstatus-web Ready Ready Preview, Comment Apr 23, 2026 1:02pm

Request Review

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 25 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/api/src/router/statusReport.ts">

<violation number="1" location="packages/api/src/router/statusReport.ts:202">
P2: `statusReport.list` now silently truncates to 100 reports because it hard-codes `limit: 100` with no pagination path. Workspaces with >100 reports will not receive the full list.</violation>
</file>

<file name="apps/server/src/routes/rpc/adapter.ts">

<violation number="1" location="apps/server/src/routes/rpc/adapter.ts:42">
P2: Map `LIMIT_EXCEEDED` to `Code.ResourceExhausted` instead of `Code.PermissionDenied` to preserve correct RPC error semantics.</violation>
</file>

<file name="biome.jsonc">

<violation number="1" location="biome.jsonc:89">
P2: Use Biome's `includes` key here; `include` prevents this override from applying.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/api/src/router/statusReport.ts Outdated
Comment thread apps/server/src/routes/rpc/adapter.ts
Comment thread biome.jsonc
@mxkaske
Copy link
Copy Markdown
Member Author

mxkaske commented Apr 23, 2026

Review response (Cubic findings)

Addressed in 539c821 + also fixes the dashboard build regression that the Vercel preview turned up.

1. ✅ statusReport.list silent truncation at limit: 100

Valid — the old tRPC list was unbounded (findMany with no limit). My migration silently capped at 100. Fixed:

  • ListStatusReportsInput.limit now min(1) with no max at the schema level
  • tRPC passes limit: 10_000 (sentinel "effectively unlimited" for dashboard use)
  • Connect handler keeps its own max=100 cap externally — external API behaviour unchanged

2. ✅ LIMIT_EXCEEDEDCode.ResourceExhausted

Valid — PermissionDenied means "authenticated but not entitled"; quota is ResourceExhausted semantically per the Connect/gRPC code table. Fixed.

3. ❌ Biome include key — no change

This one I'm pushing back on. In Biome 1.8.3 (the version pinned here), OverridePattern.include is the documented glob key — the schema entry reads:

"A list of Unix shell style patterns. The formatter will include files/folders that will match these patterns."

The override is firing — pnpm biome check flagged the Connect test file under apps/server/src/routes/rpc/services/status-report/__tests__/ the moment the noRestrictedImports rule landed (which is why the rule now has the __tests__ ignore). includes (plural) is the Biome 2.x spelling; switching would be a false migration here.

If Biome 2 lands in this repo later, this key changes as part of that upgrade — worth a biome migrate run at that point.

4. 🐛 Dashboard build regression (bonus — not from the Cubic review)

Vercel's preview build caught a TS error I missed: row.original.page.customDomain in components/data-table/status-reports/data-table-row-actions.tsx:35. My service migrated the return type from the old zod-validated page: Page (non-null) to page: Page | null (accurate to the nullable pageId FK). The dashboard narrows on pageId earlier in the component, but TS can't cross-correlate pageId nullability with page nullability.

Fixed by narrowing page to non-null in the tRPC get / list wrappers with a small narrowPage helper that throws on the shouldn't-happen case (status report without its page) — matches the old zod-parse behaviour exactly.

Base automatically changed from feat/services-scaffold to main April 23, 2026 12:29
@vercel vercel Bot temporarily deployed to Preview – openstatus-dashboard April 23, 2026 12:31 Inactive
@vercel vercel Bot temporarily deployed to Preview – openstatus-status-page April 23, 2026 12:31 Inactive
@vercel vercel Bot temporarily deployed to Preview – openstatus-web April 23, 2026 12:31 Inactive
mxkaske and others added 4 commits April 23, 2026 14:32
First domain migration on top of the services scaffold (PR 0). Freezes
the `ServiceContext` shape and the single-object `{ ctx, input }`
argument convention across all three surfaces — tRPC / Connect / Slack.

## Services
- `packages/services/src/status-report/` — create, add-update, resolve
  (convenience wrapper over add-update), update (metadata+associations),
  update-update (edit a single update row), delete, delete-update, list,
  get, notify.
- Shared internals: `validatePageComponentIds`,
  `updatePageComponentAssociations`, `getReportInWorkspace`,
  `getReportUpdateInWorkspace`, `getUpdatesForReport`,
  `getPageComponentIdsForReport`.
- All mutations run inside `withTransaction`, emit an `emitAudit` row
  before returning, and throw the typed `ServiceError` hierarchy.
- Zod schemas are the contract; `.parse()` runs at every service entry
  as defence-in-depth.
- `notifyStatusReport` is the separate-call notification dispatch that
  the dashboard invokes as its second mutation — preserves today's
  two-call pattern required by the Edge runtime.

## Surfaces
- **tRPC** (`packages/api/src/router/statusReport.ts`): every procedure
  now calls a service. External contract is preserved bit-for-bit —
  `create` still returns `{ ...initialUpdate, notifySubscribers }`,
  `list` still returns the enriched `{updates,pageComponents,page}` shape
  the dashboard consumes, etc. `emailRouter.sendStatusReport` becomes a
  thin wrapper over `notifyStatusReport`.
- **Connect RPC** (`apps/server/src/routes/rpc/services/status-report/`):
  handler extracts proto → parse → service call → convert back. Domain
  helpers previously exported from this folder (and consumed by Slack)
  are gone; callers go through services directly.
- **Slack** (`apps/server/src/routes/slack/interactions.ts`): the four
  status-report branches (create / add-update / update / resolve) call
  services. The maintenance branch still writes to db directly — that
  migrates in PR 2.

## Adapters
- `packages/api/src/service-adapter.ts` — `toServiceCtx(ctx)` +
  `toTRPCError(err)` (ServiceError → TRPCError).
- `apps/server/src/routes/rpc/adapter.ts` — `toServiceCtx(rpcCtx)` +
  `toConnectError(err)`. Synthesises `actor.keyId` as `ws:<workspaceId>`
  until the auth interceptor captures the real key id (called out in
  the plan's open questions).
- `apps/server/src/routes/slack/service-adapter.ts` — refetches the full
  workspace for the `PendingAction`'s workspaceId and builds a Slack
  actor context; `toSlackMessage(err)` renders human-readable failures.

## Enforcement
- Biome `noRestrictedImports` (nursery rule) scoped to
  `packages/api/src/router/statusReport.ts` +
  `apps/server/src/routes/rpc/services/status-report/**`. Tests are
  ignored since setup/cleanup legitimately manipulates db state.
- Subpath export `@openstatus/services/status-report` so surfaces
  import the domain barrel directly.
- `packages/api` / `apps/server` now declare `@openstatus/services` as
  a dependency.

## Tests
- Integration tests in `packages/services/src/status-report/__tests__/`
  cover happy paths, workspace isolation, cascading deletes, the
  `ForbiddenError` / `NotFoundError` paths, and the slack-actor audit
  branch. `expectAuditRow` asserts on the in-memory audit buffer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- **Dashboard build**: `data-table-row-actions.tsx:35` accesses
  `row.original.page.customDomain`, which the old tRPC zod schema made
  non-nullable. My migrated service returned `page: Page | null`,
  breaking the type. Narrow `page` to non-null in the tRPC `get` / `list`
  wrappers via a tiny `narrowPage` helper that throws on the shouldn't-
  happen case (status report without its page) — matches the old zod
  parse behaviour.

- **`listStatusReports` silent truncation (Cubic finding)**: the tRPC
  wrapper hard-coded `limit: 100`, silently dropping reports beyond
  that ceiling for workspaces with >100 reports. Service now allows an
  unbounded `limit` (min 1, no max); tRPC passes a `10_000` sentinel
  and Connect keeps its external `max=100` cap in the handler.

- **`LIMIT_EXCEEDED` Connect mapping (Cubic finding)**: map to
  `Code.ResourceExhausted` rather than `Code.PermissionDenied` —
  quota/rate-limit is the documented meaning of ResourceExhausted in
  Connect/gRPC, while PermissionDenied means "authorised but not
  entitled". Correct semantics on the wire.

Biome `include` key (Cubic's third finding) is verified correct for
Biome 1.8.3 — OverridePattern schema documents `include` as the glob
selector, and our override fires as expected (it flags the Connect
test file in the scoped dir). No change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e PR

Applies to status-report since the same patterns were copy-pasted across
both domains:

- **Dedupe pageComponentIds** in `validatePageComponentIds` — duplicate
  ids in the input would hit the composite-PK constraint on
  `status_report_to_page_component` during insert.
- **Batch list enrichment** — `listStatusReports` previously fanned out
  3 queries per row (updates / component associations / page + page
  components). Dashboard calls it with `limit: 10_000`, so the N+1 was
  real. Rewritten as three batched `IN` queries regardless of row count;
  pages are deduplicated so reports sharing a page share the Page object.
  `getStatusReport` reuses the batch path with a singleton.
- **Idempotent `delete`** — the old tRPC `statusReport.delete` silently
  succeeded when the row was missing; my service throws `NotFoundError`.
  Catch and swallow in the tRPC wrapper only. Connect still returns
  404 on missing — external API semantics preserved.
- **Connect numeric-id error** — `parsePageComponentIds` was throwing
  `invalidDateFormatError` for non-numeric input. Replaced with an
  inline `ConnectError(Code.InvalidArgument)` with the correct message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `packages/services/package.json` and `packages/importers/package.json`
to the `install` stage bind list so `bun install --frozen-lockfile` can
resolve the dep graph inside the Docker build.

Without this, the moment main receives:
  - `@openstatus/services` as a dep of `apps/server` (landing via the
    status-report PR that this branch targets), bun install fails with
    a missing workspace package.
  - `@openstatus/importers` becomes a transitive dep of
    `@openstatus/services` later in the stack (via the import-domain
    migration), so we bind it proactively in the same commit rather
    than landing a second dofigen touch-up.

`packages/importers` already exists in the repo and isn't used by
apps/server directly at this point, so binding it early is a no-op
for bun until it actually becomes a dep. Alternative was two sequential
fix PRs; one covers the whole stack cleaner.

`Dockerfile` + `dofigen.lock` are regenerated from `dofigen.yml` via
`dofigen update` — both are generated outputs that must stay in sync
with the yml source.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mxkaske mxkaske force-pushed the feat/services-status-report branch from 3dfbbb8 to db59d2f Compare April 23, 2026 12:33
@mxkaske
Copy link
Copy Markdown
Member Author

mxkaske commented Apr 23, 2026

@claude review

`packages/services/src/status-report/notify.ts` imports from
`@openstatus/subscriptions`, which transitively pulls in
`@openstatus/emails`. The emails package validates `RESEND_API_KEY`
via `@t3-oss/env-core` at module-load time and throws on missing —
which killed `@openstatus/services#test` in CI (neither the test.yml
workflow nor local `pnpm test` sets the var).

Adds a bun test preload that stubs the env var with a dummy value
before any test file is imported. Tests never exercise the real
Resend dispatch path, so this is scoped to unblocking the import
graph rather than mocking the dispatcher itself.

Added:
  - packages/services/bunfig.toml — points bun:test at the preload.
  - packages/services/test/preload.ts — sets the dummy env var.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two CI failures surfaced after the services migration changed error
shapes for status-report writes:

- **tRPC `statusReport.create` / `.updateStatus` cross-workspace tests**
  expected `FORBIDDEN` when a caller passed pageComponentIds belonging
  to another workspace. The service's `validatePageComponentIds` now
  throws `NotFoundError` for the same case — intentionally, so cross-
  workspace lookups don't leak resource existence. Updated both tests
  to assert `NOT_FOUND` and left a comment pointing at the migration
  rationale.

- **Connect `CreateStatusReport` page-mismatch test** asserted the
  error message contained "does not match the page ID". The service
  throws `ConflictError("pageId X does not match the page (Y) of the
  selected components.")` — no "ID" in the wording. Loosened the
  assertion to "does not match the page" so it matches the actual
  service string.

Both are pure test updates — the new service behavior is the intended
contract; the tests had drifted from the pre-migration wording.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five findings from the PR review, fixed in one commit:

**P1 — tRPC `LIMIT_EXCEEDED` → `TOO_MANY_REQUESTS`**
`packages/api/src/service-adapter.ts` mapped `LIMIT_EXCEEDED` to
`FORBIDDEN`. `FORBIDDEN` means "authenticated but not allowed to do
this on this resource" — plan quota exhaustion is semantically
different. Mirrors the Cubic fix on the Connect side (which moved
from `PermissionDenied` to `ResourceExhausted`).

**P2 — fragile Drizzle join result key in `enrichReportsBatch`**
`packages/services/src/status-report/list.ts` read
`row.status_report_to_page_component.statusReportId` — Drizzle
auto-derives that key from the schema variable name at import time,
so a rename would silently produce `undefined` at runtime. Replaced
with explicit-column select (`reportId:
statusReportsToPageComponents.statusReportId`) which is typed and
rename-safe.

**P2 — Slack `NOT_FOUND` messages leak internal row IDs**
`NotFoundError` formats as `"<entity> <id> not found"` (e.g.
`"page_component 17 not found"`). For API consumers that's fine —
they're developers. For Slack workspace users those IDs are
meaningless noise and surface internals. `toSlackMessage` now
returns a fixed `"Couldn't find what you were looking for."` for
`NOT_FOUND`; other `ServiceError` codes keep their hand-written
messages (they're sentence-style to start with).

**P3 — test cleanup without try/finally**
`packages/services/src/status-report/__tests__/status-report.test.ts`
had 13 tests that did inline `db.delete(statusReport)` cleanup at
the end of their body — skipped on assertion failure. Replaced with
a shared `createdReportIds` array + `afterEach` drain so cleanup
runs regardless of test outcome.

**P3 — hoisted `toServiceCtx` out of Slack `executeAction` switch**
Every service-backed branch loaded the workspace row via
`toServiceCtx`. Hoisted the call above the switch — halves db
lookups per Slack action execution. The `createMaintenance` branch
(still on db until PR 2) doesn't use the ctx but the extra lookup
there is negligible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two server-side tests were failing after the migration:
  - `updates pageId when changing components to different page`
  - `clears pageId when removing all components`

Both expected `report.pageId` to track the association set:
  - Swap components to a different page → `pageId` moves.
  - Empty the association set → `pageId` becomes null.

The service was enforcing a stricter invariant — it *threw* a
`ConflictError` when the new components' page differed from the
report's current page, and left `pageId` untouched when associations
were cleared. That was an overcautious choice in the initial
migration; no caller today wants a "report pinned to page X
regardless of its components" guarantee.

Simplified to: whenever `pageComponentIds` is supplied,
`updateValues.pageId = validated.pageId`. The zero-component case
sets `pageId = null`; the one-page case moves the report to that
page. Mixed-page inputs are still rejected by
`validatePageComponentIds`'s `componentPageIds.size > 1` check, so
the only reachable states here are "one page" or "no components".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mxkaske
Copy link
Copy Markdown
Member Author

mxkaske commented Apr 23, 2026

@claude review and check again

@claude
Copy link
Copy Markdown

claude Bot commented Apr 23, 2026

Claude finished @mxkaske's task in 2m 1s —— View job


PR Re-Review: feat(services): migrate status-report domain (PR 1/N)

Tasks:

  • Gather context from previous review and PR state
  • Check if P1 tRPC LIMIT_EXCEEDED mapping was fixed
  • Re-examine Drizzle join key fragility in enrichReportsBatch
  • Check Slack error message exposure
  • Review test cleanup patterns
  • Check latest commits since previous review
  • Post updated review

Follow-up review — all previous findings addressed

Commit 736c9823 ("address post-migration review on #2101") and 5fe213c5 close every item raised in my first pass. Here's the verification:

P1 — tRPC LIMIT_EXCEEDEDTOO_MANY_REQUESTS
packages/api/src/service-adapter.ts now maps LIMIT_EXCEEDED to TOO_MANY_REQUESTS with a comment explaining the semantic distinction. Symmetric with the Connect side's Code.ResourceExhausted.

P2 — Fragile Drizzle join key in enrichReportsBatch
packages/services/src/status-report/list.ts now uses an explicit-column select with reportId: statusReportsToPageComponents.statusReportId — typed, rename-safe, no cast. The code comment explains why the old approach was fragile.

P2 — Slack NOT_FOUND exposes internal row IDs
apps/server/src/routes/slack/service-adapter.ts now returns ":x: Couldn't find what you were looking for." for NOT_FOUND instead of the raw err.message. The JSDoc explains the reasoning for future domain expansions.

P3 — Test cleanup without try/finally
createdReportIds: number[] + afterEach drain pattern is in place. Test bodies push created IDs; cleanup is guaranteed regardless of assertion outcome. The comment above the array explains the intent clearly.

P3 — Multiple toServiceCtx calls per Slack action
toServiceCtx is hoisted above the switch in executeAction. Single workspace DB lookup per action. The comment acknowledges the createMaintenance branch doesn't use ctx (migrates in PR 2).

Bonus fix — pageId follows component associations on update
Commit 5fe213c5 simplified updateStatusReport to always set updateValues.pageId = validated.pageId when pageComponentIds is supplied. The previous overcautious guard (ConflictError on page-change, no-op on clear) didn't match what Connect's pre-migration tests encoded as the intended invariant.


Remaining minor notes (non-blocking, no action needed)

  • narrowPage throws plain Error — still lands as INTERNAL_SERVER_ERROR through toTRPCError's catch-all, which is correct. The row ID in the message is acceptable here: it only fires on a data-integrity violation that "can't happen" in normal operation, and the error goes to Sentry/logtape, not a user-facing surface.
  • updateStatus discards service return value — pre-migration parity preserved. Flag for the caller if the dashboard ever needs the updated row without a refetch.
  • resolveStatusReport double-parses input — harmless; the inner addStatusReportUpdate call re-parses but no Zod penalty worth optimising here.

Verdict

All P1/P2 issues resolved, P3 fixes solid, bonus behavioral fix correct. The service layer contract is clean and the enforcement mechanism (Biome noRestrictedImports) is in place. Ready to merge from a code-review standpoint.

@openstatusHQ openstatusHQ deleted a comment from claude Bot Apr 23, 2026
@mxkaske mxkaske merged commit 8a353d5 into main Apr 23, 2026
12 of 13 checks passed
@mxkaske mxkaske deleted the feat/services-status-report branch April 23, 2026 13:01
mxkaske added a commit that referenced this pull request Apr 23, 2026
Three CI failures on #2103:

**Slack `toSlackMessage` — revert to uniform generic message.**
The P3 fix on #2101 kept hand-written `ServiceError` messages for
`FORBIDDEN` / `CONFLICT` / `VALIDATION` / `LIMIT_EXCEEDED` — but the
existing `does not leak internal error details to user` test expects
every error to surface as a single `"Something went wrong. Please try
again."` string. The intent is stronger than just "strip row IDs":
Slack users aren't developers, so none of the service error wording
is appropriate for them. Reduced the adapter to a one-liner that
always returns the generic message; detailed error context still
flows to logtape/Sentry via the catch site upstream.

**Handler test — "does not match the page ID" → "does not match".**
`ConflictError` wording drifted during the migration (same as the
status-report fix). Loosened to the stable fragment.

**Handler test — "Page not found" → "not found".**
`NotFoundError("page", id)` formats as `"page <id> not found"`
(lowercase `page`). Pre-migration handler emitted capital-P
`"Page not found"`. Loosened the substring so both forms pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant