Commit graph inspector/scripts/sdk-watch.test.mjs
Author SHA1 Message Date
cliffhall
3b3c7e35d1 fix: address Copilot review round 1 on the SDK watch
Three findings, all real, all fixed.

1. The analysis job's tool grant was too broad. `Bash(gh api:*)` against a
   token holding `issues: write` would have let an injected instruction
   PATCH, close or relabel any issue in the repo — and this agent reads
   untrusted upstream release notes by design, so the prose constraints
   in the prompt are not a control. `gh api` is gone; the two
   mutating-capable commands are pinned to the matrix issue by NUMBER, so
   a call against any other issue does not match the prefix and is
   denied. `--disallowedTools` now names the file-writing tools and
   `git`/`gh pr`/`gh issue edit`/`gh issue close` explicitly, so a later
   edit that widens the whitelist cannot silently restore them. The
   prompt states the exact command shape the grant permits.

2. Targeting the highest `latest` in a lockstep group was wrong in both
   directions. npm publishes a release one package at a time, so a sweep
   landing mid-publish would have told maintainers to move all four
   packages to a version three of them do not have — and written a
   marker for that version, which would then suppress the real filing
   once the publication completed, so the release would never be tracked
   at all. The target is now the LOWEST `latest` across the group: the
   newest version the whole group has actually reached. That stays
   actionable during a partial publish (on 2.0.0 it files 2.1.0 rather
   than going silent), waits correctly when only the in-flight half is
   ahead, and produces a different marker when the publish completes so
   the real release gets its own issue. The body explains itself when a
   `Latest` in the table sits above the target.

3. A failure after an irreversible issue creation lost the issue. The
   sweep exited before emitting `filed`, so the issue never reached the
   analysis job — and the next night's retry would match its own marker,
   emit `[]`, and leave it permanently unanalyzed with nothing left to
   notice. Each group is now isolated, `filed` is appended the moment an
   issue exists, and the emit happens in a `finally`. The run still fails
   afterwards, loudly, but never at the cost of the record. The analyze
   job's gate changed to match: it consumes `filed` whenever the sweep
   named something, whether or not the sweep job itself went green.

Fixing 3 exposed a second instance of the bug CI caught earlier: the
test helper passed `output: undefined` to suppress the emit, but a
destructuring default fires on `undefined`, so it selected
`process.env.GITHUB_OUTPUT` instead of overriding it. Once the emit moved
into a `finally`, the throwing cases reached it and leaked a real
`filed=[]` into the runner's job-output file. The helper now passes
`null`, which suppresses a default and is falsy. Verified by running the
suite with `GITHUB_OUTPUT` set and confirming no file is created.

Test fixtures that bumped a single package's `latest` were modeling a
partial publication without meaning to, so `groupAt`/`latestAt` now make
"completed lockstep release" and "publication in flight" separate,
named cases. 42 tests in this file, 580 across `scripts/`, green with
the runner's environment set and unset.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019JWUZxLDnPgLQzjBrtk1x9
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-06 09:52:54 -04:00
cliffhall
4b611d70cd fix: stop the sdk-watch tests depending on the runner's environment
CI's `build` job went red on one case, `main refuses to run without a
repository`, which passed locally. The cause is ambient environment, not
the code under test: `main`'s `repo` parameter defaults to
`process.env.GITHUB_REPOSITORY`, which is UNSET on a developer machine
and SET on every Actions runner. Passing `undefined` therefore exercised
that default, so the throw happened locally and did not on CI.

`npm run local:gate` is a strict superset of CI's STAGES but not of its
ambient environment, so nothing local could have caught this. The test
now clears the variable around the assertion and restores it, which
makes both environments test the same thing.

The same defaulting applies to `output`, whose default is
`process.env.GITHUB_OUTPUT` — a real file on a runner, the one the job's
own outputs are read from. Every throwing case now pins it to
`undefined` via a `noAmbientOutput` helper. Nothing is written today
because these cases throw before the emit, but leaving the default in
place means a future case that stops throwing would append `filed=…` to
the live job output instead of failing an assertion.

Verified both ways: the suite is green with the variables unset, and
green with GITHUB_REPOSITORY and GITHUB_OUTPUT set as a runner sets them
— 574/574 across all of `scripts/`, with no file created at the
GITHUB_OUTPUT path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019JWUZxLDnPgLQzjBrtk1x9
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-06 09:31:49 -04:00
cliffhall
80a013c319 feat: watch the MCP SDK nightly and file an upgrade issue (#1063)
Staying abreast of SDK releases — OAuth especially — was a habit rather
than a mechanism. This adds the third of this repo's issue-filing sweeps,
after the monthly dependency refresh (#2229) and the daily Dependabot
alert sweep (#2233), following the same shape as both:

  npm registry -> sweep -> issue -> Opus analysis comment -> maintainer PR

scripts/sdk-watch.mjs compares the @modelcontextprotocol/* packages
installed on v2/main against the registry and files one tracking issue
per upstream that is behind — labeled v2 + chore + dependencies, and
milestoned to the nearest dated open bucket.

Four design points, each verified against this repo first:

  * Two upstreams, two issues. client/core/server/server-legacy ship
    from typescript-sdk in lockstep; ext-apps ships from its own repo.
  * The comparison is against the INSTALLED version, not the declared
    range. ext-apps is `^1.7.4` with 1.7.5 already in the lockfile, so
    comparing the declared string would file an issue for a bump
    `npm install` has already taken.
  * A fifth @modelcontextprotocol/* package added to the root manifest
    and not added to SDK_GROUPS fails the sweep loudly, rather than
    going unwatched behind a green run.
  * No board write and no issue closing. No PROJECT_TOKEN exists in
    this org, so the issue arrives labeled and milestoned for
    /issue-triage to board — as dependency-refresh.mjs already does. A
    further release files its own issue and leaves a supersession
    comment on the older one; closing stays a maintainer act.

The analysis half runs Claude, not Copilot. #1063 sketched "a copilot
agent running Opus", and neither half of that is reachable from a
workflow: assigning copilot-swe-agent produces a pull request — the
artifact #2229/#2233/#2235 removed from this repo — and its model cannot
be selected programmatically, since replaceActorsForAssignable takes no
model parameter. So the analyze job uses anthropics/claude-code-action
with --model claude-opus-5, told to post one comment and denied every
file-writing tool, running on the ANTHROPIC_API_KEY org secret. It runs
only over issues the sweep just created, which holds it to one analysis
per release rather than a near-identical comment every night.

Upstream release notes are untrusted input to that job, so its system
prompt says so and its --allowedTools list is a whitelist with no Edit,
Write or git; the job's token carries contents: read only.

36 tests cover the pure halves directly and main() through an injected
spawn, with the silent-failure cases first: a registry error must not
report a clean sweep, and an already-filed target must not refile.

Closes #1063

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019JWUZxLDnPgLQzjBrtk1x9
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-06 08:58:47 -04:00