Skip to content

feat(telltale): the ingest Worker (P1) - #64

Closed
adbarc92 wants to merge 20 commits into
chore/remove-embargo-guardfrom
feat/telltale-worker
Closed

feat(telltale): the ingest Worker (P1)#64
adbarc92 wants to merge 20 commits into
chore/remove-embargo-guardfrom
feat/telltale-worker

Conversation

@adbarc92

Copy link
Copy Markdown
Owner

Implements P1 of the Telltale spec — the standalone Cloudflare Worker that turns authenticated bug reports from the portfolio's apps and games into deduplicated GitHub issues.

Spec and plan are in #63. Stacked on #62 (the embargo-guard removal) — merge #62 first; this PR retargets to main automatically when it lands.

What this is

Three routes: POST /v1/events (HMAC-authenticated bug intake), plus GET /v1/issues and GET /v1/stats behind an operator token. Crashes deliberately do not flow through this Worker — Sentry's native GitHub integration owns that path with no Telltale code, which is what let the original design lose half its mass.

Every decision that can be a pure function is one (schema, scrub, fingerprint, auth, decide, registry), so the whole policy is unit-tested with no network, no KV and no mocking framework — 82 tests in ~200ms. Two I/O modules sit behind narrow interfaces the tests replace with fakes. index.ts is wiring.

Zero runtime dependencies. WebCrypto only, never node:crypto. New vitest (telltale) CI job modelled on the existing vitest (cockpit/ui) one.

Not in this PR

The senders (P2 — four reference files across nine other repos) and the Command Center dashboard adapter (P3, which consumes GET /v1/issues) are separate subsystems and get their own plans. P0 is Sentry console configuration.

Review process

Eleven tasks, each with an independent scoped review, then a whole-branch review and a final fix wave. That surfaced six defects in the plan's own reference code, all fixed here:

Defect Why it mattered
tenzy pointed at adbarc92/tenzy The repo doesn't exist — it's a transfer redirect to OpenBarclay/tenzy. The primary PAT can't write to an org repo, and a followed 301 turns a POST into a GET.
not_planned not aggregated across closed duplicates An operator's explicit "won't fix" was silently discarded and we'd comment anyway.
Missing Content-Type: application/json Every test stayed green because the fake never does HTTP; it would have failed only in production.
restClient had no tests at all The entire production HTTP path was verified only through the fake's parallel logic.
Unguarded JSON.parse of operator secrets A malformed secret crashed every request before any stat was recorded — the exact silent failure /v1/stats exists to eliminate.
401 returned before the registry lookup Made the spec's required 404 on a typo'd slug unreachable.

The final review also found the comment throttle wasn't project-scoped (KV is one namespace across eleven projects, so two apps sharing a generic title suppressed each other), that an unset OPERATOR_READ_TOKEN made the expected header the guessable literal Bearer undefined, and that there was no top-level error boundary.

Before deploying

adbarc92/telltale-intake and adbarc92/telltale-probe are in the registry but do not exist yet — the README's Deploy section now leads with creating them. Until then GET /v1/issues carries a permanent errors entry for pawsport and the live grader cannot pass.

The live grader (test/live-grader.test.ts) self-skips without credentials and makes no network call, so CI never runs it. It targets a scratch repo, never a product repo, and asserts by reading GitHub back rather than trusting the Worker's own response.

…n fake

- Add restClient tests against a stub fetch: label-drop true/false, PR
  discrimination + state_reason passthrough in findByLabel, and a non-2xx
  throw (an empty array would read as 'no match' and open a duplicate).
- src/github.ts: add the missing Content-Type: application/json header —
  without it a string body defaults to text/plain per the Fetch spec and
  GitHub reads the POST as text, not JSON.
- test/fakes.ts: FakeGitHub now computes labelsDropped with the same
  content-based check as the real client (not a count), and gains a
  selective dropLabel knob alongside the existing all-or-nothing
  dropLabels, so tests can express a partial drop.
…fuse redirects

tenzy was pointed at adbarc92/tenzy under the primary PAT. That path is only a
transfer redirect - `gh api` follows it silently, which is how the value got in -
and the real repo is OpenBarclay/tenzy. The primary PAT's resource owner is the
adbarc92 user, so it cannot write to an org repo at all; and with Workers'
default redirect:'follow' the 301 would have rewritten the POST into a GET,
making createIssue parse an issue array as an object.

So also: api() now uses redirect:'manual' and throws naming the new location, so
a repo transfer is a loud, diagnosable failure rather than a shape mismatch far
downstream.

unauthorized() compared against `Bearer ${env.OPERATOR_READ_TOKEN}`. With the
secret unset - the first-deploy state, since this Worker has never deployed -
the expected header was the literal "Bearer undefined", a guessable password for
every bug-report body in every private registry repo. A missing secret now
denies everything, and the compare reuses the constant-time equals() from
src/auth.ts rather than a second implementation.

README: create adbarc92/telltale-intake and adbarc92/telltale-probe before
deploying - both 404 today, so GET /v1/issues would carry a permanent errors
entry from its first request and the live grader could never pass. "Adding a
project" now states that `account` must name the PAT whose resource owner
actually owns the repo.
…ary, record duplicate_fingerprint

shouldComment keyed on `ct:${fp}:${bucket()}`, but the fingerprint is a hash of
the scrubbed title alone and KV is one namespace across all eleven projects. Two
apps whose users type "Save button does nothing" share a fingerprint, so the
second project's genuine recurrence comment was silently suppressed for an hour.
The key is now `ct:${project}:${fp}:${bucket()}`, consistent with `rl:proj:`.

export default { fetch } had no try/catch, and fail() awaited recordStat before
building the response. Every counter here is a hot key by construction and KV
allows roughly one write per second per key, so under real traffic the throw
escaped handleEvent and the client got Cloudflare's bare 5xx - no JSON envelope
and no stat, the exact silent failure /v1/stats exists to eliminate. recordStat
now swallows its own rejection (covering every call site) and fetch returns
json(500, { error: 'internal' }) for anything else that escapes.

duplicate_fingerprint was declared and required by the spec but unreachable. It
is now recorded alongside 'accepted' when the label lookup returns more than one
open non-PR candidate - the spec knowingly accepts a concurrent-create race, and
that acceptance is only defensible while the operator can see it happening.

Also asserts Retry-After on the 429 test, a binding constraint a refactor could
otherwise drop silently.
@adbarc92

Copy link
Copy Markdown
Owner Author

Closing, not merging — Telltale has moved to its own repo: https://github.com/adbarc92/telltale.

The work here is sound and fully reviewed; it was simply in the wrong repository. "Put it in Command Center" answered a question about where the spec document should live, and was extended to the implementation without ever being put to the operator.

Nothing is lost. All 17 commits touching telltale/ were extracted with git subtree split, so every task's TDD and review history is preserved with telltale/ as the repo root. Verified on the extracted tree before pushing:

  • npm ci — clean
  • npx vitest run82 passed, 1 skipped (the skip is the credential-gated live grader)
  • npm run check (tsc --noEmit) — exit 0

Nothing needs stripping from this repo. telltale/ never existed on main or on chore/remove-embargo-guard — it lived only on this branch — so closing this PR is sufficient. The vitest (telltale) CI job was likewise added only here and never reached main.

The two docs on this branch that were not about Telltale's implementation — docs/handoffs/31f0a85d-….md and the docs/STATUS.md pivot entry — were cherry-picked onto chore/remove-embargo-guard (#62) so the project log keeps the record.

What stays in this repo is the integration only: a feedback source adapter for the Project Dashboard reading Telltale's GET /v1/issues (spec §6, P3). Not started.

@adbarc92 adbarc92 closed this Aug 31, 2026
adbarc92 added a commit that referenced this pull request Aug 31, 2026
Telltale now lives at adbarc92/telltale, extracted with git subtree split so
all 17 commits kept their TDD and review history. Verified before pushing:
82 passed / 1 skipped, tsc --noEmit exit 0.

Also records that the handoff's step 3 was a no-op. telltale/ never existed on
main or on this branch — only on PR #64's branch — so closing that PR rather
than merging it left nothing to strip, the vitest (telltale) CI job included.

#64 and #63 are both closed.
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