diff --git a/CHANGELOG.md b/CHANGELOG.md index 413e60e..1c25602 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -252,8 +252,9 @@ folded into a version section at release time. wrapper and its sudo rule, reverse proxy / Cloudflare tunnel / polling-only ingress with what polling cannot see, smee for a laptop test, log hygiene), two host recipes in `examples/` with wake policies - sized for GitHub's event volume, and `docs/rayforce-house-rules.md` for - the target repository's practices. + sized for GitHub's event volume, and a house-rules note for the first + target repository's practices (removed after 0.1.0; such notes belong + with the host's deployment). - **One server.** `src/server.ts` (`GitHubMcplServer`) joins the seven packages into a single connection, and `src/index.ts` starts it: the MCPL diff --git a/README.md b/README.md index 446f871..c189120 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,13 @@ or several of one owner; one process per owner. rollback deletes the comments created after the checkpoint and restores the bodies of the ones edited after it. Nothing GitHub cannot undo is checkpointed, and nothing is granted that the host did not grant. -- **No merge, ever.** Not in v1, not behind a flag. The base branch of any - pull request the server opens or retargets is checked against an allow-list. +- **No merge, ever.** There is no merge tool to enable, and no flag that + adds one. The base branch of any pull request the server opens or + retargets is checked against an allow-list. -Built on [`@animalabs/mcpl-core`](../mcpl-core-ts); sibling of -[slack-mcpl](../slack-mcpl) and [telegram-mcpl](../telegram-mcpl). +Built on [`@animalabs/mcpl-core`](https://github.com/anima-research/mcpl-core-ts); +sibling of [slack-mcpl](https://github.com/anima-research/slack-mcpl) and +[telegram-mcpl](https://github.com/anima-research/telegram-mcpl). > **Status: v1, run end to end against a live GitHub App.** The server answers a > host's handshake in MCPL or in plain MCP, applies the grant it is given @@ -89,9 +91,9 @@ open it in a browser, paste `app-manifest.json` into the field and submit. ``` -Name it on the GitHub page that follows: `rayforce-clone` is the proposal for -the maintainer's clone. The name becomes the bot login with `[bot]` appended -(`rayforce-clone[bot]`), which is what `GITHUB_BOT_LOGIN` must hold. +Name it on the GitHub page that follows. The name becomes the bot login with +`[bot]` appended (an App named `my-github-agent` posts as +`my-github-agent[bot]`), which is what `GITHUB_BOT_LOGIN` must hold. **b. Catch the code.** GitHub creates the App and redirects your browser to the manifest's `redirect_url` with `?code=…` — a temporary code, good for one @@ -240,9 +242,9 @@ told where Node is instead of looking. | `GITHUB_WEBHOOK_PATH` | no | `/github/webhook` | Path the listener answers on. | | `GITHUB_POLL_INTERVAL_MS` | no | `0` (off) | Polling fallback: issues and PRs, issue comments and review comments updated since each list's cursor, plus the checks on the heads of the open PRs seen. `0` is off; a non-zero value below `60000` is refused. Partial — it cannot see a submitted review, a review request or a push ([what polling misses](docs/deploy.md#option-3--polling-only-no-webhook-at-all)) — and it is not meant to run alongside the webhook. | | `GITHUB_REPOS` | yes | — | Comma-separated `owner/name` allow-list; a write anywhere else is refused by name. Several entries must share one owner (one App installation, one token); a second owner is a second server process. With several, every tool call names its repository. | -| `GITHUB_PR_BASE_ALLOW` | no | empty = no PR target allowed | Branches a pull request may target or be retargeted to. A plain `branch` applies to every repository, `owner/name:branch` to that one: `anima-research/rayforce:dev,main`. | +| `GITHUB_PR_BASE_ALLOW` | no | empty = no PR target allowed | Branches a pull request may target or be retargeted to. A plain `branch` applies to every repository, `owner/name:branch` to that one: `my-org/app:dev,main`. | | `GITHUB_STATE_FILE` | no | unset = in-memory only | Durable JSON: checkpoint lineage, rollback bookkeeping, and the polling state: a cursor for each repository and list (`pollCursors`), `lastPollAt` where a list with none starts, and the rows announced as new (`pollNew`). A list with a cursor of its own starts from it, not from `lastPollAt`. | -| `GITHUB_BOT_LOGIN` | yes | — | The App's bot login, e.g. `rayforce-clone[bot]`. From-self and mention detection depend on it. | +| `GITHUB_BOT_LOGIN` | yes | — | The App's bot login, e.g. `my-github-agent[bot]`. From-self and mention detection depend on it. | | `GITHUB_MAINTAINERS` | no | empty | Comma-separated logins marked `isMaintainer` in metadata and tagged `github:actor=maintainer`. | ### 8. Build and run it @@ -276,10 +278,12 @@ only, who can read the key, and log hygiene, continue in ### Wake policies are not optional -A rayforce-sized repository produces 60–115 events a day, and one wake costs -roughly 100k input tokens. A policy set that wakes on everything is about -10M tokens a day of ambient chatter. Both example recipes therefore skip by -default and wake only on what is addressed to the agent: +A repository as busy as the one this connector was first built for produces +60–115 events a day (pushes, reviews, inline comments, comments, pull +requests, issues), and one wake costs roughly 100k input tokens. A policy set +that wakes on everything is about 10M tokens a day of ambient chatter. Both +example recipes therefore skip by default and wake only on what is addressed +to the agent: **The order is the design**, because the host matches first-rule-wins and `metadataTrue` is an OR-list: one rule that wakes on `isOwnThread` placed @@ -297,7 +301,7 @@ exception next, and the wake rules last: | 6 | `github-new-work` (maintainer clone only) | `github:issue=opened`, `github:pr=opened` | `debounce: 60000` — one wake once the burst settles | | 7 | `github-ambient` | anything else from `github` | `skip`, and `default` is `skip` too | -`test/wp8-policies.test.ts` runs both recipes through connectome-host's own +`test/policies.test.ts` runs both recipes through connectome-host's own `EventGate` matcher and asserts that ordering: a green build on the agent's own pull request is skipped, a broken one wakes it, a bot's pull request neither wakes nor debounces, a human mention wakes it, and no rule is left @@ -340,8 +344,8 @@ Notes worth knowing before you grant them: - `upsert_sticky_comment(thread, marker, body)` finds the agent's own comment containing an HTML marker and edits it in place, else creates it. One - running status comment instead of a tail of them — the same thing - `rayforce-audit` does with ``. + running status comment instead of a tail of them, the pattern CI bots use + with a marker such as ``. - `create_pull_request` and a base change in `edit_pull_request` refuse a base outside `GITHUB_PR_BASE_ALLOW`. Empty means no pull request may be opened. - `github.pr` has no merge tool and no branch deletion. There is nothing to @@ -353,7 +357,7 @@ Notes worth knowing before you grant them: Every event carries RFC-001 core tags, the `github:` namespace, and a flat metadata record the host matches `metadataTrue` against. The per-event detail -lands in `docs/events.md` with work package .4; the vocabulary itself is the +is in [`docs/events.md`](docs/events.md); the vocabulary itself is the contract and is listed here in full. **Core tags** (producer `github`): @@ -388,7 +392,7 @@ contract and is listed here in full. | --- | --- | | `mentioned` | the body mentions the bot | | `isOwnThread` | the bot authored the thread or has commented in it | -| `isReplyToSelf` | the event replies to a comment the bot wrote: a **review** comment whose `in_reply_to_id` is one of the bot's, or a **top-level** comment that immediately follows one of the bot's on the thread (amendment 2b, turn order). Quoting the bot's text does not count in v1 — see `docs/contract-notes.md`. | +| `isReplyToSelf` | the event replies to a comment the bot wrote: a **review** comment whose `in_reply_to_id` is one of the bot's, or a **top-level** comment that immediately follows one of the bot's on the thread (turn order). Quoting the bot's text does not count in v1 — see `docs/contract-notes.md`. | | `isReviewRequested` | the bot was asked for a review | | `isCiFailure` | a check suite on this thread's head failed | | `fromSelf` | the bot itself acted (**skip this first in every policy set**) | @@ -420,8 +424,9 @@ reach metadata and never reach the model. one agent, one context and one prompt, so keep them to repositories whose rules the prompt can state together. - **A base allow-list.** `GITHUB_PR_BASE_ALLOW` is empty by default, which - means no pull request may be opened or retargeted at all. For rayforce it is - `dev`, so the release branch is unreachable even by mistake. + means no pull request may be opened or retargeted at all. Name only the + integration branch (say `dev`) and the release branch is unreachable even + by mistake. - **No merge.** No tool merges, closes a release branch or deletes a branch. - **Bodies from strangers are data, not instructions.** An issue body, a comment, a review and a check's output are text written by other people, @@ -465,20 +470,20 @@ Node 22.12 or newer, ESM, TypeScript. Stdout carries only JSON-RPC: every diagnostic goes through the `Logger`, which writes to stderr. No test in this package reaches the network. -| File | Work package | Role | -| --- | --- | --- | -| `types.ts` | .1 | Every shared type and constant, the channel and thread id helpers, `readConfig`, `Logger`. Read by all, edited by nobody else. | -| `grant.ts`, `errors.ts` | .1 | Capability-grant state, `uses` validation, the §6.7 degradation receipt, the §6.6 error codes. | -| `github-adapter.ts` | .2 | `GitHubAdapter implements GitHubApi` over octokit: App auth, installation-token refresh, rate-limit backoff. The only file that knows octokit exists. | -| `webhook.ts`, `poller.ts` | .3 | Ingress: signature-checked deliveries and repository since-polls become `NormalizedEvent`s; dedup; allow-list. | -| `tags.ts`, `content.ts` | .4 | Pure functions: `tagsFor`, `renderEvent`, `renderThread`, `renderPullRequestSummary`, `renderChecks`, all bounded by `LIMITS`. | -| `channels.ts` | .5 | Channel registry and descriptors, lazy registration, `channels/incoming`, `channels/publish`, typing as a no-op. | -| `feature-sets.ts`, `tools-*.ts` | .6 | Feature-set declarations and the read, issues, pr and review tools. | -| `tools-comments.ts`, `state.ts` | .7 | Comment tools including the sticky upsert; checkpoint lineage and rollback. | -| `server.ts`, `index.ts` | .9 | Handshake, `featureSets/update`, tool routing, the event pipeline, plain-MCP mode. | +| File | Role | +| --- | --- | +| `types.ts` | Every shared type and constant, the channel and thread id helpers, `readConfig`, `Logger`. Read by all, edited by nobody else. | +| `grant.ts`, `errors.ts` | Capability-grant state, `uses` validation, the §6.7 degradation receipt, the §6.6 error codes. | +| `github-adapter.ts` | `GitHubAdapter implements GitHubApi` over octokit: App auth, installation-token refresh, rate-limit backoff. The only file that knows octokit exists. | +| `webhook.ts`, `poller.ts` | Ingress: signature-checked deliveries and repository since-polls become `NormalizedEvent`s; dedup; allow-list. | +| `tags.ts`, `content.ts` | Pure functions: `tagsFor`, `renderEvent`, `renderThread`, `renderPullRequestSummary`, `renderChecks`, all bounded by `LIMITS`. | +| `channels.ts` | Channel registry and descriptors, lazy registration, `channels/incoming`, `channels/publish`, typing as a no-op. | +| `feature-sets.ts`, `tools-*.ts` | Feature-set declarations and the read, issues, pr and review tools. | +| `tools-comments.ts`, `state.ts` | Comment tools including the sticky upsert; checkpoint lineage and rollback. | +| `server.ts`, `index.ts` | Handshake, `featureSets/update`, tool routing, the event pipeline, plain-MCP mode. | Changelog entries land as fragments in [`changelog.d/`](changelog.d/README.md). -Contract decisions taken where TASK-SRHKG.1's contract left a type +Design decisions taken where the original contract left a type under-specified are in [`docs/contract-notes.md`](docs/contract-notes.md). A target repository's own operating notes — branch model, commit style, CI gates, review rounds, issue style — belong with the host's deployment, beside diff --git a/app-manifest.json b/app-manifest.json index 034274e..701fc80 100644 --- a/app-manifest.json +++ b/app-manifest.json @@ -1,5 +1,5 @@ { - "name": "rayforce-clone", + "name": "my-github-agent", "description": "An agent living in one GitHub repository: it wakes on issues, comments, pull requests, reviews and check results, and answers in the thread it was spoken to in. Runs as github-mcpl, an MCPL connector. No merge, no release-branch write.", "url": "https://example.com/github-mcpl", "public": false, diff --git a/changelog.d/multi-repo.changed.md b/changelog.d/multi-repo.changed.md index 5a76176..b3a71fa 100644 --- a/changelog.d/multi-repo.changed.md +++ b/changelog.d/multi-repo.changed.md @@ -5,6 +5,6 @@ are required instead of defaulting, so a call can never land in the wrong one. - `GITHUB_PR_BASE_ALLOW` entries can be scoped: `branch` applies to every - repository, `owner/name:branch` to that one (`anima-research/rayforce:dev,main`). + repository, `owner/name:branch` to that one (`my-org/app:dev,main`). A scoped entry naming a repository outside `GITHUB_REPOS` is refused at startup; a refusal names the branches allowed on that repository. diff --git a/changelog.d/neutral-docs.changed.md b/changelog.d/neutral-docs.changed.md new file mode 100644 index 0000000..1646f15 --- /dev/null +++ b/changelog.d/neutral-docs.changed.md @@ -0,0 +1,10 @@ +- The documentation, the App manifest and the examples describe a general + GitHub connector for any MCPL host: the name of the first target + repository, internal task identifiers and work-package references are gone + from the README, `docs/` and the source and test file headers, apart from + the branch and marker names the end-to-end report's transcript records. + The App's proposed name is `my-github-agent` in the manifest and in the + README's examples, and the links to the sibling packages point at their + GitHub repositories. Test data uses neutral repository and bot names; the + two documentation tests are `test/docs.test.ts` and + `test/policies.test.ts`. diff --git a/changelog.d/recipes-by-role.changed.md b/changelog.d/recipes-by-role.changed.md index c74816b..f93870c 100644 --- a/changelog.d/recipes-by-role.changed.md +++ b/changelog.d/recipes-by-role.changed.md @@ -1,6 +1,6 @@ - The example recipes are named by role, `examples/recipe-maintainer-clone.json` and `examples/recipe-contributor.json`, with a neutral agent name, `UTC` and a prompt that names no repository; the wake policies and their tests - are unchanged. The rayforce house rules left this repository: a target - repository's operating notes belong with the host's deployment, beside its + are unchanged. A target repository's house rules left this repository: + its operating notes belong with the host's deployment, beside its recipe, since the connector enforces none of them. diff --git a/docs/contract-notes.md b/docs/contract-notes.md index b542fdd..e122342 100644 --- a/docs/contract-notes.md +++ b/docs/contract-notes.md @@ -1,9 +1,9 @@ # Contract notes -TASK-SRHKG.1's Contract section is frozen; it does not, however, fix every -type signature. This file records the decisions the .1 skeleton had to take so -that .2 to .8 compile against one surface, and the places where a later work -package may legitimately want to extend its own module instead. +The original design contract is frozen; it does not, however, fix every +type signature. This file records the decisions the shared types had to take so +that every module compiles against one surface, and the places where a later +change may legitimately want to extend its own module instead. One rule throughout: where the contract named something, the name is kept verbatim (`html_url`, `diffHunk`, `isResolved`), even where it mixes snake and @@ -35,11 +35,11 @@ camel case, so a reviewer can map a contract bullet to a type by sight. (`pulls/comments?sort=updated&since=`), returning `PolledIssue` / `PolledComment` rows, each list read by `updated_at` rather than by page offset, three requests a pass and more only to finish a second. The poller - (.3) keeps a cursor for each repository's list in whole seconds + keeps a cursor for each repository's list in whole seconds (`pollCursors`, starting from `lastPollAt` until it has its own), which a read stopped at that bound leaves where it stopped, dedups with the key `::` through `StateStore.seenPollKey` - (ring of `POLL_DEDUP_RING` = 2000 keys, .7), names poll events + (ring of `POLL_DEDUP_RING` = 2000 keys, in state.ts), names poll events `poll:::`, and asks `getCheckRuns` for the heads of the open PRs it saw, at most 20 per poll. These endpoints carry no `X-Poll-Interval`; `MIN_POLL_INTERVAL_MS` is the floor. @@ -57,7 +57,7 @@ camel case, so a reviewer can map a contract bullet to a type by sight. cursor.** Amendment 1 left the polling ingress unable to tell an opened issue from an edited one, so every open issue row arrived as `issue.edited` and a recipe keyed on `github:issue=opened` could never fire on it (found by - the WP10 end-to-end run). A `since` query still carries no action, but it + the first end-to-end run). A `since` query still carries no action, but it does carry `created_at`, and that is enough: `PolledIssue` gains `createdAt` and `normalizePolledIssue(repo, row, receivedAt, since, previousHeadSha?, announced?)` derives the kind from it. @@ -92,7 +92,7 @@ camel case, so a reviewer can map a contract bullet to a type by sight. endpoint models no reply relation at all — an issue comment has no `in_reply_to_id` — so `isReplyToSelf` could only ever be true for a *review* comment, and every human answer to a comment the bot had made on an issue - reached the host as plain `chat:ambient` (also found by the WP10 run). A + reached the host as plain `chat:ambient` (also found by the first end-to-end run). A review comment keeps the exact rule (`inReplyTo ∈ botReviewIds`); a top-level comment gets one derived rule: @@ -152,34 +152,36 @@ The channel id is `github:/#` and carries no `issue | pull` discriminator (GitHub shares one number space between issues and PRs within a repository, so the number alone cannot tell them apart). So `parseChannelId` returns `ParsedChannelId { repo, number }` and the channel -registry in .5 — which registered the channel and therefore knows its kind — -supplies `kind` when it needs a full `ThreadRef`. `channelAddress(thread)` -builds the descriptor address `{ owner, name, number, kind }` the contract -names, from a `ThreadRef` that still has its kind. +registry in channels.ts — which registered the channel and therefore knows its +kind — supplies `kind` when it needs a full `ThreadRef`. +`channelAddress(thread)` builds the descriptor address `{ owner, name, number, +kind }` the contract names, from a `ThreadRef` that still has its kind. ## Types the contract implies but does not name -- **`StateStore`** (types.ts) — `ToolContext` carries `state`, and .6 and .7 - are built in parallel, so the state seam has to exist before .7 writes - `state.ts`. The interface is exactly the contract's state bullet: checkpoint - lineage, the comment ids created and the previous bodies of the comments - edited per checkpoint, the last poll time and the poll dedup ring - (amendment 1), plus an atomic `flush()`. `StateSnapshot` is the on-disk - JSON shape. +- **`StateStore`** (types.ts) — `ToolContext` carries `state`, and the tools + and `state.ts` are built in parallel, so the state seam has to exist before + `state.ts` does. The interface is exactly the contract's state bullet: + checkpoint lineage, the comment ids created and the previous bodies of the + comments edited per checkpoint, the last poll time and the poll dedup ring + (amendment 1), plus an atomic `flush()`. `StateSnapshot` is the on-disk JSON + shape. - **`EventMetadata`, `TagResult`, `TagContext`** (types.ts) — the metadata - bullet enumerates the fields, and both .4 (which produces them) and .5 - (which sends them on `channels/incoming`) need the same type. `TagContext` - carries what tags.ts cannot derive from the event alone: the bot login, the - maintainer list, and the two thread-history facts (`isOwnThread`, + bullet enumerates the fields, and both tags.ts (which produces them) and + channels.ts (which sends them on `channels/incoming`) need the same type. + `TagContext` carries what tags.ts cannot derive from the event alone: the bot + login, the maintainer list, and the two thread-history facts (`isOwnThread`, `isReplyToSelf`) that only the caller can know. - **`CORE_TAGS` and `GITHUB_TAGS`** (types.ts) — the tag vocabulary is - enumerated in the contract and is needed by .4 (emission) and .6 (the - `tagOntology` of the declarations). Keeping the two lists in types.ts is - what stops them from drifting apart in two files owned by two packages. + enumerated in the contract and is needed by tags.ts (emission) and + feature-sets.ts (the `tagOntology` of the declarations). Keeping the two + lists in types.ts is what stops them from drifting apart in two files owned + by two packages. - **`LIMITS` and `bounded()`** (types.ts) — the contract's bounds (comments 4k - chars, lists 50 entries, titles 80 chars) as one constant, so .4, .5 and .6 - cannot each pick their own. `channelLabel()` is in types.ts for the same - reason: it is the contract's label format and its 80-char bound. + chars, lists 50 entries, titles 80 chars) as one constant, so rendering, + channels and the tools cannot each pick their own. `channelLabel()` is in + types.ts for the same reason: it is the contract's label format and its + 80-char bound. - **`errors.ts`** — not in the module map, but `grant.ts` is a copy of slack-mcpl's and that file imports `McplRpcError`. It is slack-mcpl's `errors.ts` verbatim (protocol-generic). @@ -187,7 +189,7 @@ names, from a `ThreadRef` that still has its kind. "api, state, bot login, repo allow-list, logger", but `github.pr` has to check a PR base against `GITHUB_PR_BASE_ALLOW`, and a tool cannot reach the environment. The allow-list is the sixth field rather than a second, - divergent way for .6 to find it. + divergent way for the tools to find it. - **`GITHUB_API_METHODS`** — the method-name list, so the harness can assert `FakeGitHubApi` covers all of them (`as const satisfies readonly (keyof GitHubApi)[]`: adding a method to the interface without the constant is a @@ -227,25 +229,26 @@ The contract names the variables but not every default or requiredness. property (without it the server can answer itself), so it is not something to default. -## Not materialized by .1 +## Not materialized by the first skeleton -> Historical, and all of it has since landed: .2 to .8 wrote the modules and -> .9 wired them, so `server.ts` and `index.ts` are the real server and the +> Historical, and all of it has since landed: the modules are written and +> wired together, so `server.ts` and `index.ts` are the real server and the > real entry point, `feature-sets.ts` exists and `octokit` is used by the > adapter. The list is kept because the *reasons* below are still the reasons > the seams are shaped as they are. The one item still open is the > `changelog.d` fold. -- `octokit` is a declared dependency but is imported nowhere yet; .2 is the - first and only user. +- `octokit` is a declared dependency but is imported nowhere yet; + `github-adapter.ts` is the first and only user. - `server.ts` and `index.ts` are stubs. `serve()` answers every request with `-32601` so a host and the harness can attach and detach; there is no handshake, no manifest, no routing. `index.ts` validates the environment and starts the stub with a `GitHubApi` proxy that throws if touched. - No `feature-sets.ts`: `CapabilityGrant` takes its declarations through the - constructor (as slack-mcpl's does), so .6 can write that file without - touching `grant.ts`. `TOOLS_BY_FEATURE_SET`, `ROLLBACK_CAPABLE` and - `featureSetForTool()` in types.ts are what .6 builds its declarations from. + constructor (as slack-mcpl's does), so `feature-sets.ts` can be written + without touching `grant.ts`. `TOOLS_BY_FEATURE_SET`, `ROLLBACK_CAPABLE` and + `featureSetForTool()` in types.ts are what `feature-sets.ts` builds its + declarations from. - The `changelog.d` fold (`scripts/release-changelog.mjs` and the `version` npm script) is not copied: the package scripts are the five the task names (build, typecheck, test, start, clean). Fragments accumulate until the first diff --git a/docs/deploy.md b/docs/deploy.md index fcfa022..7e27fea 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -245,9 +245,9 @@ contain and nothing else: | a check conclusion on an open pull request's head | `push` with no thread attached, and the distinction between `synchronize` and any other PR update | The two in the second column that matter are the review ones: a repository -whose reviews are the work — rayforce, where the maintainer reviews inline -and requests reviews by hand — needs the webhook. **If the clone must wake on -a submitted review or a review request, polling-only is not enough.** +whose reviews are the work, where maintainers review inline and request +reviews by hand, needs the webhook. **If the agent must wake on a submitted +review or a review request, polling-only is not enough.** Everything else degrades gracefully: the thread update that carries the review still arrives, just later, without the review's own event or tags. @@ -270,12 +270,11 @@ your secret. Do not leave a production installation pointed at smee. ## E2E against a sandbox -Do not rehearse against the real repository. The sandbox for this work is the -private repository `theaspirational/github-mcpl-sandbox`: default branch -`dev`, and a CI workflow that fails whenever a `.fail` file exists in the -tree, which is how a `github:ci=failure` event is produced on demand. Install -the App on that repository alone, set `GITHUB_REPOS` to it, and walk the list -in TASK-SRHKG.1 (an issue arrives as a push event and a `channels/incoming` +Do not rehearse against the real repository. Use a private sandbox with a +default branch of your choice and a CI workflow that fails whenever a `.fail` +file exists in the tree, which is how a `github:ci=failure` event is produced +on demand. Install the App on that repository alone, set `GITHUB_REPOS` to +it, and walk the list (an issue arrives as a push event and a `channels/incoming` with the documented tags; a publish lands as a comment; a review-thread reply lands in its thread; `create_review` posts two inline comments; `upsert_sticky_comment` edits in place on the second call; a rollback deletes diff --git a/docs/e2e-report.md b/docs/e2e-report.md index f060d18..bbbbff8 100644 --- a/docs/e2e-report.md +++ b/docs/e2e-report.md @@ -1,7 +1,8 @@ -# WP10 — end to end on the sandbox +# End to end on a sandbox repository -TASK-SRHKG.1.10. This connector against real GitHub: round 1, the review that -failed it, and the eight fix rounds that closed it. +This connector against real GitHub, before its first release: round 1, the +review that failed it, and the eight fix rounds that closed it; then the +webhook round, a soak across a token expiry and a Linux install. | | | | --- | --- | @@ -558,7 +559,7 @@ two threads, sticky edit-in-place, rollback ordering and the restore of an edit, one `checks.completed` per head sha, the base allow-list on both create and edit, and the dedup ring surviving a restart. -## Open items for the maintainer's installation +## Open items for a production installation - **O1 — closed by round 3 below** (webhook over a tunnel, 2026-10-14 21:17Z–21:31Z). As it stood after round 2: the webhook path is entirely unexercised against real GitHub. No tunnel was available, so `GITHUB_WEBHOOK_SECRET` and the webhook port were @@ -692,8 +693,7 @@ reviewers' evidence directories was copied into the tree. ## Gates -Run through `orgasmic dispatch gate` in the `repos/github-mcpl` checkout, at -the final sha on a clean tree: +Run in the checkout at the final sha on a clean tree: | Gate | Command | Result | | --- | --- | --- | @@ -766,7 +766,7 @@ deliberately unsigned probes it rejected with 401; 1 replay dropped. 21:21:40Z while `.75` and `.198` answered in 0.3 s; from 21:22Z all three answered. GitHub does not retry a timed-out delivery on its own, so each stall is a lost event unless someone redelivers it. The 34 deliveries after - 21:22Z all got 202. For the maintainer's installation this argues for a host + 21:22Z all got 202. For a production installation this argues for a host with a stable public address (deploy option 1) or Cloudflare (option 2) rather than Funnel from a laptop, and for the polling fallback as the safety net when the webhook is the only ingress. diff --git a/docs/events.md b/docs/events.md index 6982289..9fb19d7 100644 --- a/docs/events.md +++ b/docs/events.md @@ -11,7 +11,7 @@ capability grant's decision; a tag only influences treatment afterwards. ## 1. GitHub event → `NormalizedEvent.kind` -| GitHub webhook event (`action`) | Polled source (amendment 1) | `kind` | Thread | +| GitHub webhook event (`action`) | Polled source | `kind` | Thread | |---|---|---|---| | `issues` (`opened`) | `listIssuesSince` | `issue.opened` | issue | | `issues` (`edited`) | `listIssuesSince` | `issue.edited` | issue | diff --git a/scripts/e2e/fake-host.mjs b/scripts/e2e/fake-host.mjs index 423fc48..4641344 100644 --- a/scripts/e2e/fake-host.mjs +++ b/scripts/e2e/fake-host.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * A scripted MCPL host for the WP10 end-to-end run (TASK-SRHKG.1.10). + * A scripted MCPL host for end-to-end runs against a live App. * * It is the host side of `test/harness.ts`, moved off an in-memory stream pair * and onto a child process's stdio: it spawns the built server diff --git a/src/channels.ts b/src/channels.ts index 71abe2b..0662730 100644 --- a/src/channels.ts +++ b/src/channels.ts @@ -1,5 +1,5 @@ /** - * Channels (TASK-SRHKG.1.5) — one MCPL channel per issue or pull request. + * Channels — one MCPL channel per issue or pull request. * * The contract's channel bullets, and nothing beyond them: * @@ -22,7 +22,7 @@ * thread closes or reopens. What the registry reports is what the host * *acknowledged*: a refused or undelivered update leaves the descriptor * where it was and is retried on the channel's next touch. - * - **Incoming.** {@link ChannelRegistry.deliver} takes the outputs of .4 + * - **Incoming.** {@link ChannelRegistry.deliver} takes two outputs * (`tagsFor` → tags + metadata, `renderEvent` → content) and sends one * `channels/incoming` message, with the contract's thread-id rules, after * asking {@link ChannelRegistryOptions.canDeliver} — the registration it @@ -35,9 +35,9 @@ * declared for this server, so nothing here implements them. * * The host connection is reached through {@link HostConnection}, a one-method - * seam this module defines, so .9 can hand in an `McplConnection` and a test - * can hand in a recorder. Stdout belongs to JSON-RPC; every diagnostic goes - * through the {@link Logger}, which writes to stderr. + * seam this module defines, so server.ts can hand in an `McplConnection` and a + * test can hand in a recorder. Stdout belongs to JSON-RPC; every diagnostic + * goes through the {@link Logger}, which writes to stderr. */ import { diff --git a/src/content.ts b/src/content.ts index 697838c..260b3a3 100644 --- a/src/content.ts +++ b/src/content.ts @@ -1,5 +1,5 @@ /** - * Rendering (TASK-SRHKG.1.4; contract section "Tags and metadata" and "Tool + * Rendering (contract section "Tags and metadata" and "Tool * results are text blocks rendered by content.ts, bounded"). * * Pure functions: data in, `ContentBlock[]` out. Everything the model reads diff --git a/src/errors.ts b/src/errors.ts index e5cd58e..d5bf7bb 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -11,7 +11,7 @@ * -32602 Invalid params * * Copied from slack-mcpl's src/errors.ts (protocol-generic; no Slack-specific - * logic). grant.ts needs it, so it lands with the .1 skeleton. + * logic). grant.ts needs it. */ import { ERR_CAPABILITY_DENIED } from '@animalabs/mcpl-core'; diff --git a/src/feature-sets.ts b/src/feature-sets.ts index 684e74d..655797c 100644 --- a/src/feature-sets.ts +++ b/src/feature-sets.ts @@ -1,5 +1,5 @@ /** - * Feature-set declarations — MCPL 0.5 (SPEC §5.1, §6.1, §6.2), TASK-SRHKG.1.6. + * Feature-set declarations — MCPL 0.5 (SPEC §5.1, §6.1, §6.2). * * Six feature sets, exactly as the contract's "Feature sets and tools" section * names them: @@ -21,22 +21,22 @@ * justified by a call site rather than by aspiration: * * tools — every set with tools; the MCP tool surface (§5.1). - * pushEvents — the pipeline sends push/event for every event (.9), - * and it is the *only* path github.events declares: see - * {@link EVENTS_USES}. + * pushEvents — the pipeline sends push/event for every event + * (server.ts), and it is the *only* path github.events + * declares: see {@link EVENTS_USES}. * channels.publish — channels/publish writes a comment or a review reply - * (.5 over the comment API), which is what + * (channels.ts over the comment API), which is what * github.comments authorizes; a host that disables * github.comments must not get a write through publish, * so the path is declared there and not on github.events. * * The other three channel paths this server uses — `channels.register` (the - * registry announces a thread lazily, .5), `channels.lifecycle` (channels/open - * and channels/close) and `channels.incoming` (an event delivered to its - * channel) — are declared by no feature set and checked at their own call - * sites instead. They are capabilities the server *uses when it has them*, not - * capabilities any set needs to function, and §6.4 would otherwise make the - * whole of github.events depend on all of them (WP9 review, finding 4). + * registry announces a thread lazily, channels.ts), `channels.lifecycle` + * (channels/open and channels/close) and `channels.incoming` (an event + * delivered to its channel) — are declared by no feature set and checked at + * their own call sites instead. They are capabilities the server *uses when it + * has them*, not capabilities any set needs to function, and §6.4 would + * otherwise make the whole of github.events depend on all of them. * `channels.typing` is answered as a no-op behind the same per-call check. * * Deliberately NOT declared: channels.streaming (a comment is posted once), diff --git a/src/github-adapter.ts b/src/github-adapter.ts index 36321af..e1d2e17 100644 --- a/src/github-adapter.ts +++ b/src/github-adapter.ts @@ -1,6 +1,6 @@ /** * GitHubAdapter — the only module in the package that touches octokit - * (TASK-SRHKG.1 Contract, module map; work package .2). + * (the contract's module map). * * Three concerns live here and nothing else does: * diff --git a/src/index.ts b/src/index.ts index 4ffbdad..0415b77 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * github-mcpl — CLI entry point (TASK-SRHKG.1.9). + * github-mcpl — CLI entry point. * * Usage: * github-mcpl --stdio # MCP-compatible stdio transport diff --git a/src/normalize.ts b/src/normalize.ts index 5d8a440..4172be0 100644 --- a/src/normalize.ts +++ b/src/normalize.ts @@ -1,5 +1,5 @@ /** - * Ingress normalization (TASK-SRHKG.1.3, §Ingress). + * Ingress normalization (§Ingress). * * One place turns a GitHub delivery into a {@link NormalizedEvent}: a raw * webhook payload (webhook.ts) or a polled row (poller.ts). Everything here diff --git a/src/poller.ts b/src/poller.ts index 00ff107..d00b1b1 100644 --- a/src/poller.ts +++ b/src/poller.ts @@ -1,5 +1,5 @@ /** - * Polling ingress (TASK-SRHKG.1.3, §Ingress, as amended by contract + * Polling ingress (§Ingress, as amended by contract * amendment 1). * * The contract's first draft polled `GET /notifications`; an App installation diff --git a/src/server.ts b/src/server.ts index e748e8d..4f50909 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,5 +1,5 @@ /** - * GitHubMcplServer — the one server the seven packages make (TASK-SRHKG.1.9). + * GitHubMcplServer — the one server every module plugs into. * * What it joins up, in the order a connection meets it: * @@ -24,20 +24,21 @@ * thread as the bot's own when it authored anything there. * - **Channels.** `channels/list`, `channels/open`, `channels/close`, * `channels/publish` (as a Request *or* a Notification, §14.3) and - * `channels/typing` over the `ChannelRegistry` of .5, each behind its own - * capability, and all of them behind GITHUB_REPOS: a channel is a write - * surface, so the allow-list that binds the tools binds it too. - * - **State.** `state/rollback` through `rollbackComments` (.7); an unknown - * or abandoned checkpoint arrives here as a thrown `McplRpcError` and - * leaves as the JSON-RPC error `-32005` (§8.7). - * - **The pipeline.** One `onEvent` for both ingress paths (.3): - * `tagsFor` (.4) → `renderEvent` (.4) → `channels/incoming` (.5) *and* - * `push/event`. The two egress paths are granted separately, so a host - * with no channels still wakes and a host with no push still reads its - * channels; only an event with a thread can be a channel delivery. Each - * path is re-asked immediately before its own send ({@link - * GitHubMcplServer.eventEgressAllowed}), so a reduction the host - * acknowledged while the event was in flight stops it. + * `channels/typing` over the `ChannelRegistry` of channels.ts, each behind + * its own capability, and all of them behind GITHUB_REPOS: a channel is a + * write surface, so the allow-list that binds the tools binds it too. + * - **State.** `state/rollback` through `rollbackComments` + * (tools-comments.ts); an unknown or abandoned checkpoint arrives here as + * a thrown `McplRpcError` and leaves as the JSON-RPC error `-32005` + * (§8.7). + * - **The pipeline.** One `onEvent` for both ingress paths (webhook.ts, + * poller.ts): `tagsFor` (tags.ts) → `renderEvent` (content.ts) → + * `channels/incoming` (channels.ts) *and* `push/event`. The two egress + * paths are granted separately, so a host with no channels still wakes and + * a host with no push still reads its channels; only an event with a + * thread can be a channel delivery. Each path is re-asked immediately + * before its own send ({@link GitHubMcplServer.eventEgressAllowed}), so a + * reduction the host acknowledged while the event was in flight stops it. * * Stdout is the JSON-RPC channel: every diagnostic goes through the Logger, * which writes to stderr only. @@ -1808,7 +1809,7 @@ export class GitHubMcplServer { * * GitHub gives an issue comment no reply relation whatsoever — there is no * `in_reply_to_id` on the issues endpoint — so before this amendment every - * answer to the bot on an issue read as plain `chat:ambient`, which the WP10 + * answer to the bot on an issue read as plain `chat:ambient`, which the first * end-to-end run showed costs the bot every follow-up on its own comment. * * The rule that stands in for it is **turn order**: the last comment on the diff --git a/src/state.ts b/src/state.ts index e973be5..84d874d 100644 --- a/src/state.ts +++ b/src/state.ts @@ -1,5 +1,5 @@ /** - * Durable state and rollback bookkeeping (TASK-SRHKG.1.7). + * Durable state and rollback bookkeeping. * * The contract's state bullet, and SPEC 0.5 §8: * diff --git a/src/tags.ts b/src/tags.ts index b946032..3f9f92c 100644 --- a/src/tags.ts +++ b/src/tags.ts @@ -1,5 +1,5 @@ /** - * Event tags and wake metadata (TASK-SRHKG.1.4; contract section "Tags and + * Event tags and wake metadata (contract section "Tags and * metadata"; MCPL RFC-001). * * Pure functions: `tagsFor(event, ctx)` takes a `NormalizedEvent` and what the diff --git a/src/tool-support.ts b/src/tool-support.ts index 4a13964..3ac8a10 100644 --- a/src/tool-support.ts +++ b/src/tool-support.ts @@ -1,19 +1,18 @@ /** * Tool support — the arguments, refusals, failures and per-field bounds every - * ToolModule shares (TASK-SRHKG.1.9, moved out of tools-read.ts). + * ToolModule shares (moved out of tools-read.ts). * - * .6 put these in `tools-read.ts` because its write scope was those four files - * and both write modules needed them; .7 then grew its own thread parser in - * `tools-comments.ts` because the two packages were built in parallel. WP9 - * reconciles both: the helpers live here, and {@link parseThread} is the one - * parser for every form of "which issue or pull request" — `owner/name#12`, - * `github:owner/name#12`, `#12`, `12`, and `{ owner, name, number, kind }`. + * These first lived in `tools-read.ts`, which both write modules needed, and + * `tools-comments.ts` grew its own thread parser while the two were built in + * parallel. This file reconciles both: the helpers live here, and + * {@link parseThread} is the one parser for every form of "which issue or + * pull request" — `owner/name#12`, `github:owner/name#12`, `#12`, `12`, and + * `{ owner, name, number, kind }`. * * The render* functions stay where they are. `content.ts` renders an *event* * for a channel delivery; these tools render a *result* for the model, and - * only .6's renderers bound the fields the contract's `LIMITS` does not name - * (a login, a branch, a sha, a node id, a path, a URL, an echoed query). See - * the WP9 report for that decision. + * only the tool renderers bound the fields the contract's `LIMITS` does not + * name (a login, a branch, a sha, a node id, a path, a URL, an echoed query). */ import { diff --git a/src/tools-comments.ts b/src/tools-comments.ts index 7813556..39d781d 100644 --- a/src/tools-comments.ts +++ b/src/tools-comments.ts @@ -1,5 +1,5 @@ /** - * `github.comments` — the rollback-capable write tools (TASK-SRHKG.1.7). + * `github.comments` — the rollback-capable write tools. * * The contract's feature-set bullet: `comment`, `reply_in_thread`, * `edit_own_comment`, `delete_own_comment`, `upsert_sticky_comment`. Every one @@ -8,7 +8,7 @@ * {@link rollbackComments} can undo it after a restart as well as before one. * * Two arguments the contract names but does not shape, decided here and - * reported for .9 to reconcile: + * reported for tool-support.ts to reconcile: * * - **`thread`** accepts `{ owner?, name?, number, kind? }` or the string * forms `owner/name#12`, `github:owner/name#12`, `#12`, `12`. Owner and @@ -24,8 +24,8 @@ * `review` names the endpoint; omitted, an id that exists on both is * refused rather than guessed at. * - * Tool results are plain text blocks bounded by {@link LIMITS}; .4's - * `content.ts` does not exist on `main` yet, so nothing here imports it. + * Tool results are plain text blocks bounded by {@link LIMITS}; nothing here + * imports `content.ts`, which renders events rather than tool results. */ import { diff --git a/src/tools-issues-pr.ts b/src/tools-issues-pr.ts index d339c95..48539cd 100644 --- a/src/tools-issues-pr.ts +++ b/src/tools-issues-pr.ts @@ -1,6 +1,5 @@ /** - * github.issues and github.pr — the issue and pull-request write tools - * (TASK-SRHKG.1.6). + * github.issues and github.pr — the issue and pull-request write tools. * * github.issues: create_issue, edit_issue, close_issue, reopen_issue * github.pr: create_pull_request, edit_pull_request, request_review diff --git a/src/tools-read.ts b/src/tools-read.ts index 178aa8d..e4a0ae3 100644 --- a/src/tools-read.ts +++ b/src/tools-read.ts @@ -1,6 +1,6 @@ /** * github.read — the read tools, plus the argument, refusal, failure and - * rendering support the write modules share (TASK-SRHKG.1.6). + * rendering support the write modules share. * * Tools: get_issue, get_pull_request, list_comments, list_review_threads, * list_pull_request_files, get_check_runs, search_issues. None of them writes @@ -8,7 +8,7 @@ * * The shared argument helpers, the allow-list check and the failure formatting * (`runTool`, `ToolRefusal`, `resolveRepo`, the one `parseThread`) moved to - * `tool-support.ts` in WP9; every tool module imports them from there. + * `tool-support.ts`; every tool module imports them from there. * * The bounded renderer below stays. `content.ts` renders an *event* for a * channel delivery; these functions render a *result* for the model, and only diff --git a/src/tools-review.ts b/src/tools-review.ts index c15d60a..349170a 100644 --- a/src/tools-review.ts +++ b/src/tools-review.ts @@ -1,6 +1,5 @@ /** - * github.review — submitting a review and resolving review threads - * (TASK-SRHKG.1.6). + * github.review — submitting a review and resolving review threads. * * create_review — COMMENT | APPROVE | REQUEST_CHANGES, with inline * comments on diff lines. @@ -55,7 +54,7 @@ const DIFF_SIDES: readonly DiffSide[] = ['LEFT', 'RIGHT']; /** A verdict on code is only a verdict when someone else wrote it. */ const VERDICT_EVENTS: readonly ReviewEvent[] = ['APPROVE', 'REQUEST_CHANGES']; -/** GitHub logins are case-insensitive; `rayforce-clone[bot]` is one login. */ +/** GitHub logins are case-insensitive; `My-GitHub-Agent[bot]` and `my-github-agent[bot]` are one login. */ function sameLogin(a: string, b: string): boolean { return a.toLowerCase() === b.toLowerCase(); } diff --git a/src/types.ts b/src/types.ts index 69e1cc8..80c810e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,9 +1,9 @@ /** - * Shared contract for github-mcpl (TASK-SRHKG.1, section Contract). + * Shared contract for github-mcpl. * * Every other module in the map reads this file and none of them edits it, so - * the work packages .2 to .8 can be built in parallel against one import - * surface. The ordering below follows the contract's own bullets: + * the modules can be built in parallel against one import surface. The + * ordering below follows the contract's own bullets: * * 1. Repository refs and thread refs * 2. Normalized events (the ingress → pipeline currency) @@ -26,7 +26,7 @@ import type { ContentBlock } from '@animalabs/mcpl-core'; // 1. Repositories and threads // ───────────────────────────────────────────────────────────────────────────── -/** A repository in the allow-list, e.g. `{ owner: 'anima-research', name: 'rayforce' }`. */ +/** A repository in the allow-list, e.g. `{ owner: 'my-org', name: 'my-repo' }`. */ export interface RepoRef { owner: string; name: string; diff --git a/src/webhook.ts b/src/webhook.ts index 58d560f..63850f9 100644 --- a/src/webhook.ts +++ b/src/webhook.ts @@ -1,5 +1,5 @@ /** - * Webhook ingress (TASK-SRHKG.1.3, §Ingress). + * Webhook ingress (§Ingress). * * A plain `node:http` server on GITHUB_WEBHOOK_PORT and GITHUB_WEBHOOK_PATH. * The order of the checks is the security property, so it is spelled out diff --git a/test/channels.test.ts b/test/channels.test.ts index 025f4b2..ca07164 100644 --- a/test/channels.test.ts +++ b/test/channels.test.ts @@ -1,5 +1,5 @@ /** - * Channels (TASK-SRHKG.1.5): registration, the label bound, incoming shape and + * Channels: registration, the label bound, incoming shape and * thread-id rules, publish routing, refusals, typing. * * Both seams the package declares are exercised for real: @@ -7,9 +7,8 @@ * - the host, over a `McplConnection` pair, so every descriptor and every * incoming message is actually serialized to JSON-RPC and parsed back on * the host side before it is asserted on (the same fake-host wiring - * test/harness.ts uses, kept here because the .1 skeleton's server does - * not route channel methods yet — .9 does that, and `harness()` then - * drives this module through the server); + * test/harness.ts uses, kept here so this module is tested on its own; + * `harness()` drives it through the server); * - GitHub, through the harness's {@link FakeGitHubApi}: no network, and the * call log is what publish routing is asserted against. */ @@ -204,7 +203,7 @@ function metadata(partial: Partial = {}): EventMetadata { kind: 'issue', actor: 'human-one', sha: null, - htmlUrl: 'https://github.com/anima-research/rayforce/issues/7', + htmlUrl: 'https://github.com/anima-research/example-repo/issues/7', ...partial, }; } @@ -218,7 +217,7 @@ function event(partial: Partial = {}): NormalizedEvent { thread: ISSUE, actor: { login: 'human-one', isBot: false }, body: 'ping', - html_url: 'https://github.com/anima-research/rayforce/issues/7#issuecomment-1', + html_url: 'https://github.com/anima-research/example-repo/issues/7#issuecomment-1', ...partial, }; } @@ -244,11 +243,11 @@ test('registration is lazy: nothing is announced until a thread is touched', asy assert.equal(params.channels.length, 1, 'only the touched thread'); const sent = assertValidDescriptor(params.channels[0]); - assert.equal(sent.id, 'github:anima-research/rayforce#7'); + assert.equal(sent.id, 'github:anima-research/example-repo#7'); assert.equal(sent.type, 'github'); assert.equal(sent.direction, 'bidirectional'); - assert.equal(sent.label, 'anima-research/rayforce#7: Thread safety of the scheduler'); - assert.deepEqual(sent.address, { owner: 'anima-research', name: 'rayforce', number: 7, kind: 'issue' }); + assert.equal(sent.label, 'anima-research/example-repo#7: Thread safety of the scheduler'); + assert.deepEqual(sent.address, { owner: 'anima-research', name: 'example-repo', number: 7, kind: 'issue' }); assert.deepEqual(sent.metadata, { kind: 'issue', state: 'open' }); assert.deepEqual(reg.list().map((d) => d.id), [sent.id]); @@ -302,7 +301,7 @@ test("a registration still in flight does not swallow the next touch's title and release(); const [registered, delivered] = await Promise.all([first, second]); - assert.equal(registered?.label, 'anima-research/rayforce#7', 'registered before any title was known'); + assert.equal(registered?.label, 'anima-research/example-repo#7', 'registered before any title was known'); assert.equal(delivered.delivered, true); assert.equal(host.sent(method.CHANNELS_REGISTER).length, 1, 'still announced once'); @@ -310,7 +309,7 @@ test("a registration still in flight does not swallow the next touch's title and assert.equal(changed.length, 1, "the racing event's title and state reach the host"); const updated = assertValidDescriptor((changed[0].params as { updated: unknown[] }).updated[0]); assert.equal(updated.id, id); - assert.equal(updated.label, 'anima-research/rayforce#7: Closed with a title'); + assert.equal(updated.label, 'anima-research/example-repo#7: Closed with a title'); assert.deepEqual(updated.metadata, { kind: 'issue', state: 'closed' }); assert.equal(reg.get(id)?.label, updated.label, 'and the registry holds them too'); @@ -329,7 +328,7 @@ test('the first event registers the channel, with the event title', async () => ); assert.equal(outcome.delivered, true); const params = host.sent(method.CHANNELS_REGISTER)[0].params as ChannelsRegisterParams; - assert.equal(params.channels[0].label, 'anima-research/rayforce#7: Scheduler deadlock'); + assert.equal(params.channels[0].label, 'anima-research/example-repo#7: Scheduler deadlock'); host.close(); }); @@ -338,13 +337,13 @@ test('a tool touch with no title yet labels the channel without one, and the fir const reg = registry(host); await reg.ensureChannel(ISSUE); const registered = (host.sent(method.CHANNELS_REGISTER)[0].params as ChannelsRegisterParams).channels[0]; - assert.equal(registered.label, 'anima-research/rayforce#7', 'no trailing colon for an unknown title'); + assert.equal(registered.label, 'anima-research/example-repo#7', 'no trailing colon for an unknown title'); await reg.deliver(event({ kind: 'issue.edited', title: 'Now it has a title' }), [], metadata(), TEXT); const changed = host.sent(method.CHANNELS_CHANGED); assert.equal(changed.length, 1); const updated = (changed[0].params as { updated: ChannelDescriptor[] }).updated[0]; - assert.equal(updated.label, 'anima-research/rayforce#7: Now it has a title'); + assert.equal(updated.label, 'anima-research/example-repo#7: Now it has a title'); host.close(); }); @@ -374,7 +373,7 @@ test('the label title is bounded to 80 chars, notice included', async () => { await reg.ensureChannel(ISSUE, title); const sent = (host.sent(method.CHANNELS_REGISTER)[0].params as ChannelsRegisterParams).channels[0]; - const prefix = 'anima-research/rayforce#7: '; + const prefix = 'anima-research/example-repo#7: '; assert.ok(sent.label.startsWith(prefix)); const titlePart = sent.label.slice(prefix.length); assert.equal(titlePart.length, LIMITS.titleChars, 'exactly 80 chars of title'); @@ -395,8 +394,8 @@ test('a title change sends channels/changed with the new label', async () => { const changed = host.sent(method.CHANNELS_CHANGED); assert.equal(changed.length, 1); const updated = assertValidDescriptor((changed[0].params as { updated: unknown[] }).updated[0]); - assert.equal(updated.id, 'github:anima-research/rayforce#7'); - assert.equal(updated.label, 'anima-research/rayforce#7: Second title'); + assert.equal(updated.id, 'github:anima-research/example-repo#7'); + assert.equal(updated.label, 'anima-research/example-repo#7: Second title'); assert.deepEqual(updated.metadata, { kind: 'issue', state: 'open' }); assert.equal(reg.get(updated.id)?.label, updated.label, 'the registry holds the new descriptor'); host.close(); @@ -430,14 +429,14 @@ test('a channels/changed the host refuses leaves the acknowledged descriptor in const reg = registry(host); const id = channelId(ISSUE); await reg.ensureChannel(ISSUE, 'First title'); - assert.equal(reg.get(id)?.label, 'anima-research/rayforce#7: First title'); + assert.equal(reg.get(id)?.label, 'anima-research/example-repo#7: First title'); host.refuse.add(id); await reg.ensureChannel(ISSUE, 'Second title'); assert.equal(host.sent(method.CHANNELS_CHANGED).length, 1, 'the update was sent'); assert.equal( reg.get(id)?.label, - 'anima-research/rayforce#7: First title', + 'anima-research/example-repo#7: First title', 'the registry reports what the host acknowledged, not what it was told', ); @@ -448,9 +447,9 @@ test('a channels/changed the host refuses leaves the acknowledged descriptor in assert.equal(changed.length, 2, 'a refused update stays retryable'); assert.equal( (changed[1].params as { updated: ChannelDescriptor[] }).updated[0].label, - 'anima-research/rayforce#7: Second title', + 'anima-research/example-repo#7: Second title', ); - assert.equal(reg.get(id)?.label, 'anima-research/rayforce#7: Second title'); + assert.equal(reg.get(id)?.label, 'anima-research/example-repo#7: Second title'); // Acknowledged now: a further touch owes nothing. await reg.ensureChannel(ISSUE); @@ -468,7 +467,7 @@ test('a channels/changed that errors in transport stays retryable', async () => // A host error on a label update is logged, not thrown: the channel works. await reg.ensureChannel(ISSUE, 'Second title'); assert.equal(host.sent(method.CHANNELS_CHANGED).length, 1); - assert.equal(reg.get(id)?.label, 'anima-research/rayforce#7: First title', 'the cache did not advance'); + assert.equal(reg.get(id)?.label, 'anima-research/example-repo#7: First title', 'the cache did not advance'); host.errorOn.clear(); await reg.ensureChannel(ISSUE); @@ -476,9 +475,9 @@ test('a channels/changed that errors in transport stays retryable', async () => assert.equal(changed.length, 2, 'the update is sent again'); assert.equal( (changed[1].params as { updated: ChannelDescriptor[] }).updated[0].label, - 'anima-research/rayforce#7: Second title', + 'anima-research/example-repo#7: Second title', ); - assert.equal(reg.get(id)?.label, 'anima-research/rayforce#7: Second title'); + assert.equal(reg.get(id)?.label, 'anima-research/example-repo#7: Second title'); host.close(); }); @@ -504,7 +503,7 @@ test('incoming carries the contract shape: sender, timestamp, tags, metadata', a const outcome = await reg.deliver(event({ id: 'delivery-42' }), tags, meta, TEXT); assert.deepEqual(outcome, { delivered: true, - channelId: 'github:anima-research/rayforce#7', + channelId: 'github:anima-research/example-repo#7', messageId: 'delivery-42', threadId: undefined, }); @@ -515,7 +514,7 @@ test('incoming carries the contract shape: sender, timestamp, tags, metadata', a assert.equal(params.messages.length, 1); const message = assertValidIncoming(params.messages[0]); - assert.equal(message.channelId, 'github:anima-research/rayforce#7'); + assert.equal(message.channelId, 'github:anima-research/example-repo#7'); assert.equal(message.messageId, 'delivery-42', 'the event id is the message id'); assert.equal(message.timestamp, '2026-10-14T09:00:00.000Z', 'receivedAt is the timestamp'); assert.deepEqual(message.author, { id: 'human-one', name: 'human-one' }); @@ -620,7 +619,7 @@ test('deliver puts the thread id on the incoming message', async () => { assert.equal(outcome.threadId, 'rt:555'); const params = host.sent(method.CHANNELS_INCOMING)[0].params as ChannelsIncomingParams; assert.equal(params.messages[0].threadId, 'rt:555'); - assert.equal(params.messages[0].channelId, 'github:anima-research/rayforce#12'); + assert.equal(params.messages[0].channelId, 'github:anima-research/example-repo#12'); host.close(); }); @@ -667,7 +666,7 @@ test('publish without a threadId creates a comment and reports the created id', assert.equal(outcome.isReviewComment, false); assert.equal(typeof outcome.commentId, 'number'); assert.equal(outcome.messageId, String(outcome.commentId)); - assert.ok(outcome.htmlUrl?.startsWith('https://github.com/anima-research/rayforce/')); + assert.ok(outcome.htmlUrl?.startsWith('https://github.com/anima-research/example-repo/')); assert.deepEqual(outcome.thread, ISSUE); host.close(); }); @@ -776,7 +775,7 @@ test('publishing to an unknown channel is refused with -32023', async () => { reg.handlePublish( { conversationId: 'c', - channelId: 'github:anima-research/rayforce#999', + channelId: 'github:anima-research/example-repo#999', content: [{ type: 'text', text: 'hi' }], }, api, @@ -861,7 +860,7 @@ test('typing is a no-op that answers success', async () => { const before = host.saw.length; assert.deepEqual(reg.handleTyping({ channelId: channelId(ISSUE) }), {}); - assert.deepEqual(reg.handleTyping({ channelId: 'github:anima-research/rayforce#999' }), {}, 'no refusal either'); + assert.deepEqual(reg.handleTyping({ channelId: 'github:anima-research/example-repo#999' }), {}, 'no refusal either'); assert.equal(host.saw.length, before, 'nothing is written anywhere'); host.close(); }); diff --git a/test/content.test.ts b/test/content.test.ts index a965627..7fdcbea 100644 --- a/test/content.test.ts +++ b/test/content.test.ts @@ -38,10 +38,10 @@ import { renderThread, } from '../src/content.js'; -const REPO: RepoRef = { owner: 'anima-research', name: 'rayforce' }; +const REPO: RepoRef = { owner: 'anima-research', name: 'example-repo' }; const ISSUE: ThreadRef = { repo: REPO, number: 7, kind: 'issue' }; const PULL: ThreadRef = { repo: REPO, number: 12, kind: 'pull' }; -const URL = 'https://github.com/anima-research/rayforce/issues/7'; +const URL = 'https://github.com/anima-research/example-repo/issues/7'; /** One repeated character, so the longest run of it is the longest body shown. */ const FILL = 'z'; @@ -163,8 +163,8 @@ for (const row of RENDER_TABLE) { // The header line: repo#number (or repo, for a threadless event), kind, // actor, link. - if (ev.thread) assert.ok(header.startsWith(`anima-research/rayforce#${ev.thread.number} `), header); - else assert.ok(header.startsWith('anima-research/rayforce '), header); + if (ev.thread) assert.ok(header.startsWith(`anima-research/example-repo#${ev.thread.number} `), header); + else assert.ok(header.startsWith('anima-research/example-repo '), header); assert.ok(header.includes(row.kind), header); assert.ok(header.includes(ev.actor.login), header); assert.ok(header.includes(ev.html_url), header); @@ -430,7 +430,7 @@ function reviewComment( test('renderIssue shows state, author, labels and the body', () => { const text = only(renderIssue(issue({ state: 'closed', stateReason: 'not_planned' }))); - assert.ok(text.includes('anima-research/rayforce#7 issue closed (not_planned)'), text); + assert.ok(text.includes('anima-research/example-repo#7 issue closed (not_planned)'), text); assert.ok(text.includes('Rate limiter drops bursts')); assert.ok(text.includes('author: alice')); assert.ok(text.includes('labels: bug')); @@ -472,7 +472,7 @@ test('renderThread bounds every comment body and caps the list', () => { comment({ id: 100 + i, body: HUGE_BODY }), ); const text = only(renderThread(ISSUE, comments)); - assert.ok(text.includes('anima-research/rayforce#7 issue: 62 comment(s)'), text.split('\n')[0]); + assert.ok(text.includes('anima-research/example-repo#7 issue: 62 comment(s)'), text.split('\n')[0]); assert.ok(longestBody(text) <= LIMITS.commentChars, String(longestBody(text))); assert.equal(text.match(/^#1\d\d by /gm)?.length, LIMITS.listEntries); assert.ok(text.includes(`and 12 more (showing ${LIMITS.listEntries} of 62)`), text.slice(-120)); diff --git a/test/wp8-docs.test.ts b/test/docs.test.ts similarity index 98% rename from test/wp8-docs.test.ts rename to test/docs.test.ts index 459e9d4..89614fc 100644 --- a/test/wp8-docs.test.ts +++ b/test/docs.test.ts @@ -1,8 +1,8 @@ /** - * WP8 operator surface: the manifest, the recipes and the deploy files are + * The operator surface: the manifest, the recipes and the deploy files are * data a reader follows literally, so they are checked like code. * - * Three things this proves, one per acceptance criterion of TASK-SRHKG.1.8: + * Three things this proves, one per acceptance criterion of the operator surface: * 1. app-manifest.json carries GitHub's manifest fields, exactly the five * permissions the tools need, and no OAuth user flow. * 2. Both example recipes parse, and every wake policy references only @@ -202,8 +202,8 @@ test('the operator surface carries no secret material', () => { 'deploy/github-mcpl', 'deploy/capture-manifest-code.mjs', // The tests too: a fixture is the classic place a real key gets pasted. - 'test/wp8-docs.test.ts', - 'test/wp8-policies.test.ts', + 'test/docs.test.ts', + 'test/policies.test.ts', ...RECIPES, ]; // Private keys, GitHub token forms, and an installation id pasted into a URL. @@ -256,7 +256,7 @@ const modeOf = (path: string): string => (statSync(path).mode & 0o777).toString( /** A temp home with ~/.config/github-mcpl as README step 2 leaves it. */ function freshHome(): { home: string; cfg: string } { - const home = mkdtempSync(join(tmpdir(), 'wp8-home-')); + const home = mkdtempSync(join(tmpdir(), 'docs-home-')); const cfg = join(home, '.config/github-mcpl'); mkdirSync(cfg, { recursive: true, mode: 0o700 }); return { home, cfg }; @@ -338,7 +338,7 @@ test('README: the credential split creates both files at mode 600', { const fixture = join(home, 'conversion-fixture.json'); writeFileSync(fixture, JSON.stringify({ id: 987654, - slug: 'rayforce-clone', + slug: 'example-agent', pem, webhook_secret: 'not-a-real-secret', client_id: 'Iv1.unused', @@ -359,7 +359,7 @@ test('README: the credential split creates both files at mode 600', { assert.equal(modeOf(join(cfg, 'webhook-secret')), '600'); assert.ok(!existsSync(join(cfg, 'conversion.json')), 'the response is deleted, key and all'); assert.match(stdout, /^GITHUB_APP_ID=987654$/m); - assert.match(stdout, /^GITHUB_BOT_LOGIN=rayforce-clone\[bot\]$/m); + assert.match(stdout, /^GITHUB_BOT_LOGIN=example-agent\[bot\]$/m); assert.doesNotMatch(stdout, /PLACEHOLDER|not-a-real-secret/, 'no secret is printed to a terminal'); } finally { rmSync(home, { recursive: true, force: true }); @@ -472,7 +472,7 @@ test('README: the checkout exists before a step runs something out of it', () => test('README: an empty host reaches the registration script by following step 1', { skip: !existsSync(root('.git')) && !process.env.CI && 'this copy is not a git checkout, and step 1 clones it', }, () => { - const home = mkdtempSync(join(tmpdir(), 'wp8-host-')); + const home = mkdtempSync(join(tmpdir(), 'docs-host-')); try { const step1 = setupBlocks().find((b) => /git clone/.test(b)); assert.ok(step1, 'step 1 clones'); diff --git a/test/fixtures/check_run.completed.json b/test/fixtures/check_run.completed.json index 16791ee..473e11f 100644 --- a/test/fixtures/check_run.completed.json +++ b/test/fixtures/check_run.completed.json @@ -9,8 +9,8 @@ "conclusion": "failure", "started_at": "2026-10-13T10:11:30Z", "completed_at": "2026-10-13T10:19:40Z", - "html_url": "https://github.com/anima-research/rayforce/runs/2700001234", - "details_url": "https://github.com/anima-research/rayforce/actions/runs/88001", + "html_url": "https://github.com/anima-research/example-repo/runs/2700001234", + "details_url": "https://github.com/anima-research/example-repo/actions/runs/88001", "output": { "title": "3 failed", "summary": "scheduler::tick::drains_once", "annotations_count": 3 }, "check_suite": { "id": 2600000900, "head_branch": "fix-final-batch", "head_sha": "4d7b2e8c1a93f05627de4b1c8a0f3927e5c6d418" }, "pull_requests": [ @@ -23,7 +23,7 @@ ], "app": { "id": 15368, "slug": "github-actions", "name": "GitHub Actions" } }, - "repository": { "id": 100200300, "name": "rayforce", "full_name": "anima-research/rayforce", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/rayforce", "default_branch": "dev" }, - "sender": { "login": "rayforce-clone[bot]", "id": 9001, "type": "Bot" }, + "repository": { "id": 100200300, "name": "example-repo", "full_name": "anima-research/example-repo", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/example-repo", "default_branch": "dev" }, + "sender": { "login": "example-agent[bot]", "id": 9001, "type": "Bot" }, "installation": { "id": 70000001 } } diff --git a/test/fixtures/check_suite.completed.json b/test/fixtures/check_suite.completed.json index 512105e..ace16dc 100644 --- a/test/fixtures/check_suite.completed.json +++ b/test/fixtures/check_suite.completed.json @@ -7,7 +7,7 @@ "head_sha": "4d7b2e8c1a93f05627de4b1c8a0f3927e5c6d418", "status": "completed", "conclusion": "failure", - "url": "https://api.github.com/repos/anima-research/rayforce/check-suites/2600000900", + "url": "https://api.github.com/repos/anima-research/example-repo/check-suites/2600000900", "before": "1f0c9a4d2b7e55c3a81d6e40b9f27c3d5a8e1b02", "after": "4d7b2e8c1a93f05627de4b1c8a0f3927e5c6d418", "latest_check_runs_count": 3, @@ -23,7 +23,7 @@ "created_at": "2026-10-13T10:11:05Z", "updated_at": "2026-10-13T10:19:48Z" }, - "repository": { "id": 100200300, "name": "rayforce", "full_name": "anima-research/rayforce", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/rayforce", "default_branch": "dev" }, - "sender": { "login": "rayforce-clone[bot]", "id": 9001, "type": "Bot" }, + "repository": { "id": 100200300, "name": "example-repo", "full_name": "anima-research/example-repo", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/example-repo", "default_branch": "dev" }, + "sender": { "login": "example-agent[bot]", "id": 9001, "type": "Bot" }, "installation": { "id": 70000001 } } diff --git a/test/fixtures/check_suite.completed.no_pull.json b/test/fixtures/check_suite.completed.no_pull.json index 147570b..a4718fe 100644 --- a/test/fixtures/check_suite.completed.no_pull.json +++ b/test/fixtures/check_suite.completed.no_pull.json @@ -12,7 +12,7 @@ "created_at": "2026-10-13T17:41:00Z", "updated_at": "2026-10-13T17:49:12Z" }, - "repository": { "id": 100200300, "name": "rayforce", "full_name": "anima-research/rayforce", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/rayforce", "default_branch": "dev" }, + "repository": { "id": 100200300, "name": "example-repo", "full_name": "anima-research/example-repo", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/example-repo", "default_branch": "dev" }, "sender": { "login": "octocat", "id": 1001, "type": "User" }, "installation": { "id": 70000001 } } diff --git a/test/fixtures/check_suite.completed.second_suite.json b/test/fixtures/check_suite.completed.second_suite.json index 14dea30..49d9bb8 100644 --- a/test/fixtures/check_suite.completed.second_suite.json +++ b/test/fixtures/check_suite.completed.second_suite.json @@ -21,7 +21,7 @@ "created_at": "2026-10-13T10:11:06Z", "updated_at": "2026-10-13T10:21:30Z" }, - "repository": { "id": 100200300, "name": "rayforce", "full_name": "anima-research/rayforce", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/rayforce", "default_branch": "dev" }, - "sender": { "login": "rayforce-clone[bot]", "id": 9001, "type": "Bot" }, + "repository": { "id": 100200300, "name": "example-repo", "full_name": "anima-research/example-repo", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/example-repo", "default_branch": "dev" }, + "sender": { "login": "example-agent[bot]", "id": 9001, "type": "Bot" }, "installation": { "id": 70000001 } } diff --git a/test/fixtures/check_suite.completed.second_suite_failed.json b/test/fixtures/check_suite.completed.second_suite_failed.json index 481eb6d..6b344c5 100644 --- a/test/fixtures/check_suite.completed.second_suite_failed.json +++ b/test/fixtures/check_suite.completed.second_suite_failed.json @@ -21,7 +21,7 @@ "created_at": "2026-10-13T10:11:07Z", "updated_at": "2026-10-13T10:24:02Z" }, - "repository": { "id": 100200300, "name": "rayforce", "full_name": "anima-research/rayforce", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/rayforce", "default_branch": "dev" }, - "sender": { "login": "rayforce-clone[bot]", "id": 9001, "type": "Bot" }, + "repository": { "id": 100200300, "name": "example-repo", "full_name": "anima-research/example-repo", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/example-repo", "default_branch": "dev" }, + "sender": { "login": "example-agent[bot]", "id": 9001, "type": "Bot" }, "installation": { "id": 70000001 } } diff --git a/test/fixtures/issue_comment.created.json b/test/fixtures/issue_comment.created.json index 571e349..b4a7180 100644 --- a/test/fixtures/issue_comment.created.json +++ b/test/fixtures/issue_comment.created.json @@ -9,8 +9,8 @@ "comments": 1, "created_at": "2026-10-12T09:14:02Z", "updated_at": "2026-10-12T11:30:00Z", - "body": "Steps: run `rayforce bench --queue 4`.", - "html_url": "https://github.com/anima-research/rayforce/issues/41" + "body": "Steps: run `example-repo bench --queue 4`.", + "html_url": "https://github.com/anima-research/example-repo/issues/41" }, "comment": { "id": 2200000007, @@ -18,11 +18,11 @@ "user": { "login": "hubot", "id": 1002, "type": "User" }, "created_at": "2026-10-12T11:30:00Z", "updated_at": "2026-10-12T11:30:00Z", - "body": "@rayforce-clone[bot] can you reproduce this on dev?", - "html_url": "https://github.com/anima-research/rayforce/issues/41#issuecomment-2200000007", - "issue_url": "https://api.github.com/repos/anima-research/rayforce/issues/41" + "body": "@example-agent[bot] can you reproduce this on dev?", + "html_url": "https://github.com/anima-research/example-repo/issues/41#issuecomment-2200000007", + "issue_url": "https://api.github.com/repos/anima-research/example-repo/issues/41" }, - "repository": { "id": 100200300, "name": "rayforce", "full_name": "anima-research/rayforce", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/rayforce", "default_branch": "dev" }, + "repository": { "id": 100200300, "name": "example-repo", "full_name": "anima-research/example-repo", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/example-repo", "default_branch": "dev" }, "sender": { "login": "hubot", "id": 1002, "type": "User" }, "installation": { "id": 70000001, "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uNzAwMDAwMDE=" } } diff --git a/test/fixtures/issue_comment.created.on_pull.json b/test/fixtures/issue_comment.created.on_pull.json index b1e7b3f..395104e 100644 --- a/test/fixtures/issue_comment.created.on_pull.json +++ b/test/fixtures/issue_comment.created.on_pull.json @@ -4,17 +4,17 @@ "id": 2100000055, "number": 58, "title": "fix(scheduler): dispatch the final batch", - "user": { "login": "rayforce-clone[bot]", "id": 9001, "type": "Bot" }, + "user": { "login": "example-agent[bot]", "id": 9001, "type": "Bot" }, "state": "open", "comments": 1, "pull_request": { - "url": "https://api.github.com/repos/anima-research/rayforce/pulls/58", - "html_url": "https://github.com/anima-research/rayforce/pull/58" + "url": "https://api.github.com/repos/anima-research/example-repo/pulls/58", + "html_url": "https://github.com/anima-research/example-repo/pull/58" }, "created_at": "2026-10-13T08:00:00Z", "updated_at": "2026-10-13T09:05:00Z", "body": "Closes #41.", - "html_url": "https://github.com/anima-research/rayforce/pull/58" + "html_url": "https://github.com/anima-research/example-repo/pull/58" }, "comment": { "id": 2200000031, @@ -22,10 +22,10 @@ "created_at": "2026-10-13T09:05:00Z", "updated_at": "2026-10-13T09:05:00Z", "body": "Looks right. One nit in the review.", - "html_url": "https://github.com/anima-research/rayforce/pull/58#issuecomment-2200000031", - "issue_url": "https://api.github.com/repos/anima-research/rayforce/issues/58" + "html_url": "https://github.com/anima-research/example-repo/pull/58#issuecomment-2200000031", + "issue_url": "https://api.github.com/repos/anima-research/example-repo/issues/58" }, - "repository": { "id": 100200300, "name": "rayforce", "full_name": "anima-research/rayforce", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/rayforce", "default_branch": "dev" }, + "repository": { "id": 100200300, "name": "example-repo", "full_name": "anima-research/example-repo", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/example-repo", "default_branch": "dev" }, "sender": { "login": "octocat", "id": 1001, "type": "User" }, "installation": { "id": 70000001, "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uNzAwMDAwMDE=" } } diff --git a/test/fixtures/issues.closed.json b/test/fixtures/issues.closed.json index dc6d4d5..6ad3174 100644 --- a/test/fixtures/issues.closed.json +++ b/test/fixtures/issues.closed.json @@ -12,10 +12,10 @@ "created_at": "2026-10-12T09:14:02Z", "updated_at": "2026-10-13T16:02:55Z", "closed_at": "2026-10-13T16:02:55Z", - "body": "Steps: run `rayforce bench --queue 4`. The final batch is never dispatched.", - "html_url": "https://github.com/anima-research/rayforce/issues/41" + "body": "Steps: run `example-repo bench --queue 4`. The final batch is never dispatched.", + "html_url": "https://github.com/anima-research/example-repo/issues/41" }, - "repository": { "id": 100200300, "name": "rayforce", "full_name": "anima-research/rayforce", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/rayforce", "default_branch": "dev" }, + "repository": { "id": 100200300, "name": "example-repo", "full_name": "anima-research/example-repo", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/example-repo", "default_branch": "dev" }, "sender": { "login": "hubot", "id": 1002, "type": "User" }, "installation": { "id": 70000001, "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uNzAwMDAwMDE=" } } diff --git a/test/fixtures/issues.opened.json b/test/fixtures/issues.opened.json index 722b893..3e9bd7d 100644 --- a/test/fixtures/issues.opened.json +++ b/test/fixtures/issues.opened.json @@ -14,10 +14,10 @@ "created_at": "2026-10-12T09:14:02Z", "updated_at": "2026-10-12T09:14:02Z", "closed_at": null, - "body": "Steps: run `rayforce bench --queue 4`. The final batch is never dispatched.", - "html_url": "https://github.com/anima-research/rayforce/issues/41" + "body": "Steps: run `example-repo bench --queue 4`. The final batch is never dispatched.", + "html_url": "https://github.com/anima-research/example-repo/issues/41" }, - "repository": { "id": 100200300, "name": "rayforce", "full_name": "anima-research/rayforce", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/rayforce", "default_branch": "dev" }, + "repository": { "id": 100200300, "name": "example-repo", "full_name": "anima-research/example-repo", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/example-repo", "default_branch": "dev" }, "sender": { "login": "octocat", "id": 1001, "type": "User" }, "installation": { "id": 70000001, "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uNzAwMDAwMDE=" } } diff --git a/test/fixtures/ping.json b/test/fixtures/ping.json index 4a9e13f..a5d24e8 100644 --- a/test/fixtures/ping.json +++ b/test/fixtures/ping.json @@ -8,7 +8,7 @@ "events": ["issues", "issue_comment", "pull_request", "push"], "config": { "content_type": "json", "insecure_ssl": "0", "url": "https://ingress.example.invalid/github/webhook" } }, - "repository": { "id": 100200300, "name": "rayforce", "full_name": "anima-research/rayforce", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/rayforce", "default_branch": "dev" }, + "repository": { "id": 100200300, "name": "example-repo", "full_name": "anima-research/example-repo", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/example-repo", "default_branch": "dev" }, "sender": { "login": "octocat", "id": 1001, "type": "User" }, "installation": { "id": 70000001 } } diff --git a/test/fixtures/pull_request.closed.json b/test/fixtures/pull_request.closed.json index 433afce..2391e5b 100644 --- a/test/fixtures/pull_request.closed.json +++ b/test/fixtures/pull_request.closed.json @@ -7,7 +7,7 @@ "state": "closed", "title": "fix(scheduler): dispatch the final batch", "body": "Closes #41. Adds a drain check before the tick returns.", - "user": { "login": "rayforce-clone[bot]", "id": 9001, "type": "Bot" }, + "user": { "login": "example-agent[bot]", "id": 9001, "type": "Bot" }, "draft": false, "merged": true, "mergeable": null, @@ -18,9 +18,9 @@ "head": { "ref": "fix-final-batch", "sha": "4d7b2e8c1a93f05627de4b1c8a0f3927e5c6d418" }, "base": { "ref": "dev", "sha": "9ab3c1d7e5f20418b6c93d7a2e1f08c4d6b5a397" }, "requested_reviewers": [], - "html_url": "https://github.com/anima-research/rayforce/pull/58" + "html_url": "https://github.com/anima-research/example-repo/pull/58" }, - "repository": { "id": 100200300, "name": "rayforce", "full_name": "anima-research/rayforce", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/rayforce", "default_branch": "dev" }, + "repository": { "id": 100200300, "name": "example-repo", "full_name": "anima-research/example-repo", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/example-repo", "default_branch": "dev" }, "sender": { "login": "octocat", "id": 1001, "type": "User" }, "installation": { "id": 70000001 } } diff --git a/test/fixtures/pull_request.opened.json b/test/fixtures/pull_request.opened.json index d8d6b0e..3124789 100644 --- a/test/fixtures/pull_request.opened.json +++ b/test/fixtures/pull_request.opened.json @@ -8,7 +8,7 @@ "state": "open", "title": "fix(scheduler): dispatch the final batch", "body": "Closes #41. Adds a drain check before the tick returns.", - "user": { "login": "rayforce-clone[bot]", "id": 9001, "type": "Bot" }, + "user": { "login": "example-agent[bot]", "id": 9001, "type": "Bot" }, "draft": false, "merged": false, "mergeable": true, @@ -19,9 +19,9 @@ "head": { "label": "anima-research:fix-final-batch", "ref": "fix-final-batch", "sha": "1f0c9a4d2b7e55c3a81d6e40b9f27c3d5a8e1b02" }, "base": { "label": "anima-research:dev", "ref": "dev", "sha": "9ab3c1d7e5f20418b6c93d7a2e1f08c4d6b5a397" }, "requested_reviewers": [], - "html_url": "https://github.com/anima-research/rayforce/pull/58" + "html_url": "https://github.com/anima-research/example-repo/pull/58" }, - "repository": { "id": 100200300, "name": "rayforce", "full_name": "anima-research/rayforce", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/rayforce", "default_branch": "dev" }, - "sender": { "login": "rayforce-clone[bot]", "id": 9001, "type": "Bot" }, + "repository": { "id": 100200300, "name": "example-repo", "full_name": "anima-research/example-repo", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/example-repo", "default_branch": "dev" }, + "sender": { "login": "example-agent[bot]", "id": 9001, "type": "Bot" }, "installation": { "id": 70000001 } } diff --git a/test/fixtures/pull_request.ready_for_review.json b/test/fixtures/pull_request.ready_for_review.json index 356a10e..300af79 100644 --- a/test/fixtures/pull_request.ready_for_review.json +++ b/test/fixtures/pull_request.ready_for_review.json @@ -8,7 +8,7 @@ "state": "open", "title": "fix(scheduler): dispatch the final batch", "body": "Closes #41. Adds a drain check before the tick returns.", - "user": { "login": "rayforce-clone[bot]", "id": 9001, "type": "Bot" }, + "user": { "login": "example-agent[bot]", "id": 9001, "type": "Bot" }, "draft": false, "merged": false, "mergeable": true, @@ -19,9 +19,9 @@ "head": { "label": "anima-research:fix-final-batch", "ref": "fix-final-batch", "sha": "1f0c9a4d2b7e55c3a81d6e40b9f27c3d5a8e1b02" }, "base": { "label": "anima-research:dev", "ref": "dev", "sha": "9ab3c1d7e5f20418b6c93d7a2e1f08c4d6b5a397" }, "requested_reviewers": [], - "html_url": "https://github.com/anima-research/rayforce/pull/58" + "html_url": "https://github.com/anima-research/example-repo/pull/58" }, - "repository": { "id": 100200300, "name": "rayforce", "full_name": "anima-research/rayforce", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/rayforce", "default_branch": "dev" }, - "sender": { "login": "rayforce-clone[bot]", "id": 9001, "type": "Bot" }, + "repository": { "id": 100200300, "name": "example-repo", "full_name": "anima-research/example-repo", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/example-repo", "default_branch": "dev" }, + "sender": { "login": "example-agent[bot]", "id": 9001, "type": "Bot" }, "installation": { "id": 70000001 } } diff --git a/test/fixtures/pull_request.review_requested.json b/test/fixtures/pull_request.review_requested.json index a79fc17..21afbf9 100644 --- a/test/fixtures/pull_request.review_requested.json +++ b/test/fixtures/pull_request.review_requested.json @@ -9,7 +9,7 @@ "state": "open", "title": "fix(scheduler): dispatch the final batch", "body": "Closes #41. Adds a drain check before the tick returns.", - "user": { "login": "rayforce-clone[bot]", "id": 9001, "type": "Bot" }, + "user": { "login": "example-agent[bot]", "id": 9001, "type": "Bot" }, "draft": false, "merged": false, "mergeable": true, @@ -20,9 +20,9 @@ "head": { "label": "anima-research:fix-final-batch", "ref": "fix-final-batch", "sha": "1f0c9a4d2b7e55c3a81d6e40b9f27c3d5a8e1b02" }, "base": { "label": "anima-research:dev", "ref": "dev", "sha": "9ab3c1d7e5f20418b6c93d7a2e1f08c4d6b5a397" }, "requested_reviewers": [], - "html_url": "https://github.com/anima-research/rayforce/pull/58" + "html_url": "https://github.com/anima-research/example-repo/pull/58" }, - "repository": { "id": 100200300, "name": "rayforce", "full_name": "anima-research/rayforce", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/rayforce", "default_branch": "dev" }, - "sender": { "login": "rayforce-clone[bot]", "id": 9001, "type": "Bot" }, + "repository": { "id": 100200300, "name": "example-repo", "full_name": "anima-research/example-repo", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/example-repo", "default_branch": "dev" }, + "sender": { "login": "example-agent[bot]", "id": 9001, "type": "Bot" }, "installation": { "id": 70000001 } } diff --git a/test/fixtures/pull_request.synchronize.json b/test/fixtures/pull_request.synchronize.json index 703421a..5ee560a 100644 --- a/test/fixtures/pull_request.synchronize.json +++ b/test/fixtures/pull_request.synchronize.json @@ -9,7 +9,7 @@ "state": "open", "title": "fix(scheduler): dispatch the final batch", "body": "Closes #41. Adds a drain check before the tick returns.", - "user": { "login": "rayforce-clone[bot]", "id": 9001, "type": "Bot" }, + "user": { "login": "example-agent[bot]", "id": 9001, "type": "Bot" }, "draft": false, "merged": false, "mergeable": null, @@ -18,9 +18,9 @@ "head": { "ref": "fix-final-batch", "sha": "4d7b2e8c1a93f05627de4b1c8a0f3927e5c6d418" }, "base": { "ref": "dev", "sha": "9ab3c1d7e5f20418b6c93d7a2e1f08c4d6b5a397" }, "requested_reviewers": [], - "html_url": "https://github.com/anima-research/rayforce/pull/58" + "html_url": "https://github.com/anima-research/example-repo/pull/58" }, - "repository": { "id": 100200300, "name": "rayforce", "full_name": "anima-research/rayforce", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/rayforce", "default_branch": "dev" }, - "sender": { "login": "rayforce-clone[bot]", "id": 9001, "type": "Bot" }, + "repository": { "id": 100200300, "name": "example-repo", "full_name": "anima-research/example-repo", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/example-repo", "default_branch": "dev" }, + "sender": { "login": "example-agent[bot]", "id": 9001, "type": "Bot" }, "installation": { "id": 70000001 } } diff --git a/test/fixtures/pull_request_review.submitted.json b/test/fixtures/pull_request_review.submitted.json index 76f5b5b..1b3120e 100644 --- a/test/fixtures/pull_request_review.submitted.json +++ b/test/fixtures/pull_request_review.submitted.json @@ -8,7 +8,7 @@ "state": "changes_requested", "submitted_at": "2026-10-13T12:20:00Z", "commit_id": "4d7b2e8c1a93f05627de4b1c8a0f3927e5c6d418", - "html_url": "https://github.com/anima-research/rayforce/pull/58#pullrequestreview-2400000012" + "html_url": "https://github.com/anima-research/example-repo/pull/58#pullrequestreview-2400000012" }, "pull_request": { "id": 2300000058, @@ -16,13 +16,13 @@ "state": "open", "title": "fix(scheduler): dispatch the final batch", "body": "Closes #41.", - "user": { "login": "rayforce-clone[bot]", "id": 9001, "type": "Bot" }, + "user": { "login": "example-agent[bot]", "id": 9001, "type": "Bot" }, "draft": false, "head": { "ref": "fix-final-batch", "sha": "4d7b2e8c1a93f05627de4b1c8a0f3927e5c6d418" }, "base": { "ref": "dev", "sha": "9ab3c1d7e5f20418b6c93d7a2e1f08c4d6b5a397" }, - "html_url": "https://github.com/anima-research/rayforce/pull/58" + "html_url": "https://github.com/anima-research/example-repo/pull/58" }, - "repository": { "id": 100200300, "name": "rayforce", "full_name": "anima-research/rayforce", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/rayforce", "default_branch": "dev" }, + "repository": { "id": 100200300, "name": "example-repo", "full_name": "anima-research/example-repo", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/example-repo", "default_branch": "dev" }, "sender": { "login": "octocat", "id": 1001, "type": "User" }, "installation": { "id": 70000001 } } diff --git a/test/fixtures/pull_request_review.submitted.stale_commit.json b/test/fixtures/pull_request_review.submitted.stale_commit.json index 88e1934..f5c2269 100644 --- a/test/fixtures/pull_request_review.submitted.stale_commit.json +++ b/test/fixtures/pull_request_review.submitted.stale_commit.json @@ -7,7 +7,7 @@ "state": "approved", "submitted_at": "2026-10-13T11:00:00Z", "commit_id": "1f0c9a4d2b7e55c3a81d6e40b9f27c3d5a8e1b02", - "html_url": "https://github.com/anima-research/rayforce/pull/58#pullrequestreview-2400000044" + "html_url": "https://github.com/anima-research/example-repo/pull/58#pullrequestreview-2400000044" }, "pull_request": { "id": 2300000058, @@ -15,13 +15,13 @@ "state": "open", "title": "fix(scheduler): dispatch the final batch", "body": "Closes #41.", - "user": { "login": "rayforce-clone[bot]", "id": 9001, "type": "Bot" }, + "user": { "login": "example-agent[bot]", "id": 9001, "type": "Bot" }, "draft": false, "head": { "ref": "fix-final-batch", "sha": "4d7b2e8c1a93f05627de4b1c8a0f3927e5c6d418" }, "base": { "ref": "dev", "sha": "9ab3c1d7e5f20418b6c93d7a2e1f08c4d6b5a397" }, - "html_url": "https://github.com/anima-research/rayforce/pull/58" + "html_url": "https://github.com/anima-research/example-repo/pull/58" }, - "repository": { "id": 100200300, "name": "rayforce", "full_name": "anima-research/rayforce", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/rayforce", "default_branch": "dev" }, + "repository": { "id": 100200300, "name": "example-repo", "full_name": "anima-research/example-repo", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/example-repo", "default_branch": "dev" }, "sender": { "login": "octocat", "id": 1001, "type": "User" }, "installation": { "id": 70000001 } } diff --git a/test/fixtures/pull_request_review_comment.created.json b/test/fixtures/pull_request_review_comment.created.json index 8ab5875..cbd5c77 100644 --- a/test/fixtures/pull_request_review_comment.created.json +++ b/test/fixtures/pull_request_review_comment.created.json @@ -18,19 +18,19 @@ "in_reply_to_id": null, "created_at": "2026-10-13T12:20:01Z", "updated_at": "2026-10-13T12:20:01Z", - "html_url": "https://github.com/anima-research/rayforce/pull/58#discussion_r2500000077" + "html_url": "https://github.com/anima-research/example-repo/pull/58#discussion_r2500000077" }, "pull_request": { "id": 2300000058, "number": 58, "state": "open", "title": "fix(scheduler): dispatch the final batch", - "user": { "login": "rayforce-clone[bot]", "id": 9001, "type": "Bot" }, + "user": { "login": "example-agent[bot]", "id": 9001, "type": "Bot" }, "head": { "ref": "fix-final-batch", "sha": "4d7b2e8c1a93f05627de4b1c8a0f3927e5c6d418" }, "base": { "ref": "dev", "sha": "9ab3c1d7e5f20418b6c93d7a2e1f08c4d6b5a397" }, - "html_url": "https://github.com/anima-research/rayforce/pull/58" + "html_url": "https://github.com/anima-research/example-repo/pull/58" }, - "repository": { "id": 100200300, "name": "rayforce", "full_name": "anima-research/rayforce", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/rayforce", "default_branch": "dev" }, + "repository": { "id": 100200300, "name": "example-repo", "full_name": "anima-research/example-repo", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/example-repo", "default_branch": "dev" }, "sender": { "login": "octocat", "id": 1001, "type": "User" }, "installation": { "id": 70000001 } } diff --git a/test/fixtures/pull_request_review_comment.created.reply.json b/test/fixtures/pull_request_review_comment.created.reply.json index 494b02f..485401e 100644 --- a/test/fixtures/pull_request_review_comment.created.reply.json +++ b/test/fixtures/pull_request_review_comment.created.reply.json @@ -3,7 +3,7 @@ "comment": { "id": 2500000078, "pull_request_review_id": 2400000019, - "user": { "login": "rayforce-clone[bot]", "id": 9001, "type": "Bot" }, + "user": { "login": "example-agent[bot]", "id": 9001, "type": "Bot" }, "body": "Guarded on `pending.is_empty()` now.", "path": "src/scheduler/tick.rs", "line": 118, @@ -14,19 +14,19 @@ "in_reply_to_id": 2500000077, "created_at": "2026-10-13T13:02:00Z", "updated_at": "2026-10-13T13:02:00Z", - "html_url": "https://github.com/anima-research/rayforce/pull/58#discussion_r2500000078" + "html_url": "https://github.com/anima-research/example-repo/pull/58#discussion_r2500000078" }, "pull_request": { "id": 2300000058, "number": 58, "state": "open", "title": "fix(scheduler): dispatch the final batch", - "user": { "login": "rayforce-clone[bot]", "id": 9001, "type": "Bot" }, + "user": { "login": "example-agent[bot]", "id": 9001, "type": "Bot" }, "head": { "ref": "fix-final-batch", "sha": "4d7b2e8c1a93f05627de4b1c8a0f3927e5c6d418" }, "base": { "ref": "dev", "sha": "9ab3c1d7e5f20418b6c93d7a2e1f08c4d6b5a397" }, - "html_url": "https://github.com/anima-research/rayforce/pull/58" + "html_url": "https://github.com/anima-research/example-repo/pull/58" }, - "repository": { "id": 100200300, "name": "rayforce", "full_name": "anima-research/rayforce", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/rayforce", "default_branch": "dev" }, - "sender": { "login": "rayforce-clone[bot]", "id": 9001, "type": "Bot" }, + "repository": { "id": 100200300, "name": "example-repo", "full_name": "anima-research/example-repo", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/example-repo", "default_branch": "dev" }, + "sender": { "login": "example-agent[bot]", "id": 9001, "type": "Bot" }, "installation": { "id": 70000001 } } diff --git a/test/fixtures/push.json b/test/fixtures/push.json index 985c091..987fcd8 100644 --- a/test/fixtures/push.json +++ b/test/fixtures/push.json @@ -5,13 +5,13 @@ "created": false, "deleted": false, "forced": false, - "compare": "https://github.com/anima-research/rayforce/compare/1f0c9a4d2b7e...4d7b2e8c1a93", + "compare": "https://github.com/anima-research/example-repo/compare/1f0c9a4d2b7e...4d7b2e8c1a93", "commits": [ { "id": "4d7b2e8c1a93f05627de4b1c8a0f3927e5c6d418", "message": "fix(scheduler): guard the drain on an empty queue", "timestamp": "2026-10-13T10:10:50Z", - "author": { "name": "Rayforce Clone", "email": "clone@example.invalid", "username": "rayforce-clone[bot]" }, + "author": { "name": "Example Agent", "email": "clone@example.invalid", "username": "example-agent[bot]" }, "added": [], "removed": [], "modified": ["src/scheduler/tick.rs"] @@ -22,8 +22,8 @@ "message": "fix(scheduler): guard the drain on an empty queue", "timestamp": "2026-10-13T10:10:50Z" }, - "pusher": { "name": "rayforce-clone[bot]", "email": "clone@example.invalid" }, - "repository": { "id": 100200300, "name": "rayforce", "full_name": "anima-research/rayforce", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/rayforce", "default_branch": "dev" }, - "sender": { "login": "rayforce-clone[bot]", "id": 9001, "type": "Bot" }, + "pusher": { "name": "example-agent[bot]", "email": "clone@example.invalid" }, + "repository": { "id": 100200300, "name": "example-repo", "full_name": "anima-research/example-repo", "private": true, "owner": { "login": "anima-research", "id": 4001, "type": "Organization" }, "html_url": "https://github.com/anima-research/example-repo", "default_branch": "dev" }, + "sender": { "login": "example-agent[bot]", "id": 9001, "type": "Bot" }, "installation": { "id": 70000001 } } diff --git a/test/githubAdapter.test.ts b/test/githubAdapter.test.ts index 2660111..7a4be54 100644 --- a/test/githubAdapter.test.ts +++ b/test/githubAdapter.test.ts @@ -1,5 +1,5 @@ /** - * GitHubAdapter against a local fake GitHub (TASK-SRHKG.1.2). + * GitHubAdapter against a local fake GitHub. * * Nothing here leaves the process: the adapter is pointed at a `node:http` * server on 127.0.0.1 that answers the endpoints the contract names, and the @@ -46,7 +46,7 @@ import { // Fixtures // ───────────────────────────────────────────────────────────────────────────── -const REPO: RepoRef = { owner: 'anima-research', name: 'rayforce' }; +const REPO: RepoRef = { owner: 'anima-research', name: 'example-repo' }; const ISSUE: ThreadRef = { repo: REPO, number: 7, kind: 'issue' }; const PULL: ThreadRef = { repo: REPO, number: 12, kind: 'pull' }; const HEAD_SHA = 'f00dcafe00000000000000000000000000000000'; @@ -71,8 +71,8 @@ function config(extra: Record = {}): Config { GITHUB_APP_ID: APP_ID, GITHUB_APP_PRIVATE_KEY_FILE: KEY_FILE, GITHUB_APP_INSTALLATION_ID: String(INSTALLATION_ID), - GITHUB_REPOS: 'anima-research/rayforce', - GITHUB_BOT_LOGIN: 'rayforce-clone[bot]', + GITHUB_REPOS: 'anima-research/example-repo', + GITHUB_BOT_LOGIN: 'example-agent[bot]', GITHUB_PR_BASE_ALLOW: 'dev', ...extra, }); @@ -177,7 +177,7 @@ const ISSUE_PAYLOAD = { labels: [{ name: 'bug' }, 'chore'], created_at: '2026-10-01T00:00:00Z', updated_at: '2026-10-02T00:00:00Z', - html_url: 'https://github.com/anima-research/rayforce/issues/7', + html_url: 'https://github.com/anima-research/example-repo/issues/7', }; const PULL_PAYLOAD = { @@ -191,11 +191,11 @@ const PULL_PAYLOAD = { mergeable: true, head: { ref: 'fix/thing', sha: HEAD_SHA }, base: { ref: 'dev', sha: 'babe0000' }, - user: { login: 'rayforce-clone[bot]', type: 'Bot' }, + user: { login: 'example-agent[bot]', type: 'Bot' }, requested_reviewers: [{ login: 'maintainer', type: 'User' }], created_at: '2026-10-01T00:00:00Z', updated_at: '2026-10-02T00:00:00Z', - html_url: 'https://github.com/anima-research/rayforce/pull/12', + html_url: 'https://github.com/anima-research/example-repo/pull/12', }; const COMMENT_PAYLOAD = { @@ -204,19 +204,19 @@ const COMMENT_PAYLOAD = { body: 'hello', created_at: '2026-10-01T00:00:00Z', updated_at: '2026-10-01T00:00:00Z', - html_url: 'https://github.com/anima-research/rayforce/issues/7#issuecomment-555', + html_url: 'https://github.com/anima-research/example-repo/issues/7#issuecomment-555', }; /** A fake with every endpoint the contract's methods touch already answered. */ function defaultRoutes(fake: FakeGitHub, tokenExpiresInMs = 3_600_000, token = TOKEN): FakeGitHub { - const r = '/repos/anima-research/rayforce'; + const r = '/repos/anima-research/example-repo'; return fake .on(`POST /app/installations/${INSTALLATION_ID}/access_tokens`, () => ({ status: 201, body: { token, expires_at: new Date(Date.now() + tokenExpiresInMs).toISOString() }, })) .on(`GET ${r}/installation`, { body: { id: INSTALLATION_ID } }) - .on('GET /app', { body: { id: 777, slug: 'rayforce-clone', name: 'Rayforce Clone' } }) + .on('GET /app', { body: { id: 777, slug: 'example-agent', name: 'Example Agent' } }) .on(`GET ${r}/issues/7`, { body: ISSUE_PAYLOAD }) .on(`POST ${r}/issues`, { status: 201, body: ISSUE_PAYLOAD }) .on(`PATCH ${r}/issues/7`, { body: { ...ISSUE_PAYLOAD, state: 'closed', state_reason: 'not_planned' } }) @@ -362,7 +362,7 @@ test('mints an App JWT the private key verifies, and swaps it for an installatio 'the JWT signature does not verify against the App key', ); - assert.equal(r.fake.for('GET /repos/anima-research/rayforce/issues/7')[0].headers.authorization, `token ${TOKEN}`); + assert.equal(r.fake.for('GET /repos/anima-research/example-repo/issues/7')[0].headers.authorization, `token ${TOKEN}`); assertNoTokenLeak(r); }); @@ -396,32 +396,32 @@ test('discovers the installation from GITHUB_REPOS when GITHUB_APP_INSTALLATION_ const r = await rig({ env: { GITHUB_APP_INSTALLATION_ID: undefined } }); await r.adapter.getIssue(ISSUE); - const discovery = r.fake.for('GET /repos/anima-research/rayforce/installation'); + const discovery = r.fake.for('GET /repos/anima-research/example-repo/installation'); assert.equal(discovery.length, 1); assert.ok(discovery[0].headers.authorization.startsWith('Bearer '), 'discovery is JWT-authenticated'); // Discovered once, then reused: the second call does not look it up again. await r.adapter.getIssue(ISSUE); - assert.equal(r.fake.for('GET /repos/anima-research/rayforce/installation').length, 1); + assert.equal(r.fake.for('GET /repos/anima-research/example-repo/installation').length, 1); assert.equal(r.fake.for(`POST /app/installations/${INSTALLATION_ID}/access_tokens`).length, 1); assertNoTokenLeak(r); }); test('a 401 on a call re-mints the token once and retries', async () => { const r = await rig(); - r.fake.on('GET /repos/anima-research/rayforce/issues/7', (_req, nth) => + r.fake.on('GET /repos/anima-research/example-repo/issues/7', (_req, nth) => nth === 0 ? { status: 401, body: { message: 'Bad credentials' } } : { body: ISSUE_PAYLOAD }, ); const issue = await r.adapter.getIssue(ISSUE); assert.equal(issue.number, 7); - assert.equal(r.fake.for('GET /repos/anima-research/rayforce/issues/7').length, 2); + assert.equal(r.fake.for('GET /repos/anima-research/example-repo/issues/7').length, 2); assert.equal(r.fake.for(`POST /app/installations/${INSTALLATION_ID}/access_tokens`).length, 2); assertNoTokenLeak(r); }); test('a second 401 is not retried again; it surfaces as a 401 error', async () => { const r = await rig(); - r.fake.on('GET /repos/anima-research/rayforce/issues/7', { status: 401, body: { message: 'Bad credentials' } }); + r.fake.on('GET /repos/anima-research/example-repo/issues/7', { status: 401, body: { message: 'Bad credentials' } }); const err = await r.adapter.getIssue(ISSUE).then( () => null, @@ -430,7 +430,7 @@ test('a second 401 is not retried again; it surfaces as a 401 error', async () = assert.ok(err instanceof GitHubApiError); assert.equal(err.status, 401); assert.equal(err.githubMessage, 'Bad credentials'); - assert.equal(r.fake.for('GET /repos/anima-research/rayforce/issues/7').length, 2); + assert.equal(r.fake.for('GET /repos/anima-research/example-repo/issues/7').length, 2); }); // ───────────────────────────────────────────────────────────────────────────── @@ -439,7 +439,7 @@ test('a second 401 is not retried again; it surfaces as a 401 error', async () = test('a 403 secondary rate limit is retried once, after the Retry-After it names', async () => { const r = await rig(); - r.fake.on('POST /repos/anima-research/rayforce/issues/7/comments', (_req, nth) => + r.fake.on('POST /repos/anima-research/example-repo/issues/7/comments', (_req, nth) => nth === 0 ? { status: 403, @@ -452,14 +452,14 @@ test('a 403 secondary rate limit is retried once, after the Retry-After it names const comment = await r.adapter.createComment(ISSUE, 'hello'); assert.equal(comment.id, 555); assert.deepEqual(r.sleeps, [3000]); - assert.equal(r.fake.for('POST /repos/anima-research/rayforce/issues/7/comments').length, 2); + assert.equal(r.fake.for('POST /repos/anima-research/example-repo/issues/7/comments').length, 2); assertNoTokenLeak(r); }); test('the secondary-limit retry is spent once; a second 403 is the caller’s error', async () => { const r = await rig(); const message = 'You have exceeded a secondary rate limit.'; - r.fake.on('POST /repos/anima-research/rayforce/issues/7/comments', { + r.fake.on('POST /repos/anima-research/example-repo/issues/7/comments', { status: 403, headers: { 'retry-after': '1' }, body: { message }, @@ -473,14 +473,14 @@ test('the secondary-limit retry is spent once; a second 403 is the caller’s er assert.equal(err.status, 403); assert.equal(err.githubMessage, message); assert.deepEqual(r.sleeps, [1000], 'exactly one bounded retry'); - assert.equal(r.fake.for('POST /repos/anima-research/rayforce/issues/7/comments').length, 2); + assert.equal(r.fake.for('POST /repos/anima-research/example-repo/issues/7/comments').length, 2); }); test('a 403 with no Retry-After but a spent budget waits for the reset, bounded', async () => { const now = Date.now(); const r = await rig(); r.setNow(now); - r.fake.on('GET /repos/anima-research/rayforce/issues/7', (_req, nth) => + r.fake.on('GET /repos/anima-research/example-repo/issues/7', (_req, nth) => nth === 0 ? { status: 403, @@ -502,7 +502,7 @@ test('a reset further out than the backoff bound is not slept on at all', async const now = Date.now(); const r = await rig(); r.setNow(now); - r.fake.on('GET /repos/anima-research/rayforce/issues/7', { + r.fake.on('GET /repos/anima-research/example-repo/issues/7', { status: 403, headers: { 'x-ratelimit-remaining': '0', @@ -513,16 +513,16 @@ test('a reset further out than the backoff bound is not slept on at all', async await assert.rejects(r.adapter.getIssue(ISSUE), (e: unknown) => e instanceof GitHubApiError && e.status === 403); assert.deepEqual(r.sleeps, [], 'an hour-long reset is reported, not slept through'); - assert.equal(r.fake.for('GET /repos/anima-research/rayforce/issues/7').length, 1); + assert.equal(r.fake.for('GET /repos/anima-research/example-repo/issues/7').length, 1); }); test('octokit’s own retry plugin is off, so a 5xx is one request and the caller’s error', async () => { const r = await rig(); - r.fake.on('GET /repos/anima-research/rayforce/issues/7', { status: 500, body: { message: 'Server Error' } }); + r.fake.on('GET /repos/anima-research/example-repo/issues/7', { status: 500, body: { message: 'Server Error' } }); await assert.rejects(r.adapter.getIssue(ISSUE), (e: unknown) => e instanceof GitHubApiError && e.status === 500); assert.equal( - r.fake.for('GET /repos/anima-research/rayforce/issues/7').length, + r.fake.for('GET /repos/anima-research/example-repo/issues/7').length, 1, 'a bundled plugin retrying underneath would make backoff unbounded and invisible', ); @@ -531,7 +531,7 @@ test('octokit’s own retry plugin is off, so a 5xx is one request and the calle test('x-ratelimit-remaining is read off every answer and a low budget is warned about', async () => { const r = await rig(); - r.fake.on('GET /repos/anima-research/rayforce/issues/7', { + r.fake.on('GET /repos/anima-research/example-repo/issues/7', { headers: { 'x-ratelimit-remaining': '3', 'x-ratelimit-reset': '1800000000' }, body: ISSUE_PAYLOAD, }); @@ -551,7 +551,7 @@ test('x-ratelimit-remaining is read off every answer and a low budget is warned test('an error carries the status and GitHub’s message, and no token, URL or header', async () => { const r = await rig(); - r.fake.on('GET /repos/anima-research/rayforce/issues/7', { + r.fake.on('GET /repos/anima-research/example-repo/issues/7', { status: 404, body: { message: 'Not Found', documentation_url: 'https://docs.github.com/rest' }, }); @@ -575,10 +575,10 @@ test('an error carries the status and GitHub’s message, and no token, URL or h test('no log line from a full session carries the token, however the calls went', async () => { const r = await rig({ tokenExpiresInMs: 30_000 }); - r.fake.on('GET /repos/anima-research/rayforce/pulls/12', (_req, nth) => + r.fake.on('GET /repos/anima-research/example-repo/pulls/12', (_req, nth) => nth === 0 ? { status: 401, body: { message: 'Bad credentials' } } : { body: PULL_PAYLOAD }, ); - r.fake.on('POST /repos/anima-research/rayforce/issues/7/comments', (_req, nth) => + r.fake.on('POST /repos/anima-research/example-repo/issues/7/comments', (_req, nth) => nth === 0 ? { status: 403, headers: { 'retry-after': '1' }, body: { message: 'secondary rate limit' } } : { status: 201, body: COMMENT_PAYLOAD }, @@ -629,7 +629,7 @@ test('listReviewThreads maps reviewThreads, isResolved and the comment chain', a }, { databaseId: 101, - author: { login: 'rayforce-clone', __typename: 'Bot' }, + author: { login: 'example-agent', __typename: 'Bot' }, body: 'because', createdAt: '2026-10-01T01:00:00Z', lastEditedAt: '2026-10-01T02:00:00Z', @@ -674,7 +674,7 @@ test('listReviewThreads maps reviewThreads, isResolved and the comment chain', a }, { id: 101, - author: { login: 'rayforce-clone', isBot: true }, + author: { login: 'example-agent', isBot: true }, body: 'because', createdAt: '2026-10-01T01:00:00Z', updatedAt: '2026-10-01T02:00:00Z', @@ -694,7 +694,7 @@ test('listReviewThreads maps reviewThreads, isResolved and the comment chain', a assert.match(String(sent.body.query), /isResolved/); assert.deepEqual(sent.body.variables, { owner: 'anima-research', - name: 'rayforce', + name: 'example-repo', number: 12, threads: 50, comments: 50, @@ -749,7 +749,7 @@ test('reads hit the documented REST paths and map the payloads', async () => { labels: ['bug', 'chore'], createdAt: '2026-10-01T00:00:00Z', updatedAt: '2026-10-02T00:00:00Z', - html_url: 'https://github.com/anima-research/rayforce/issues/7', + html_url: 'https://github.com/anima-research/example-repo/issues/7', }); const pull = await r.adapter.getPullRequest(PULL); @@ -758,7 +758,7 @@ test('reads hit the documented REST paths and map the payloads', async () => { assert.deepEqual(pull.base, { ref: 'dev', sha: 'babe0000' }); assert.equal(pull.draft, false); assert.equal(pull.mergeable, true); - assert.deepEqual(pull.author, { login: 'rayforce-clone[bot]', isBot: true }); + assert.deepEqual(pull.author, { login: 'example-agent[bot]', isBot: true }); assert.deepEqual(pull.requestedReviewers, ['maintainer']); assert.equal(pull.checksSummary, null, 'no check runs on the head sha is null, not an empty summary'); @@ -769,12 +769,12 @@ test('reads hit the documented REST paths and map the payloads', async () => { const comments = await r.adapter.listComments(ISSUE, '2026-10-01T00:00:00Z'); assert.equal(comments.length, 1); assert.equal(comments[0].id, 555); - const commentCall = r.fake.for('GET /repos/anima-research/rayforce/issues/7/comments')[0]; + const commentCall = r.fake.for('GET /repos/anima-research/example-repo/issues/7/comments')[0]; assert.equal(commentCall.query.since, '2026-10-01T00:00:00Z'); assert.equal(commentCall.query.per_page, '50', 'lists are capped at 50'); const bot = await r.adapter.getAuthenticatedBot(); - assert.deepEqual(bot, { login: 'rayforce-clone[bot]', id: 777, isBot: true }); + assert.deepEqual(bot, { login: 'example-agent[bot]', id: 777, isBot: true }); assert.ok(r.fake.for('GET /app')[0].headers.authorization.startsWith('Bearer '), 'GET /app is JWT-authenticated'); assertNoTokenLeak(r); @@ -791,11 +791,11 @@ test('searchIssues scopes to the repository and drops a caller’s repo: qualifi title: 'A bug', state: 'open', updatedAt: '2026-10-02T00:00:00Z', - html_url: 'https://github.com/anima-research/rayforce/issues/7', + html_url: 'https://github.com/anima-research/example-repo/issues/7', }, ]); const sent = r.fake.for('GET /search/issues')[0]; - assert.equal(sent.query.q, 'repo:anima-research/rayforce is:open flaky'); + assert.equal(sent.query.q, 'repo:anima-research/example-repo is:open flaky'); assert.equal(sent.query.per_page, '50'); // An explicit limit is honoured but never exceeds the rendered list bound. @@ -807,7 +807,7 @@ test('searchIssues scopes to the repository and drops a caller’s repo: qualifi test('getCheckRuns rolls a sha’s runs up and lists the failures', async () => { const r = await rig(); - r.fake.on(`GET /repos/anima-research/rayforce/commits/${HEAD_SHA}/check-runs`, { + r.fake.on(`GET /repos/anima-research/example-repo/commits/${HEAD_SHA}/check-runs`, { body: { total_count: 4, check_runs: [ @@ -861,23 +861,23 @@ test('comment writes hit the issues or pulls comment path as `review` says', asy const r = await rig(); await r.adapter.createComment(ISSUE, 'hello'); - assert.deepEqual(r.fake.for('POST /repos/anima-research/rayforce/issues/7/comments')[0].body, { body: 'hello' }); + assert.deepEqual(r.fake.for('POST /repos/anima-research/example-repo/issues/7/comments')[0].body, { body: 'hello' }); await r.adapter.updateComment(REPO, 555, 'edited'); - assert.deepEqual(r.fake.for('PATCH /repos/anima-research/rayforce/issues/comments/555')[0].body, { body: 'edited' }); + assert.deepEqual(r.fake.for('PATCH /repos/anima-research/example-repo/issues/comments/555')[0].body, { body: 'edited' }); const reviewEdit = await r.adapter.updateComment(REPO, 555, 'edited', true); - assert.equal(r.fake.for('PATCH /repos/anima-research/rayforce/pulls/comments/555').length, 1); + assert.equal(r.fake.for('PATCH /repos/anima-research/example-repo/pulls/comments/555').length, 1); assert.equal(reviewEdit.path, 'a.ts'); assert.equal(reviewEdit.side, 'RIGHT'); await r.adapter.deleteComment(REPO, 555); - assert.equal(r.fake.for('DELETE /repos/anima-research/rayforce/issues/comments/555').length, 1); + assert.equal(r.fake.for('DELETE /repos/anima-research/example-repo/issues/comments/555').length, 1); await r.adapter.deleteComment(REPO, 555, true); - assert.equal(r.fake.for('DELETE /repos/anima-research/rayforce/pulls/comments/555').length, 1); + assert.equal(r.fake.for('DELETE /repos/anima-research/example-repo/pulls/comments/555').length, 1); const reply = await r.adapter.replyToReviewComment(PULL, 555, 'answering'); - assert.deepEqual(r.fake.for('POST /repos/anima-research/rayforce/pulls/12/comments/555/replies')[0].body, { + assert.deepEqual(r.fake.for('POST /repos/anima-research/example-repo/pulls/12/comments/555/replies')[0].body, { body: 'answering', }); assert.equal(reply.inReplyTo, 555); @@ -893,7 +893,7 @@ test('createReview posts the event, body and inline comments', async () => { }); assert.deepEqual(review, { id: 909, state: 'changes_requested', html_url: 'https://github.com/x#pullrequestreview-909' }); - assert.deepEqual(r.fake.for('POST /repos/anima-research/rayforce/pulls/12/reviews')[0].body, { + assert.deepEqual(r.fake.for('POST /repos/anima-research/example-repo/pulls/12/reviews')[0].body, { event: 'REQUEST_CHANGES', body: 'two things', comments: [{ path: 'src/a.ts', line: 12, side: 'RIGHT', body: 'this one' }], @@ -901,17 +901,17 @@ test('createReview posts the event, body and inline comments', async () => { // A bare COMMENT review sends no empty `comments` array. await r.adapter.createReview(PULL, { event: 'COMMENT' }); - assert.deepEqual(r.fake.for('POST /repos/anima-research/rayforce/pulls/12/reviews')[1].body, { event: 'COMMENT' }); + assert.deepEqual(r.fake.for('POST /repos/anima-research/example-repo/pulls/12/reviews')[1].body, { event: 'COMMENT' }); }); test('issue create, edit and close-with-reason go through one PATCH', async () => { const r = await rig(); await r.adapter.createIssue(REPO, { title: 'New', body: 'text' }); - assert.deepEqual(r.fake.for('POST /repos/anima-research/rayforce/issues')[0].body, { title: 'New', body: 'text' }); + assert.deepEqual(r.fake.for('POST /repos/anima-research/example-repo/issues')[0].body, { title: 'New', body: 'text' }); const closed = await r.adapter.updateIssue(ISSUE, { state: 'closed', stateReason: 'not_planned' }); - assert.deepEqual(r.fake.for('PATCH /repos/anima-research/rayforce/issues/7')[0].body, { + assert.deepEqual(r.fake.for('PATCH /repos/anima-research/example-repo/issues/7')[0].body, { state: 'closed', state_reason: 'not_planned', }); @@ -920,14 +920,14 @@ test('issue create, edit and close-with-reason go through one PATCH', async () = // An absent field is absent from the body, not sent as null. await r.adapter.updateIssue(ISSUE, { title: 'Retitled' }); - assert.deepEqual(r.fake.for('PATCH /repos/anima-research/rayforce/issues/7')[1].body, { title: 'Retitled' }); + assert.deepEqual(r.fake.for('PATCH /repos/anima-research/example-repo/issues/7')[1].body, { title: 'Retitled' }); }); test('pull request create, retarget, reviewers — and the draft flag through GraphQL', async () => { const r = await rig(); await r.adapter.createPullRequest(REPO, { head: 'fix/thing', base: 'dev', title: 'Fix it', body: 'the fix', draft: true }); - assert.deepEqual(r.fake.for('POST /repos/anima-research/rayforce/pulls')[0].body, { + assert.deepEqual(r.fake.for('POST /repos/anima-research/example-repo/pulls')[0].body, { head: 'fix/thing', base: 'dev', title: 'Fix it', @@ -936,7 +936,7 @@ test('pull request create, retarget, reviewers — and the draft flag through Gr }); const retargeted = await r.adapter.updatePullRequest(PULL, { title: 'Retitled', base: 'dev' }); - assert.deepEqual(r.fake.for('PATCH /repos/anima-research/rayforce/pulls/12')[0].body, { + assert.deepEqual(r.fake.for('PATCH /repos/anima-research/example-repo/pulls/12')[0].body, { title: 'Retitled', base: 'dev', }); @@ -944,12 +944,12 @@ test('pull request create, retarget, reviewers — and the draft flag through Gr assert.equal(r.fake.for('POST /graphql').length, 0, 'no draft change means no mutation'); await r.adapter.requestReviewers(PULL, ['maintainer', 'other']); - assert.deepEqual(r.fake.for('POST /repos/anima-research/rayforce/pulls/12/requested_reviewers')[0].body, { + assert.deepEqual(r.fake.for('POST /repos/anima-research/example-repo/pulls/12/requested_reviewers')[0].body, { reviewers: ['maintainer', 'other'], }); // REST cannot convert a PR to draft; GraphQL can, and the adapter re-reads. - r.fake.on('GET /repos/anima-research/rayforce/pulls/12', (_req, nth) => + r.fake.on('GET /repos/anima-research/example-repo/pulls/12', (_req, nth) => nth === 0 ? { body: PULL_PAYLOAD } : { body: { ...PULL_PAYLOAD, draft: true } }, ); const drafted = await r.adapter.updatePullRequest(PULL, { draft: true }); @@ -959,12 +959,12 @@ test('pull request create, retarget, reviewers — and the draft flag through Gr assert.equal(drafted.draft, true); // Already a draft: no mutation is sent. - r.fake.on('GET /repos/anima-research/rayforce/pulls/12', { body: { ...PULL_PAYLOAD, draft: true } }); + r.fake.on('GET /repos/anima-research/example-repo/pulls/12', { body: { ...PULL_PAYLOAD, draft: true } }); await r.adapter.updatePullRequest(PULL, { draft: true }); assert.equal(r.fake.for('POST /graphql').length, 1, 'a no-op draft change sends nothing'); // Ready for review takes the other mutation. - r.fake.on('GET /repos/anima-research/rayforce/pulls/12', (_req, nth) => + r.fake.on('GET /repos/anima-research/example-repo/pulls/12', (_req, nth) => nth === 0 ? { body: { ...PULL_PAYLOAD, draft: true } } : { body: PULL_PAYLOAD }, ); await r.adapter.updatePullRequest(PULL, { draft: false }); @@ -979,7 +979,7 @@ test('pull request create, retarget, reviewers — and the draft flag through Gr test('listIssuesSince asks the documented query and fills the head sha of open PRs', async () => { const r = await rig(); const since = '2026-10-10T00:00:00Z'; - r.fake.on('GET /repos/anima-research/rayforce/issues', { + r.fake.on('GET /repos/anima-research/example-repo/issues', { body: [ { ...ISSUE_PAYLOAD, id: 1, number: 7 }, { ...ISSUE_PAYLOAD, id: 2, number: 12, pull_request: { url: 'x' }, state: 'open' }, @@ -989,16 +989,16 @@ test('listIssuesSince asks the documented query and fills the head sha of open P number: 13, pull_request: { url: 'x' }, state: 'closed', - closed_by: { login: 'rayforce-clone[bot]', type: 'Bot' }, + closed_by: { login: 'example-agent[bot]', type: 'Bot' }, }, ], }); const { rows } = await r.adapter.listIssuesSince(REPO, since); // The list carries closed_by; a closed row is attributed to its closer. - assert.deepEqual(rows[2].closedBy, { login: 'rayforce-clone[bot]', isBot: true }); + assert.deepEqual(rows[2].closedBy, { login: 'example-agent[bot]', isBot: true }); assert.equal(rows[0].closedBy, undefined); - const query = r.fake.for('GET /repos/anima-research/rayforce/issues')[0].query; + const query = r.fake.for('GET /repos/anima-research/example-repo/issues')[0].query; assert.equal(query.state, 'all'); assert.equal(query.sort, 'updated'); assert.equal(query.since, since); @@ -1013,7 +1013,7 @@ test('listIssuesSince asks the documented query and fills the head sha of open P ], ); // Exactly one head read: the closed PR and the issue cost nothing. - assert.equal(r.fake.for('GET /repos/anima-research/rayforce/pulls/12').length, 1); + assert.equal(r.fake.for('GET /repos/anima-research/example-repo/pulls/12').length, 1); }); /** An issue row for a paging test: its number is its id, created before any cursor below. */ @@ -1047,7 +1047,7 @@ function serveSince(r: Rig, route: string, rows: { id: number; updated_at: strin }); } -const ISSUES = 'GET /repos/anima-research/rayforce/issues'; +const ISSUES = 'GET /repos/anima-research/example-repo/issues'; test('a poll follows rel="next" past a short page', async () => { // GitHub answered 99 rows and said more follow: the 99 are not the end. @@ -1198,12 +1198,12 @@ test('both comment lists stop at the request bound and say where', async () => { ...COMMENT_PAYLOAD, id, updated_at: second(id - 1), - issue_url: 'https://api.github.com/repos/anima-research/rayforce/issues/7', - pull_request_url: 'https://api.github.com/repos/anima-research/rayforce/pulls/12', + issue_url: 'https://api.github.com/repos/anima-research/example-repo/issues/7', + pull_request_url: 'https://api.github.com/repos/anima-research/example-repo/pulls/12', }); const all = Array.from({ length: 350 }, (_, i) => comment(1 + i)); - serveSince(r, 'GET /repos/anima-research/rayforce/issues/comments', all); - serveSince(r, 'GET /repos/anima-research/rayforce/pulls/comments', all); + serveSince(r, 'GET /repos/anima-research/example-repo/issues/comments', all); + serveSince(r, 'GET /repos/anima-research/example-repo/pulls/comments', all); for (const read of [ await r.adapter.listIssueCommentsSince(REPO, '2026-10-10T00:00:00Z'), @@ -1265,8 +1265,8 @@ test('a read the request bound stops leaves the second it stopped in for the nex /** A poller over a rig's adapter, its pass start set by `at`. */ function pollerOver(r: Rig, state: GithubState, at: { now: string }, delivered: NormalizedEvent[]): Poller { - serveSince(r, 'GET /repos/anima-research/rayforce/issues/comments', []); - serveSince(r, 'GET /repos/anima-research/rayforce/pulls/comments', []); + serveSince(r, 'GET /repos/anima-research/example-repo/issues/comments', []); + serveSince(r, 'GET /repos/anima-research/example-repo/pulls/comments', []); return new Poller({ api: r.adapter, repos: [REPO], @@ -1292,11 +1292,11 @@ test('a pass the request bound stops is read on from where it stopped, and every const first = await poller.pollOnce(); assert.equal(first.delivered.length, 297); - assert.deepEqual(state.pollCursors()['anima-research/rayforce:issues'], { + assert.deepEqual(state.pollCursors()['anima-research/example-repo:issues'], { since: '2026-10-10T00:00:00.000Z', resumeAt: second(297), }); - assert.deepEqual(state.pollCursors()['anima-research/rayforce:issue_comments'], { since: at.now }); + assert.deepEqual(state.pollCursors()['anima-research/example-repo:issue_comments'], { since: at.now }); assert.equal(state.lastPollAt(), second(297), 'every list has delivered what changed before the stop'); at.now = '2026-10-10T02:01:00.000Z'; @@ -1312,7 +1312,7 @@ test('a pass the request bound stops is read on from where it stopped, and every all.map((row) => row.id), 'every row once, in order', ); - assert.deepEqual(state.pollCursors()['anima-research/rayforce:issues'], { since: at.now }); + assert.deepEqual(state.pollCursors()['anima-research/example-repo:issues'], { since: at.now }); assert.equal(state.lastPollAt(), at.now); }); @@ -1328,8 +1328,8 @@ test('a row changed later in the second a pass started in arrives next pass, and await poller.pollOnce(); assert.deepEqual(delivered.map((e) => [e.thread?.number, e.kind]), [[1, 'issue.opened']]); - assert.deepEqual(state.pollCursors()['anima-research/rayforce:issues'], { since: '2026-10-10T02:00:00.000Z' }); - assert.deepEqual(state.polledNew()['anima-research/rayforce:issues'], { '1': '2026-10-10T02:00:00Z' }, 'created inside the next window, so kept'); + assert.deepEqual(state.pollCursors()['anima-research/example-repo:issues'], { since: '2026-10-10T02:00:00.000Z' }); + assert.deepEqual(state.polledNew()['anima-research/example-repo:issues'], { '1': '2026-10-10T02:00:00Z' }, 'created inside the next window, so kept'); // Still inside that second, after the read: #2 changes. Then #1 is edited. all.push(pagedIssue(2, '2026-10-10T02:00:00Z')); @@ -1362,7 +1362,7 @@ test('across a stopped read, a row opened in the window is opened once', async ( await poller.pollOnce(); assert.equal(delivered.find((e) => e.thread?.number === 10)?.kind, 'issue.opened'); - assert.deepEqual(state.polledNew()['anima-research/rayforce:issues'], { '10': second(0) }); + assert.deepEqual(state.polledNew()['anima-research/example-repo:issues'], { '10': second(0) }); // #10 is edited before the next pass reads on. all[9] = { ...all[9], body: 'edited', updated_at: second(400) }; @@ -1375,7 +1375,7 @@ test('across a stopped read, a row opened in the window is opened once', async ( test('a poll never reads more than the amendment’s 20 PR heads', async () => { const r = await rig(); - r.fake.on('GET /repos/anima-research/rayforce/issues', { + r.fake.on('GET /repos/anima-research/example-repo/issues', { body: Array.from({ length: 30 }, (_, i) => ({ ...ISSUE_PAYLOAD, id: i, @@ -1385,7 +1385,7 @@ test('a poll never reads more than the amendment’s 20 PR heads', async () => { })), }); for (let i = 0; i < 30; i++) { - r.fake.on(`GET /repos/anima-research/rayforce/pulls/${100 + i}`, { + r.fake.on(`GET /repos/anima-research/example-repo/pulls/${100 + i}`, { body: { ...PULL_PAYLOAD, number: 100 + i }, }); } @@ -1397,17 +1397,17 @@ test('a poll never reads more than the amendment’s 20 PR heads', async () => { test('the comment polls carry the issue or pull number the comment hangs on', async () => { const r = await rig(); const since = '2026-10-10T00:00:00Z'; - r.fake.on('GET /repos/anima-research/rayforce/issues/comments', { - body: [{ ...COMMENT_PAYLOAD, issue_url: 'https://api.github.com/repos/anima-research/rayforce/issues/7' }], + r.fake.on('GET /repos/anima-research/example-repo/issues/comments', { + body: [{ ...COMMENT_PAYLOAD, issue_url: 'https://api.github.com/repos/anima-research/example-repo/issues/7' }], }); - r.fake.on('GET /repos/anima-research/rayforce/pulls/comments', { + r.fake.on('GET /repos/anima-research/example-repo/pulls/comments', { body: [ { ...COMMENT_PAYLOAD, id: 556, path: 'src/a.ts', in_reply_to_id: 555, - pull_request_url: 'https://api.github.com/repos/anima-research/rayforce/pulls/12', + pull_request_url: 'https://api.github.com/repos/anima-research/example-repo/pulls/12', }, ], }); @@ -1416,14 +1416,14 @@ test('the comment polls carry the issue or pull number the comment hangs on', as assert.equal(issueComments.length, 1); assert.equal(issueComments[0].number, 7); assert.equal(issueComments[0].path, undefined); - assert.equal(r.fake.for('GET /repos/anima-research/rayforce/issues/comments')[0].query.sort, 'updated'); - assert.equal(r.fake.for('GET /repos/anima-research/rayforce/issues/comments')[0].query.since, since); + assert.equal(r.fake.for('GET /repos/anima-research/example-repo/issues/comments')[0].query.sort, 'updated'); + assert.equal(r.fake.for('GET /repos/anima-research/example-repo/issues/comments')[0].query.since, since); const { rows: reviewComments } = await r.adapter.listReviewCommentsSince(REPO, since); assert.equal(reviewComments[0].number, 12); assert.equal(reviewComments[0].path, 'src/a.ts'); assert.equal(reviewComments[0].inReplyTo, 555); - assert.equal(r.fake.for('GET /repos/anima-research/rayforce/pulls/comments')[0].query.since, since); + assert.equal(r.fake.for('GET /repos/anima-research/example-repo/pulls/comments')[0].query.since, since); assertNoTokenLeak(r); }); @@ -1449,14 +1449,14 @@ test('redactSecrets scrubs token forms and URL userinfo, and leaves prose alone' assert.equal(redactSecrets('header was token 0123456789abcdef'), `header was ${REDACTED}`); // An ordinary message and an ordinary URL come through untouched. assert.equal( - redactSecrets('Not Found — see https://api.github.com/repos/anima-research/rayforce'), - 'Not Found — see https://api.github.com/repos/anima-research/rayforce', + redactSecrets('Not Found — see https://api.github.com/repos/anima-research/example-repo'), + 'Not Found — see https://api.github.com/repos/anima-research/example-repo', ); }); test('a GitHub error that reflects the live token back redacts it everywhere', async () => { const r = await rig(); - r.fake.on('GET /repos/anima-research/rayforce/issues/7', { + r.fake.on('GET /repos/anima-research/example-repo/issues/7', { status: 422, body: { message: `Bad credentials for ${TOKEN} on this installation` }, }); @@ -1483,7 +1483,7 @@ test('a token with no recognizable shape is still redacted, because its value is const r = await rig({ token: opaque }); assert.equal(redactSecrets(opaque), opaque, 'the premise: no pattern matches this token'); - r.fake.on('GET /repos/anima-research/rayforce/issues/7', { + r.fake.on('GET /repos/anima-research/example-repo/issues/7', { status: 401, body: { message: `credentials ${opaque} are not valid` }, }); @@ -1501,7 +1501,7 @@ test('a token with no recognizable shape is still redacted, because its value is test('a credential-bearing URL reflected in an error loses its userinfo', async () => { const r = await rig(); - r.fake.on('GET /repos/anima-research/rayforce/issues/7', { + r.fake.on('GET /repos/anima-research/example-repo/issues/7', { status: 502, body: { message: 'upstream https://octocat:hunter2@api.github.com/repos/x refused the connection' }, }); @@ -1539,7 +1539,7 @@ test('a log line built from a reflected secret is redacted too', async () => { const r = await rig(); // The retry warning carries the operation, but the rate-limit path also logs // what GitHub said; drive a secondary limit whose message quotes the token. - r.fake.on('POST /repos/anima-research/rayforce/issues/7/comments', { + r.fake.on('POST /repos/anima-research/example-repo/issues/7/comments', { status: 403, headers: { 'retry-after': '1' }, body: { message: `secondary rate limit hit by ${TOKEN}` }, @@ -1569,7 +1569,7 @@ function successRuns(from: number, count: number) { test('getCheckRuns follows pages, so a failure on page two is not reported as success', async () => { const r = await rig(); - r.fake.on(`GET /repos/anima-research/rayforce/commits/${HEAD_SHA}/check-runs`, (req) => ({ + r.fake.on(`GET /repos/anima-research/example-repo/commits/${HEAD_SHA}/check-runs`, (req) => ({ body: { total_count: 101, check_runs: @@ -1584,21 +1584,21 @@ test('getCheckRuns follows pages, so a failure on page two is not reported as su assert.equal(summary.conclusion, 'failure', 'a failure on page two is a failure'); assert.deepEqual(summary.failed, [{ name: 'late-failure', url: 'https://ci/late' }]); assert.deepEqual( - r.fake.for(`GET /repos/anima-research/rayforce/commits/${HEAD_SHA}/check-runs`).map((c) => c.query.page), + r.fake.for(`GET /repos/anima-research/example-repo/commits/${HEAD_SHA}/check-runs`).map((c) => c.query.page), ['1', '2'], ); }); test('a sha with more check runs than the page bound is undecided, never success', async () => { const r = await rig(); - r.fake.on(`GET /repos/anima-research/rayforce/commits/${HEAD_SHA}/check-runs`, (req) => ({ + r.fake.on(`GET /repos/anima-research/example-repo/commits/${HEAD_SHA}/check-runs`, (req) => ({ body: { total_count: 1000, check_runs: successRuns((Number(req.query.page) - 1) * 100, 100) }, })); const summary = await r.adapter.getCheckRuns(REPO, HEAD_SHA); assert.equal(summary.conclusion, null, 'what was not read cannot be called green'); assert.equal(summary.total, 1000); - assert.equal(r.fake.for(`GET /repos/anima-research/rayforce/commits/${HEAD_SHA}/check-runs`).length, 5); + assert.equal(r.fake.for(`GET /repos/anima-research/example-repo/commits/${HEAD_SHA}/check-runs`).length, 5); assert.ok( r.logs.some((l) => l.startsWith('warn check runs did not fit')), `expected a truncation warning, got ${JSON.stringify(r.logs)}`, @@ -1607,19 +1607,19 @@ test('a sha with more check runs than the page bound is undecided, never success test('a single complete page still stops at one request', async () => { const r = await rig(); - r.fake.on(`GET /repos/anima-research/rayforce/commits/${HEAD_SHA}/check-runs`, { + r.fake.on(`GET /repos/anima-research/example-repo/commits/${HEAD_SHA}/check-runs`, { body: { total_count: 2, check_runs: successRuns(0, 2) }, }); const summary = await r.adapter.getCheckRuns(REPO, HEAD_SHA); assert.equal(summary.conclusion, 'success'); assert.equal(summary.total, 2); - assert.equal(r.fake.for(`GET /repos/anima-research/rayforce/commits/${HEAD_SHA}/check-runs`).length, 1); + assert.equal(r.fake.for(`GET /repos/anima-research/example-repo/commits/${HEAD_SHA}/check-runs`).length, 1); }); test('getCheckRuns reads on past a short page while total_count says more runs follow', async () => { const r = await rig(); - r.fake.on(`GET /repos/anima-research/rayforce/commits/${HEAD_SHA}/check-runs`, (req) => ({ + r.fake.on(`GET /repos/anima-research/example-repo/commits/${HEAD_SHA}/check-runs`, (req) => ({ body: { total_count: 100, check_runs: @@ -1631,12 +1631,12 @@ test('getCheckRuns reads on past a short page while total_count says more runs f const summary = await r.adapter.getCheckRuns(REPO, HEAD_SHA); assert.equal(summary.conclusion, 'failure', 'a page of 99 was not the last'); - assert.equal(r.fake.for(`GET /repos/anima-research/rayforce/commits/${HEAD_SHA}/check-runs`).length, 2); + assert.equal(r.fake.for(`GET /repos/anima-research/example-repo/commits/${HEAD_SHA}/check-runs`).length, 2); }); test('getCheckRuns without a total_count reads on past a full page', async () => { const r = await rig(); - r.fake.on(`GET /repos/anima-research/rayforce/commits/${HEAD_SHA}/check-runs`, (req) => ({ + r.fake.on(`GET /repos/anima-research/example-repo/commits/${HEAD_SHA}/check-runs`, (req) => ({ body: { check_runs: Number(req.query.page) === 1 @@ -1647,36 +1647,36 @@ test('getCheckRuns without a total_count reads on past a full page', async () => const summary = await r.adapter.getCheckRuns(REPO, HEAD_SHA); assert.equal(summary.conclusion, 'failure'); - assert.equal(r.fake.for(`GET /repos/anima-research/rayforce/commits/${HEAD_SHA}/check-runs`).length, 2); + assert.equal(r.fake.for(`GET /repos/anima-research/example-repo/commits/${HEAD_SHA}/check-runs`).length, 2); }); test('getCheckRuns without a total_count is undecided when the page bound stops it', async () => { const r = await rig(); - r.fake.on(`GET /repos/anima-research/rayforce/commits/${HEAD_SHA}/check-runs`, (req) => ({ + r.fake.on(`GET /repos/anima-research/example-repo/commits/${HEAD_SHA}/check-runs`, (req) => ({ body: { check_runs: successRuns((Number(req.query.page) - 1) * 100, 100) }, })); const summary = await r.adapter.getCheckRuns(REPO, HEAD_SHA); assert.equal(summary.conclusion, null, 'what was not read cannot be called green'); - assert.equal(r.fake.for(`GET /repos/anima-research/rayforce/commits/${HEAD_SHA}/check-runs`).length, 5); + assert.equal(r.fake.for(`GET /repos/anima-research/example-repo/commits/${HEAD_SHA}/check-runs`).length, 5); }); test('getCheckRuns ends at an empty page as undecided, and says the count was not reached', async () => { // total_count counts a run that is gone by the time its page is asked for. const r = await rig(); - r.fake.on(`GET /repos/anima-research/rayforce/commits/${HEAD_SHA}/check-runs`, (req) => ({ + r.fake.on(`GET /repos/anima-research/example-repo/commits/${HEAD_SHA}/check-runs`, (req) => ({ body: { total_count: 101, check_runs: Number(req.query.page) === 1 ? successRuns(0, 100) : [] }, })); const summary = await r.adapter.getCheckRuns(REPO, HEAD_SHA); assert.equal(summary.conclusion, null); - assert.equal(r.fake.for(`GET /repos/anima-research/rayforce/commits/${HEAD_SHA}/check-runs`).length, 2); + assert.equal(r.fake.for(`GET /repos/anima-research/example-repo/commits/${HEAD_SHA}/check-runs`).length, 2); assert.ok(r.logs.some((l) => l.startsWith('warn check runs ended before total_count')), r.logs.join('\n')); }); test('a check-run read that ended short is still a failure when a run it read failed', async () => { const r = await rig(); - r.fake.on(`GET /repos/anima-research/rayforce/commits/${HEAD_SHA}/check-runs`, (req) => ({ + r.fake.on(`GET /repos/anima-research/example-repo/commits/${HEAD_SHA}/check-runs`, (req) => ({ body: { total_count: 101, check_runs: @@ -1715,7 +1715,7 @@ test('summarizeChecks refuses to call a truncated read success', () => { test('a secondary limit with no Retry-After waits the minute GitHub asks for', async () => { const r = await rig(); - r.fake.on('POST /repos/anima-research/rayforce/issues/7/comments', (_req, nth) => + r.fake.on('POST /repos/anima-research/example-repo/issues/7/comments', (_req, nth) => nth === 0 ? { // No `retry-after`, and the budget is not spent: only the message @@ -1729,7 +1729,7 @@ test('a secondary limit with no Retry-After waits the minute GitHub asks for', a const comment = await r.adapter.createComment(ISSUE, 'hello'); assert.equal(comment.id, 555); assert.deepEqual(r.sleeps, [60_000], 'retrying a secondary limit sooner than a minute earns the next one'); - assert.equal(r.fake.for('POST /repos/anima-research/rayforce/issues/7/comments').length, 2); + assert.equal(r.fake.for('POST /repos/anima-research/example-repo/issues/7/comments').length, 2); }); // ───────────────────────────────────────────────────────────────────────────── @@ -1752,13 +1752,13 @@ test('a failure minting the installation token keeps its status and names the mi assert.equal(err.githubMessage, 'Resource not accessible by integration'); assert.equal(err.operation, 'mintInstallationToken'); // The call it was blocking never went out. - assert.equal(r.fake.for('GET /repos/anima-research/rayforce/issues/7').length, 0); + assert.equal(r.fake.for('GET /repos/anima-research/example-repo/issues/7').length, 0); assertNoTokenLeak(r, [renderError(err)]); }); test('a failure discovering the installation keeps its status and names the discovery', async () => { const r = await rig({ env: { GITHUB_APP_INSTALLATION_ID: undefined } }); - r.fake.on('GET /repos/anima-research/rayforce/installation', { + r.fake.on('GET /repos/anima-research/example-repo/installation', { status: 404, body: { message: 'Not Found' }, }); @@ -1797,7 +1797,7 @@ test('an auth failure on a GraphQL call keeps its status too', async () => { test('octokit’s deprecation warning reaches the injected Logger, not stderr', async () => { const r = await rig(); - r.fake.on('GET /repos/anima-research/rayforce/issues/7', { + r.fake.on('GET /repos/anima-research/example-repo/issues/7', { headers: { deprecation: 'true', sunset: '2026-12-01' }, body: ISSUE_PAYLOAD, }); @@ -1826,7 +1826,7 @@ test('an octokit diagnostic is redacted like any other log line', async () => { const r = await rig(); // The deprecation warning quotes the request URL back; make that URL carry // something that must not be logged. - r.fake.on(`GET /repos/anima-research/rayforce/commits/ghs_LEAKED_IN_A_URL/check-runs`, { + r.fake.on(`GET /repos/anima-research/example-repo/commits/ghs_LEAKED_IN_A_URL/check-runs`, { headers: { deprecation: 'true', sunset: '2026-12-01' }, body: { total_count: 0, check_runs: [] }, }); @@ -1868,16 +1868,16 @@ test('a token still in flight when a refresh replaces it is redacted all the sam const held = new Promise((resolve) => { release = resolve; }); - r.fake.on('GET /repos/anima-research/rayforce/issues/7', () => held); + r.fake.on('GET /repos/anima-research/example-repo/issues/7', () => held); const inFlight = r.adapter.getIssue(ISSUE).then( () => null, (e: unknown) => e as GitHubApiError, ); - await waitFor(() => r.fake.for('GET /repos/anima-research/rayforce/issues/7').length === 1, 'the held request'); + await waitFor(() => r.fake.for('GET /repos/anima-research/example-repo/issues/7').length === 1, 'the held request'); assert.deepEqual(issued, [TOKEN_A]); assert.equal( - r.fake.for('GET /repos/anima-research/rayforce/issues/7')[0].headers.authorization, + r.fake.for('GET /repos/anima-research/example-repo/issues/7')[0].headers.authorization, `token ${TOKEN_A}`, 'the held request really is carrying token A', ); @@ -1948,16 +1948,16 @@ test('no rotation ages a credential out while the request that sent it is unreso const held = new Promise((resolve) => { release = resolve; }); - r.fake.on('GET /repos/anima-research/rayforce/issues/7', () => held); + r.fake.on('GET /repos/anima-research/example-repo/issues/7', () => held); const inFlight = r.adapter.getIssue(ISSUE).then( () => null, (e: unknown) => e as GitHubApiError, ); - await waitFor(() => r.fake.for('GET /repos/anima-research/rayforce/issues/7').length === 1, 'the held request'); + await waitFor(() => r.fake.for('GET /repos/anima-research/example-repo/issues/7').length === 1, 'the held request'); const tokenA = issued[0]; assert.equal( - r.fake.for('GET /repos/anima-research/rayforce/issues/7')[0].headers.authorization, + r.fake.for('GET /repos/anima-research/example-repo/issues/7')[0].headers.authorization, `token ${tokenA}`, 'the held request really is carrying the first token', ); @@ -1965,7 +1965,7 @@ test('no rotation ages a credential out while the request that sent it is unreso // Ten sequential calls, each refused once and retried, which mints a fresh // token every time. No clock advance: rotation alone drives this. - r.fake.on('GET /repos/anima-research/rayforce/issues/7/comments', (_req, nth) => + r.fake.on('GET /repos/anima-research/example-repo/issues/7/comments', (_req, nth) => nth % 2 === 0 ? { status: 401, body: { message: 'Bad credentials' } } : { body: [COMMENT_PAYLOAD] }, ); const ROTATIONS = 10; @@ -2012,7 +2012,7 @@ test('retention is by validity, so a credential GitHub would refuse is not kept await r.adapter.getIssue(ISSUE); assert.ok(issued.length >= 2, 'the expired token was replaced'); - r.fake.on('GET /repos/anima-research/rayforce/issues/7', { + r.fake.on('GET /repos/anima-research/example-repo/issues/7', { status: 422, body: { message: `stale ${early} and current ${issued[issued.length - 1]}` }, }); diff --git a/test/harness.ts b/test/harness.ts index e2afa12..9b26ea3 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -1,7 +1,7 @@ /** * Shared test harness. * - * Two pieces, and every other work package tests against both: + * Two pieces, and every module tests against both: * * `harness()` — the real GitHubMcplServer over an in-memory stream * pair, driven the way an MCPL host drives it. Same API @@ -65,8 +65,8 @@ export interface RecordedCall { args: unknown[]; } -export const TEST_REPO: RepoRef = { owner: 'anima-research', name: 'rayforce' }; -export const TEST_BOT: BotIdentity = { login: 'rayforce-clone[bot]', id: 9001, isBot: true }; +export const TEST_REPO: RepoRef = { owner: 'anima-research', name: 'example-repo' }; +export const TEST_BOT: BotIdentity = { login: 'example-agent[bot]', id: 9001, isBot: true }; function humanActor(login: string): EventActor { return { login, isBot: false }; diff --git a/test/integration.test.ts b/test/integration.test.ts index a8f694a..f455b63 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -1,5 +1,5 @@ /** - * The wired server (TASK-SRHKG.1.9): the seven packages as one connection. + * The wired server: every module as one connection. * * Every case here goes through the real `GitHubMcplServer` over a real * `McplConnection` pair (the {@link harness}), the real `WebhookListener` over @@ -356,7 +356,7 @@ test('an issue webhook delivery arrives as push/event and channels/incoming with const origin = push.origin as Record; assert.equal(origin.source, 'github'); assert.equal(origin.kind, 'issue.opened'); - assert.equal(origin.repo, 'anima-research/rayforce'); + assert.equal(origin.repo, 'anima-research/example-repo'); assert.equal(origin.channelId, channelId(ISSUE)); assert.deepEqual(origin.sender, { login: 'octocat', isBot: false }); // The wake metadata rides on `origin`, where a host's matchers read it. @@ -388,8 +388,8 @@ test('a CI failure carries the failed jobs and their urls, and the bot’s own s conclusion: 'failure', total: 3, failed: [ - { name: 'test (ubuntu-latest)', url: 'https://github.com/anima-research/rayforce/runs/2700001234' }, - { name: 'clippy', url: 'https://github.com/anima-research/rayforce/runs/2700001235' }, + { name: 'test (ubuntu-latest)', url: 'https://github.com/anima-research/example-repo/runs/2700001234' }, + { name: 'clippy', url: 'https://github.com/anima-research/example-repo/runs/2700001235' }, ], }); const h = rig(t, { api }); @@ -413,8 +413,8 @@ test('a CI failure carries the failed jobs and their urls, and the bot’s own s // Every failed job, by name and by url, is in the rendered body. const text = textOf(push.payload as { content: [] }); assert.match(text, /checks: failure, 2 failed/); - assert.match(text, /test \(ubuntu-latest\) — https:\/\/github\.com\/anima-research\/rayforce\/runs\/2700001234/); - assert.match(text, /clippy — https:\/\/github\.com\/anima-research\/rayforce\/runs\/2700001235/); + assert.match(text, /test \(ubuntu-latest\) — https:\/\/github\.com\/anima-research\/example-repo\/runs\/2700001234/); + assert.match(text, /clippy — https:\/\/github\.com\/anima-research\/example-repo\/runs\/2700001235/); // And it is a channel delivery too, because a check suite has a pull request. assert.equal(incoming(h)[0].channelId, channelId(PULL)); }); @@ -1915,7 +1915,7 @@ test('finding 1: a channel registered before the allow-list narrowed cannot be w }), ); assert.equal(refused.code, -32602); - assert.match(refused.message, /channels\/publish: repository anima-research\/rayforce is not in GITHUB_REPOS/); + assert.match(refused.message, /channels\/publish: repository anima-research\/example-repo is not in GITHUB_REPOS/); assert.deepEqual(api.callsTo('createComment'), [], 'the refusal came before the GitHub call'); }); diff --git a/test/normalize.test.ts b/test/normalize.test.ts index 0cebf90..6544661 100644 --- a/test/normalize.test.ts +++ b/test/normalize.test.ts @@ -1,5 +1,5 @@ /** - * Fixture → NormalizedEvent (TASK-SRHKG.1.3). + * Fixture → NormalizedEvent. * * Every file in test/fixtures/ is a hand-written payload shaped like GitHub's * documented webhook JSON — `octocat`/`hubot` placeholders, invented ids, no @@ -84,7 +84,7 @@ const EXPECTED: Record = { thread: ISSUE_41, actor: { login: 'octocat', isBot: false }, title: 'Scheduler drops the last batch when the queue drains mid-tick', - html_url: 'https://github.com/anima-research/rayforce/issues/41', + html_url: 'https://github.com/anima-research/example-repo/issues/41', }, ], }, @@ -94,7 +94,7 @@ const EXPECTED: Record = { kind: 'issue.closed', thread: ISSUE_41, actor: { login: 'hubot', isBot: false }, - html_url: 'https://github.com/anima-research/rayforce/issues/41', + html_url: 'https://github.com/anima-research/example-repo/issues/41', }, ], }, @@ -104,8 +104,8 @@ const EXPECTED: Record = { kind: 'comment.created', thread: ISSUE_41, actor: { login: 'hubot', isBot: false }, - body: '@rayforce-clone[bot] can you reproduce this on dev?', - html_url: 'https://github.com/anima-research/rayforce/issues/41#issuecomment-2200000007', + body: '@example-agent[bot] can you reproduce this on dev?', + html_url: 'https://github.com/anima-research/example-repo/issues/41#issuecomment-2200000007', // Amendment 2b: a top-level comment carries its own id, so the // turn-order rule can place it in the thread. comment: { id: 2200000007 }, @@ -119,7 +119,7 @@ const EXPECTED: Record = { // `issue.pull_request` present ⇒ the thread is a pull request. thread: PULL_58, actor: { login: 'octocat', isBot: false }, - html_url: 'https://github.com/anima-research/rayforce/pull/58#issuecomment-2200000031', + html_url: 'https://github.com/anima-research/example-repo/pull/58#issuecomment-2200000031', comment: { id: 2200000031 }, }, ], @@ -129,15 +129,15 @@ const EXPECTED: Record = { { kind: 'pull.opened', thread: PULL_58, - actor: { login: 'rayforce-clone[bot]', isBot: true }, + actor: { login: 'example-agent[bot]', isBot: true }, title: 'fix(scheduler): dispatch the final batch', sha: FIRST_HEAD, - html_url: 'https://github.com/anima-research/rayforce/pull/58', + html_url: 'https://github.com/anima-research/example-repo/pull/58', }, ], }, 'pull_request.synchronize.json': { - events: [{ kind: 'pull.synchronize', thread: PULL_58, actor: { login: 'rayforce-clone[bot]', isBot: true }, sha: HEAD }], + events: [{ kind: 'pull.synchronize', thread: PULL_58, actor: { login: 'example-agent[bot]', isBot: true }, sha: HEAD }], }, 'pull_request.ready_for_review.json': { events: [{ kind: 'pull.ready_for_review', thread: PULL_58, sha: FIRST_HEAD }], @@ -157,7 +157,7 @@ const EXPECTED: Record = { body: 'Two things before this goes in.', sha: HEAD, review: { id: 2400000012, state: 'changes_requested' }, - html_url: 'https://github.com/anima-research/rayforce/pull/58#pullrequestreview-2400000012', + html_url: 'https://github.com/anima-research/example-repo/pull/58#pullrequestreview-2400000012', }, ], }, @@ -196,7 +196,7 @@ const EXPECTED: Record = { { kind: 'review_comment.created', thread: PULL_58, - actor: { login: 'rayforce-clone[bot]', isBot: true }, + actor: { login: 'example-agent[bot]', isBot: true }, sha: HEAD, reviewComment: { id: 2500000078, @@ -216,7 +216,7 @@ const EXPECTED: Record = { kind: 'checks.completed', // The suite's head sha is the head of PR 58, so the event is threaded. thread: PULL_58, - actor: { login: 'rayforce-clone[bot]', isBot: true }, + actor: { login: 'example-agent[bot]', isBot: true }, sha: HEAD, checks: { conclusion: 'failure', failed: [] }, }, @@ -254,7 +254,7 @@ const EXPECTED: Record = { actor: { login: 'octocat', isBot: false }, sha: DEV_SHA, checks: { conclusion: 'success', failed: [] }, - html_url: `https://github.com/anima-research/rayforce/commit/${DEV_SHA}`, + html_url: `https://github.com/anima-research/example-repo/commit/${DEV_SHA}`, }, ], }, @@ -268,9 +268,9 @@ const EXPECTED: Record = { // with every failure on the sha. checks: { conclusion: 'failure', - failed: [{ name: 'test (ubuntu-latest)', url: 'https://github.com/anima-research/rayforce/runs/2700001234' }], + failed: [{ name: 'test (ubuntu-latest)', url: 'https://github.com/anima-research/example-repo/runs/2700001234' }], }, - html_url: 'https://github.com/anima-research/rayforce/runs/2700001234', + html_url: 'https://github.com/anima-research/example-repo/runs/2700001234', }, ], }, @@ -279,9 +279,9 @@ const EXPECTED: Record = { { kind: 'push', thread: undefined, - actor: { login: 'rayforce-clone[bot]', isBot: true }, + actor: { login: 'example-agent[bot]', isBot: true }, sha: HEAD, - html_url: 'https://github.com/anima-research/rayforce/compare/1f0c9a4d2b7e...4d7b2e8c1a93', + html_url: 'https://github.com/anima-research/example-repo/compare/1f0c9a4d2b7e...4d7b2e8c1a93', }, ], }, @@ -373,7 +373,7 @@ test('actor and repo extraction', () => { assert.deepEqual(actorFrom({ login: 'dependabot[bot]' }), { login: 'dependabot[bot]', isBot: true }); assert.deepEqual(actorFrom({ login: 'some-app', type: 'Bot' }), { login: 'some-app', isBot: true }); assert.equal(actorFrom({}), undefined); - assert.deepEqual(repoFrom({ repository: { full_name: 'anima-research/rayforce' } }), TEST_REPO); + assert.deepEqual(repoFrom({ repository: { full_name: 'anima-research/example-repo' } }), TEST_REPO); // No full_name: owner.login + name is the fallback. assert.deepEqual(repoFrom({ repository: { owner: { login: 'a' }, name: 'b' } }), { owner: 'a', name: 'b' }); assert.equal(repoFrom({ repository: { full_name: 'not-a-slug' } }), undefined); @@ -399,7 +399,7 @@ const POLLED_ISSUE: PolledIssue = { author: { login: 'octocat', isBot: false }, createdAt: '2026-10-12T08:00:00Z', updatedAt: '2026-10-13T09:00:00Z', - html_url: 'https://github.com/anima-research/rayforce/issues/41', + html_url: 'https://github.com/anima-research/example-repo/issues/41', }; /** The cursor the poll pass queried with, after POLLED_ISSUE was created. */ @@ -513,7 +513,7 @@ test('a polled top-level comment carries its own id, a review comment does not ( author: { login: 'octocat', isBot: false }, body: 'a followup', updatedAt: '2026-10-13T09:00:00Z', - html_url: 'https://github.com/anima-research/rayforce/issues/41#issuecomment-2200000031', + html_url: 'https://github.com/anima-research/example-repo/issues/41#issuecomment-2200000031', }; // Amendment 2b places a comment in its thread's turn order, which needs its // id; a `since` row has always carried one, it was just thrown away. @@ -534,7 +534,7 @@ test('a polled issue comment takes its thread kind from the html_url', () => { author: { login: 'octocat', isBot: false }, body: 'Looks right.', updatedAt: '2026-10-13T09:05:00Z', - html_url: 'https://github.com/anima-research/rayforce/pull/58#issuecomment-2200000031', + html_url: 'https://github.com/anima-research/example-repo/pull/58#issuecomment-2200000031', }; const event = normalizePolledComment(TEST_REPO, row, 'issue_comment', RECEIVED_AT); assert.equal(event.kind, 'comment.created'); @@ -544,7 +544,7 @@ test('a polled issue comment takes its thread kind from the html_url', () => { const onIssue = normalizePolledComment( TEST_REPO, - { ...row, number: 41, html_url: 'https://github.com/anima-research/rayforce/issues/41#issuecomment-1' }, + { ...row, number: 41, html_url: 'https://github.com/anima-research/example-repo/issues/41#issuecomment-1' }, 'issue_comment', RECEIVED_AT, ); @@ -555,10 +555,10 @@ test('a polled review comment carries path and inReplyTo and is always on a pull const row: PolledComment = { id: 2500000078, number: 58, - author: { login: 'rayforce-clone[bot]', isBot: true }, + author: { login: 'example-agent[bot]', isBot: true }, body: 'Guarded now.', updatedAt: '2026-10-13T13:02:00Z', - html_url: 'https://github.com/anima-research/rayforce/pull/58#discussion_r2500000078', + html_url: 'https://github.com/anima-research/example-repo/pull/58#discussion_r2500000078', path: 'src/scheduler/tick.rs', inReplyTo: 2500000077, }; @@ -580,7 +580,7 @@ test('a polled check summary is keyed on the conclusion, not on a timestamp', () sha: HEAD, conclusion: 'failure' as CheckConclusion, total: 3, - failed: [{ name: 'clippy', url: 'https://github.com/anima-research/rayforce/runs/2700001235' }], + failed: [{ name: 'clippy', url: 'https://github.com/anima-research/example-repo/runs/2700001235' }], }; const event = normalizePolledChecks(TEST_REPO, PULL_58, summary, PULL_58.repo.name, RECEIVED_AT); assert.equal(event.kind, 'checks.completed'); @@ -900,7 +900,7 @@ test('resolveCheckEvents holds back a head whose other suite is still running', sha: HEAD, conclusion: null, total: 4, - failed: [{ name: 'lint', url: 'https://github.com/anima-research/rayforce/runs/1' }], + failed: [{ name: 'lint', url: 'https://github.com/anima-research/example-repo/runs/1' }], }); assert.deepEqual(await resolveCheckEvents(normalizeWebhook(load('check_suite.completed.json')), api), []); }); @@ -912,8 +912,8 @@ test('resolveCheckEvents emits the settled sha with every failed job on it', asy conclusion: 'failure', total: 4, failed: [ - { name: 'lint', url: 'https://github.com/anima-research/rayforce/runs/1' }, - { name: 'test', url: 'https://github.com/anima-research/rayforce/runs/2' }, + { name: 'lint', url: 'https://github.com/anima-research/example-repo/runs/1' }, + { name: 'test', url: 'https://github.com/anima-research/example-repo/runs/2' }, ], }); const [event] = await resolveCheckEvents(normalizeWebhook(load('check_suite.completed.json')), api); diff --git a/test/wp8-policies.test.ts b/test/policies.test.ts similarity index 99% rename from test/wp8-policies.test.ts rename to test/policies.test.ts index 5278148..b5bc6dc 100644 --- a/test/wp8-policies.test.ts +++ b/test/policies.test.ts @@ -155,7 +155,7 @@ const humanOpenedPull: GateEventInfo = { const humanMention: GateEventInfo = { ...base, eventType: 'mcpl:channel-incoming', - content: '@rayforce-clone can you look at the tsan failure?', + content: '@example-agent can you look at the tsan failure?', tags: ['chat:mention', 'chat:from-human', 'github:actor=maintainer'], metadata: { mentioned: true, isOwnThread: false, fromSelf: false, repo: 'owner/repo', number: 12 }, }; diff --git a/test/poller.test.ts b/test/poller.test.ts index cf93a58..9aafc15 100644 --- a/test/poller.test.ts +++ b/test/poller.test.ts @@ -1,5 +1,5 @@ /** - * Polling ingress (TASK-SRHKG.1.3, contract amendment 1). + * Polling ingress (contract amendment 1). * * The two acceptance properties are asserted directly: * @@ -175,7 +175,7 @@ function failedSummary(sha: string): ChecksSummary { sha, conclusion: 'failure', total: 2, - failed: [{ name: 'clippy', url: 'https://github.com/anima-research/rayforce/runs/2700001235' }], + failed: [{ name: 'clippy', url: 'https://github.com/anima-research/example-repo/runs/2700001235' }], }; } @@ -262,7 +262,7 @@ test('every allow-listed repository is polled, in one pass', async () => { const result = await r.poller.pollOnce(); assert.deepEqual( result.delivered.map((e) => [e.repo.name, e.thread?.number]), - [['rayforce', 41], ['chronicle', 3]], + [['example-repo', 41], ['chronicle', 3]], ); assert.deepEqual(r.state.snapshot().pollKeys, ['issue:41:2026-10-13T12:00:00.000Z', 'issue:3:2026-10-13T12:00:01.000Z']); }); @@ -312,7 +312,7 @@ test('an open pull request whose head did not move is dropped on the next pass ( first.delivered.map((e) => e.kind), ['pull.synchronize'], ); - assert.equal(r.state.heads.get('github:anima-research/rayforce#58'), pull.head.sha); + assert.equal(r.state.heads.get('github:anima-research/example-repo#58'), pull.head.sha); // Pass two: a comment bumps the pull request's row without moving its head. // The comment is the event; re-announcing the pull request beside it is the @@ -334,7 +334,7 @@ test('an open pull request whose head did not move is dropped on the next pass ( ['pull.synchronize'], ); assert.equal(third.delivered[0].sha, 'f'.repeat(40)); - assert.equal(r.state.heads.get('github:anima-research/rayforce#58'), 'f'.repeat(40)); + assert.equal(r.state.heads.get('github:anima-research/example-repo#58'), 'f'.repeat(40)); }); test('a pull request opened since the cursor is pull.opened, not synchronize (amendment 2a)', async () => { @@ -376,7 +376,7 @@ test('a refused pull.synchronize is retried on the next pass and delivered once // …and the head must be held back with it. Committing it here is what lost // the event for good: the next pass would compare the row against this very // sha, find no change, drop it, and let the cursor move past it. - assert.equal(r.state.heads.get('github:anima-research/rayforce#58'), undefined); + assert.equal(r.state.heads.get('github:anima-research/example-repo#58'), undefined); // Pass two: the same row is queried again (the cursor never moved) and the // host takes it this time. @@ -392,7 +392,7 @@ test('a refused pull.synchronize is retried on the next pass and delivered once assert.equal(second.failed, 0); assert.deepEqual(attempts, ['pull.synchronize', 'pull.synchronize'], 'attempted twice, delivered once'); // Accepted, so now it is committed. - assert.equal(r.state.heads.get('github:anima-research/rayforce#58'), pull.head.sha); + assert.equal(r.state.heads.get('github:anima-research/example-repo#58'), pull.head.sha); // Pass three: nothing moved since, so the row is dropped as usual. pull.updatedAt = '2026-10-14T01:00:00.000Z'; @@ -416,7 +416,7 @@ test('a collection failure after a changed head does not advance the head (findi const r = rig({ api, cursor: '2026-10-13T09:00:00.000Z' }); await assert.rejects(() => r.poller.pollOnce(), /502 on the comment page/); assert.equal( - r.state.heads.get('github:anima-research/rayforce#58'), + r.state.heads.get('github:anima-research/example-repo#58'), undefined, 'a pass that delivered nothing advanced nothing', ); @@ -428,7 +428,7 @@ test('a collection failure after a changed head does not advance the head (findi recovered.delivered.map((e) => e.kind), ['pull.synchronize'], ); - assert.equal(r.state.heads.get('github:anima-research/rayforce#58'), pull.head.sha); + assert.equal(r.state.heads.get('github:anima-research/example-repo#58'), pull.head.sha); }); test('a dropped pull-request row still records its head, so the next pass agrees (amendment 2a)', async () => { @@ -452,7 +452,7 @@ test('a dropped pull-request row still records its head, so the next pass agrees ['comment.created'], ); } - assert.equal(r.state.heads.get('github:anima-research/rayforce#58'), pull.head.sha); + assert.equal(r.state.heads.get('github:anima-research/example-repo#58'), pull.head.sha); }); test('issues, comments and review comments updated since the cursor are delivered oldest first', async () => { @@ -512,7 +512,7 @@ test('an open pull-request head gets one checks event per conclusion, and none w sha: pull.head.sha, conclusion: 'failure', total: 2, - failed: [{ name: 'clippy', url: 'https://github.com/anima-research/rayforce/runs/2700001235' }], + failed: [{ name: 'clippy', url: 'https://github.com/anima-research/example-repo/runs/2700001235' }], }); const r = rig({ api }); @@ -522,7 +522,7 @@ test('an open pull-request head gets one checks event per conclusion, and none w assert.equal(checks[0].sha, pull.head.sha); assert.deepEqual(checks[0].checks, { conclusion: 'failure', - failed: [{ name: 'clippy', url: 'https://github.com/anima-research/rayforce/runs/2700001235' }], + failed: [{ name: 'clippy', url: 'https://github.com/anima-research/example-repo/runs/2700001235' }], }); assert.deepEqual(checks[0].thread, { repo: TEST_REPO, number: 58, kind: 'pull' }); assert.deepEqual(api.callsTo('getCheckRuns')[0].args, [TEST_REPO, pull.head.sha]); @@ -543,8 +543,8 @@ test('a settled head whose failed jobs change with the same count is news, not a const api = new FakeGitHubApi(); const pull = api.seedPullRequest({ number: 58, updatedAt: '2026-10-13T12:00:00.000Z' }); const sets = [ - [{ name: 'lint', url: 'https://github.com/anima-research/rayforce/runs/1' }], - [{ name: 'test', url: 'https://github.com/anima-research/rayforce/runs/2' }], + [{ name: 'lint', url: 'https://github.com/anima-research/example-repo/runs/1' }], + [{ name: 'test', url: 'https://github.com/anima-research/example-repo/runs/2' }], ]; const r = rig({ api, @@ -804,9 +804,9 @@ test('each list starts from lastPollAt until it has a cursor of its own, then ke assert.deepEqual(asked(), Array(6).fill('2026-10-13T00:00:00.000Z')); const start = '2026-10-14T00:00:00.000Z'; assert.deepEqual(r.state.pollCursors(), { - 'anima-research/rayforce:issues': { since: start }, - 'anima-research/rayforce:issue_comments': { since: start }, - 'anima-research/rayforce:review_comments': { since: start }, + 'anima-research/example-repo:issues': { since: start }, + 'anima-research/example-repo:issue_comments': { since: start }, + 'anima-research/example-repo:review_comments': { since: start }, 'other-org/other-repo:issues': { since: start }, 'other-org/other-repo:issue_comments': { since: start }, 'other-org/other-repo:review_comments': { since: start }, @@ -815,7 +815,7 @@ test('each list starts from lastPollAt until it has a cursor of its own, then ke // A list catching up reads from where it stopped; the others from their own. r.state.setPollCursors({ ...r.state.pollCursors(), - 'anima-research/rayforce:issues': { since: '2026-10-13T00:00:00.000Z', resumeAt: '2026-10-13T06:00:00Z' }, + 'anima-research/example-repo:issues': { since: '2026-10-13T00:00:00.000Z', resumeAt: '2026-10-13T06:00:00Z' }, }); r.api.calls.length = 0; await r.poller.pollOnce(); @@ -831,7 +831,7 @@ test('a failed delivery leaves every list’s cursor where it was', async () => throw new Error('host refused the event'); }, }); - const before = { 'anima-research/rayforce:issues': { since: '2026-10-13T00:00:00.000Z', resumeAt: '2026-10-13T06:00:00Z' } }; + const before = { 'anima-research/example-repo:issues': { since: '2026-10-13T00:00:00.000Z', resumeAt: '2026-10-13T06:00:00Z' } }; r.state.setPollCursors(before); const result = await r.poller.pollOnce(); @@ -856,7 +856,7 @@ test('a refused pass keeps the rows it announced as new, so a retry does not ope assert.deepEqual(first.delivered.map((e) => e.kind).sort(), ['issue.opened', 'pull.opened']); assert.equal(first.cursorAdvanced, false); assert.deepEqual(r.state.fresh, { - 'anima-research/rayforce:issues': { '41': '2026-10-13T12:00:00.000Z', '58': '2026-10-13T12:00:00.000Z' }, + 'anima-research/example-repo:issues': { '41': '2026-10-13T12:00:00.000Z', '58': '2026-10-13T12:00:00.000Z' }, }); // Both change before the retry: the issue is edited, the pull request pushed. @@ -916,8 +916,8 @@ test('a first pass whose read throws keeps the window it read', async () => { test('a stored position with milliseconds is read from its whole second', async () => { const r = rig({ cursor: '2026-10-13T00:00:00.700Z' }); r.state.setPollCursors({ - 'anima-research/rayforce:issues': { since: '2026-10-13T00:00:00.000Z', resumeAt: '2026-10-13T06:00:00.300Z' }, - 'anima-research/rayforce:issue_comments': { since: '2026-10-13T00:00:00.500Z' }, + 'anima-research/example-repo:issues': { since: '2026-10-13T00:00:00.000Z', resumeAt: '2026-10-13T06:00:00.300Z' }, + 'anima-research/example-repo:issue_comments': { since: '2026-10-13T00:00:00.500Z' }, }); await r.poller.pollOnce(); assert.deepEqual( diff --git a/test/smoke.test.ts b/test/smoke.test.ts index 8f500e0..afe6d8b 100644 --- a/test/smoke.test.ts +++ b/test/smoke.test.ts @@ -1,7 +1,7 @@ /** - * Skeleton smoke test (TASK-SRHKG.1.1). + * Skeleton smoke test. * - * Proves the three things the work packages .2 to .8 start from: the harness + * Proves the three things every module starts from: the harness * builds the server over a stream pair and tears it down, FakeGitHubApi * answers a read and records the call, and readConfig rejects a bad * environment by variable name without leaking a secret. @@ -142,7 +142,7 @@ test('readConfig rejects a missing GITHUB_REPOS by name', () => { const env: Env = { GITHUB_APP_ID: '12345', GITHUB_APP_PRIVATE_KEY_FILE: pem, - GITHUB_BOT_LOGIN: 'rayforce-clone[bot]', + GITHUB_BOT_LOGIN: 'example-agent[bot]', }; assert.throws( @@ -171,9 +171,9 @@ test('readConfig validates a whole environment', () => { GITHUB_WEBHOOK_SECRET_FILE: secret, GITHUB_WEBHOOK_PORT: '8443', GITHUB_POLL_INTERVAL_MS: '60000', - GITHUB_REPOS: 'anima-research/rayforce', + GITHUB_REPOS: 'anima-research/example-repo', GITHUB_PR_BASE_ALLOW: 'dev', - GITHUB_BOT_LOGIN: 'rayforce-clone[bot]', + GITHUB_BOT_LOGIN: 'example-agent[bot]', GITHUB_MAINTAINERS: 'human-one, human-two', }); @@ -205,8 +205,8 @@ test('readConfig ignores the webhook secret file while the listener is off', () GITHUB_APP_PRIVATE_KEY_FILE: pem, GITHUB_WEBHOOK_SECRET_FILE: secret, GITHUB_POLL_INTERVAL_MS: '60000', - GITHUB_REPOS: 'anima-research/rayforce', - GITHUB_BOT_LOGIN: 'rayforce-clone[bot]', + GITHUB_REPOS: 'anima-research/example-repo', + GITHUB_BOT_LOGIN: 'example-agent[bot]', }); assert.equal(config.webhookPort, null); assert.equal(config.webhookSecret, null); @@ -218,8 +218,8 @@ test('readConfig ignores the webhook secret file while the listener is off', () GITHUB_APP_PRIVATE_KEY_FILE: pem, GITHUB_WEBHOOK_SECRET_FILE: secret, GITHUB_WEBHOOK_PORT: '8443', - GITHUB_REPOS: 'anima-research/rayforce', - GITHUB_BOT_LOGIN: 'rayforce-clone[bot]', + GITHUB_REPOS: 'anima-research/example-repo', + GITHUB_BOT_LOGIN: 'example-agent[bot]', }), (err: unknown) => err instanceof ConfigError && /GITHUB_WEBHOOK_SECRET_FILE: .* is empty/.test(err.message), ); @@ -232,13 +232,13 @@ test('readConfig accepts several repositories of one owner, and a per-repository const base = { GITHUB_APP_ID: '12345', GITHUB_APP_PRIVATE_KEY_FILE: pem, - GITHUB_BOT_LOGIN: 'rayforce-clone[bot]', + GITHUB_BOT_LOGIN: 'example-agent[bot]', }; const config = readConfig({ ...base, - GITHUB_REPOS: 'anima-research/rayforce, anima-research/chronicle', - GITHUB_PR_BASE_ALLOW: 'anima-research/rayforce:dev,main', + GITHUB_REPOS: 'anima-research/example-repo, anima-research/chronicle', + GITHUB_PR_BASE_ALLOW: 'anima-research/example-repo:dev,main', }); assert.deepEqual(config.repos, [TEST_REPO, { owner: 'anima-research', name: 'chronicle' }]); assert.deepEqual(allowedBases(TEST_REPO, config.prBaseAllow), ['dev', 'main']); @@ -254,19 +254,19 @@ test('readConfig accepts several repositories of one owner, and a per-repository assert.fail('expected a ConfigError'); }; // One owner is one App installation and one token; a second owner is a second process. - assert.match(problems({ GITHUB_REPOS: 'anima-research/rayforce,other-org/thing' })[0], /must share one owner/); - assert.match(problems({ GITHUB_REPOS: 'anima-research/rayforce,anima-research/rayforce' })[0], /listed twice/); + assert.match(problems({ GITHUB_REPOS: 'anima-research/example-repo,other-org/thing' })[0], /must share one owner/); + assert.match(problems({ GITHUB_REPOS: 'anima-research/example-repo,anima-research/example-repo' })[0], /listed twice/); // A scoped entry has to name a repository the server serves, so a typo is a startup error. assert.match( - problems({ GITHUB_REPOS: 'anima-research/rayforce', GITHUB_PR_BASE_ALLOW: 'anima-research/chronicle:main' })[0], + problems({ GITHUB_REPOS: 'anima-research/example-repo', GITHUB_PR_BASE_ALLOW: 'anima-research/chronicle:main' })[0], /names a repository outside GITHUB_REPOS/, ); - assert.match(problems({ GITHUB_REPOS: 'anima-research/rayforce', GITHUB_PR_BASE_ALLOW: 'rayforce:' })[0], /neither a branch nor/); + assert.match(problems({ GITHUB_REPOS: 'anima-research/example-repo', GITHUB_PR_BASE_ALLOW: 'example-repo:' })[0], /neither a branch nor/); }); test('readConfig names every problem at once', () => { assert.throws( - () => readConfig({ GITHUB_REPOS: 'rayforce', GITHUB_WEBHOOK_PORT: '0' }), + () => readConfig({ GITHUB_REPOS: 'example-repo', GITHUB_WEBHOOK_PORT: '0' }), (err: unknown) => { assert.ok(err instanceof ConfigError); const named = err.problems.map((p) => p.split(':')[0]).sort(); @@ -292,8 +292,8 @@ test('readConfig refuses a secret file readable by anyone else', () => { readConfig({ GITHUB_APP_ID: '12345', GITHUB_APP_PRIVATE_KEY_FILE: pem, - GITHUB_REPOS: 'anima-research/rayforce', - GITHUB_BOT_LOGIN: 'rayforce-clone[bot]', + GITHUB_REPOS: 'anima-research/example-repo', + GITHUB_BOT_LOGIN: 'example-agent[bot]', }), (err: unknown) => { assert.ok(err instanceof ConfigError); @@ -304,14 +304,14 @@ test('readConfig refuses a secret file readable by anyone else', () => { }); test('channel and thread ids round-trip', () => { - assert.equal(channelId(ISSUE), 'github:anima-research/rayforce#7'); - assert.deepEqual(parseChannelId('github:anima-research/rayforce#7'), { repo: TEST_REPO, number: 7 }); + assert.equal(channelId(ISSUE), 'github:anima-research/example-repo#7'); + assert.deepEqual(parseChannelId('github:anima-research/example-repo#7'), { repo: TEST_REPO, number: 7 }); assert.equal(parseChannelId('slack:C1'), null); - assert.equal(parseChannelId('github:rayforce#7'), null); + assert.equal(parseChannelId('github:example-repo#7'), null); - assert.equal(channelLabel(ISSUE, 'a short title'), 'anima-research/rayforce#7: a short title'); + assert.equal(channelLabel(ISSUE, 'a short title'), 'anima-research/example-repo#7: a short title'); const long = 'x'.repeat(200); - const prefix = 'anima-research/rayforce#7: '; + const prefix = 'anima-research/example-repo#7: '; const label = channelLabel(ISSUE, long); assert.ok(label.startsWith(`${prefix}xxxx`)); assert.ok(label.includes('[truncated')); @@ -328,8 +328,8 @@ test('channel and thread ids round-trip', () => { }); test('the repo allow-list and its parsing', () => { - assert.deepEqual(parseRepoSlug('anima-research/rayforce'), TEST_REPO); - assert.equal(parseRepoSlug('rayforce'), null); + assert.deepEqual(parseRepoSlug('anima-research/example-repo'), TEST_REPO); + assert.equal(parseRepoSlug('example-repo'), null); assert.equal(repoAllowed(TEST_REPO, [TEST_REPO]), true); assert.equal(repoAllowed({ owner: 'other', name: 'repo' }, [TEST_REPO]), false); }); diff --git a/test/state.test.ts b/test/state.test.ts index b245295..2826799 100644 --- a/test/state.test.ts +++ b/test/state.test.ts @@ -1,6 +1,5 @@ /** - * state.ts — checkpoint lineage, rollback planning, and the atomic state file - * (TASK-SRHKG.1.7). + * state.ts — checkpoint lineage, rollback planning, and the atomic state file. * * Every test works in its own temp directory; nothing here touches the network * or a real state file path. @@ -15,7 +14,7 @@ import { test } from 'node:test'; import { GithubState, ROOT_BUCKET, STATE_VERSION, type GithubStateSnapshot } from '../src/state.js'; import { POLL_DEDUP_RING, POLL_HEAD_MEMORY, type Logger } from '../src/types.js'; -const REPO = 'anima-research/rayforce'; +const REPO = 'anima-research/example-repo'; function tempFile(name = 'state.json'): string { return join(mkdtempSync(join(tmpdir(), 'github-mcpl-state-')), name); @@ -272,17 +271,17 @@ test('the poll dedup ring remembers the last 2000 keys and no more', () => { test('the polled heads are bounded, oldest out first, and re-recording a head refreshes its place', async () => { const state = GithubState.empty(); - assert.equal(state.polledHead('github:anima-research/rayforce#58'), null); - state.setPolledHead('github:anima-research/rayforce#58', 'a'.repeat(40)); - assert.equal(state.polledHead('github:anima-research/rayforce#58'), 'a'.repeat(40)); + assert.equal(state.polledHead('github:anima-research/example-repo#58'), null); + state.setPolledHead('github:anima-research/example-repo#58', 'a'.repeat(40)); + assert.equal(state.polledHead('github:anima-research/example-repo#58'), 'a'.repeat(40)); // Last write wins: this is "the head the last pass saw", not a history. - state.setPolledHead('github:anima-research/rayforce#58', 'b'.repeat(40)); - assert.equal(state.polledHead('github:anima-research/rayforce#58'), 'b'.repeat(40)); + state.setPolledHead('github:anima-research/example-repo#58', 'b'.repeat(40)); + assert.equal(state.polledHead('github:anima-research/example-repo#58'), 'b'.repeat(40)); // Bounded, and the *oldest* entry is the one that goes. for (let i = 0; i < POLL_HEAD_MEMORY; i++) state.setPolledHead(`github:${REPO}#${1000 + i}`, 'c'.repeat(40)); assert.equal(Object.keys(state.snapshot().pollHeads).length, POLL_HEAD_MEMORY); - assert.equal(state.polledHead('github:anima-research/rayforce#58'), null); + assert.equal(state.polledHead('github:anima-research/example-repo#58'), null); assert.equal(state.polledHead(`github:${REPO}#${1000 + POLL_HEAD_MEMORY - 1}`), 'c'.repeat(40)); // Re-recording an unchanged head refreshes its place in the eviction order, @@ -328,10 +327,10 @@ test('poll cursors and announced rows round-trip through a flush, and a cursor w const file = tempFile(); const first = GithubState.load(file, capturingLogger()); first.setPollCursors({ - 'anima-research/rayforce:issues': { since: '2026-10-14T00:00:00.000Z', resumeAt: '2026-10-14T06:00:00Z' }, - 'anima-research/rayforce:issue_comments': { since: '2026-10-14T07:00:00.000Z' }, + 'anima-research/example-repo:issues': { since: '2026-10-14T00:00:00.000Z', resumeAt: '2026-10-14T06:00:00Z' }, + 'anima-research/example-repo:issue_comments': { since: '2026-10-14T07:00:00.000Z' }, }); - first.setPolledNew({ 'anima-research/rayforce:issues': { '7': '2026-10-14T01:00:00Z' } }); + first.setPolledNew({ 'anima-research/example-repo:issues': { '7': '2026-10-14T01:00:00Z' } }); await first.flush(); const onDisk = JSON.parse(readFileSync(file, 'utf8')) as Record; onDisk.pollCursors = { @@ -351,12 +350,12 @@ test('poll cursors and announced rows round-trip through a flush, and a cursor w const loaded = GithubState.load(file, capturingLogger()); assert.deepEqual(loaded.pollCursors(), { - 'anima-research/rayforce:issues': { since: '2026-10-14T00:00:00.000Z', resumeAt: '2026-10-14T06:00:00Z' }, - 'anima-research/rayforce:issue_comments': { since: '2026-10-14T07:00:00.000Z' }, + 'anima-research/example-repo:issues': { since: '2026-10-14T00:00:00.000Z', resumeAt: '2026-10-14T06:00:00Z' }, + 'anima-research/example-repo:issue_comments': { since: '2026-10-14T07:00:00.000Z' }, 'x/z:issue_comments': { since: '2026-10-14T07:00:00.000Z' }, }); assert.deepEqual(loaded.polledNew(), { - 'anima-research/rayforce:issues': { '7': '2026-10-14T01:00:00Z' }, + 'anima-research/example-repo:issues': { '7': '2026-10-14T01:00:00Z' }, 'x/y:issues': {}, 'x/z:issues': { '4': '2026-10-14T02:00:00Z' }, }); @@ -395,18 +394,18 @@ test('a lastPollAt that is null or left out loads quietly', () => { test('a polled head round-trips through a flush (amendment 2a)', async () => { const file = tempFile(); const first = GithubState.load(file, capturingLogger()); - first.setPolledHead('github:anima-research/rayforce#58', 'e'.repeat(40)); + first.setPolledHead('github:anima-research/example-repo#58', 'e'.repeat(40)); await first.flush(); const reloaded = GithubState.load(file, capturingLogger()); - assert.equal(reloaded.polledHead('github:anima-research/rayforce#58'), 'e'.repeat(40)); + assert.equal(reloaded.polledHead('github:anima-research/example-repo#58'), 'e'.repeat(40)); // A non-string value in the file is dropped rather than trusted. const raw = readSnapshot(file) as unknown as { pollHeads: Record }; - raw.pollHeads['github:anima-research/rayforce#59'] = 42; + raw.pollHeads['github:anima-research/example-repo#59'] = 42; writeFileSync(file, JSON.stringify(raw)); const third = GithubState.load(file, capturingLogger()); - assert.equal(third.polledHead('github:anima-research/rayforce#59'), null); - assert.equal(third.polledHead('github:anima-research/rayforce#58'), 'e'.repeat(40)); + assert.equal(third.polledHead('github:anima-research/example-repo#59'), null); + assert.equal(third.polledHead('github:anima-research/example-repo#58'), 'e'.repeat(40)); }); test('a checkpoint abandoned by an earlier rollback is refused, not accepted empty', () => { diff --git a/test/tags.test.ts b/test/tags.test.ts index 716528c..ba96eff 100644 --- a/test/tags.test.ts +++ b/test/tags.test.ts @@ -28,8 +28,8 @@ import { type GitHubTagContext, } from '../src/tags.js'; -const REPO: RepoRef = { owner: 'anima-research', name: 'rayforce' }; -const BOT = 'rayforce-clone[bot]'; +const REPO: RepoRef = { owner: 'anima-research', name: 'example-repo' }; +const BOT = 'example-agent[bot]'; const ISSUE: ThreadRef = { repo: REPO, number: 7, kind: 'issue' }; const PULL: ThreadRef = { repo: REPO, number: 12, kind: 'pull' }; @@ -47,7 +47,7 @@ function event(kind: NormalizedEventKind, over: Partial = {}): kind, thread, actor: { login: 'alice', isBot: false }, - html_url: 'https://github.com/anima-research/rayforce/issues/7#issuecomment-1', + html_url: 'https://github.com/anima-research/example-repo/issues/7#issuecomment-1', ...over, }; } @@ -226,7 +226,7 @@ test('a close, reopen, push or new commits never count the mention in a body the // The 2026-10-15 rehearsal: a polled issue.closed re-carried the issue's // body, and its old mention woke the bot that had just closed it. for (const kind of NORMALIZED_EVENT_KINDS) { - const { tags, metadata } = tagsFor(event(kind, { body: '@rayforce-clone please look' }), ctx()); + const { tags, metadata } = tagsFor(event(kind, { body: '@example-agent please look' }), ctx()); const counts = TEXT_KINDS.includes(kind); assert.equal(tags.includes('chat:mention'), counts, `${kind}: chat:mention`); assert.equal(metadata.mentioned, counts, `${kind}: metadata.mentioned`); @@ -235,10 +235,10 @@ test('a close, reopen, push or new commits never count the mention in a body the test('chat:ambient never co-occurs with chat:mention or chat:reply, on any kind', () => { const addressing: Array<[string, Partial, Partial]> = [ - ['mention', {}, { body: `@rayforce-clone please look` }], + ['mention', {}, { body: `@example-agent please look` }], ['own thread', { isOwnThread: true }, {}], ['reply to self', { isReplyToSelf: true }, {}], - ['mention and reply', { isOwnThread: true }, { body: `@rayforce-clone ping` }], + ['mention and reply', { isOwnThread: true }, { body: `@example-agent ping` }], ]; for (const kind of NORMALIZED_EVENT_KINDS) { for (const [label, ctxOver, eventOver] of addressing) { @@ -257,7 +257,7 @@ test('chat:ambient never co-occurs with chat:mention or chat:reply, on any kind' }); test('chat:addressed is never emitted — the host expands it (RFC-001 §4.1)', () => { - const { tags } = tagsFor(event('comment.created', { body: '@rayforce-clone hi' }), ctx()); + const { tags } = tagsFor(event('comment.created', { body: '@example-agent hi' }), ctx()); assert.ok(!tags.includes('chat:addressed')); assert.ok(!EMITTED_TAGS.includes('chat:addressed')); }); @@ -272,7 +272,7 @@ test('chat:dm is never emitted', () => { test('exactly one sender tag, and from-self only for the bot login', () => { const senders: Array<[NormalizedEvent['actor'], string]> = [ [{ login: BOT, isBot: true }, 'chat:from-self'], - [{ login: 'rayforce-clone', isBot: true }, 'chat:from-bot'], + [{ login: 'example-agent', isBot: true }, 'chat:from-bot'], [{ login: 'other-app[bot]', isBot: true }, 'chat:from-bot'], [{ login: 'alice', isBot: false }, 'chat:from-human'], ]; @@ -290,28 +290,28 @@ test('exactly one sender tag, and from-self only for the bot login', () => { test('mentionsBot accepts both spellings of an App login and rejects near misses', () => { const yes = [ - '@rayforce-clone please look', - 'cc @rayforce-clone[bot]', - 'hey @RAYFORCE-CLONE', - 'line\n@rayforce-clone\n', - '(@rayforce-clone)', + '@example-agent please look', + 'cc @example-agent[bot]', + 'hey @EXAMPLE-AGENT', + 'line\n@example-agent\n', + '(@example-agent)', ]; const no = [ - 'rayforce-clone please look', - 'mail me at bob@rayforce-clone.example', - '@rayforce-clone-staging is a different app', - '@rayforce-cloneX', - 'https://example.invalid/@rayforce-clone', + 'example-agent please look', + 'mail me at bob@example-agent.example', + '@example-agent-staging is a different app', + '@example-agentX', + 'https://example.invalid/@example-agent', '', ]; for (const body of yes) assert.ok(mentionsBot(body, BOT), JSON.stringify(body)); for (const body of no) assert.ok(!mentionsBot(body, BOT), JSON.stringify(body)); assert.ok(!mentionsBot(undefined, BOT)); - assert.ok(!mentionsBot('@rayforce-clone', '')); + assert.ok(!mentionsBot('@example-agent', '')); }); test('a mention is read from the body only, not the title', () => { - const titled = event('issue.opened', { title: '@rayforce-clone fix this', body: 'details' }); + const titled = event('issue.opened', { title: '@example-agent fix this', body: 'details' }); assert.ok(!tagsFor(titled, ctx()).metadata.mentioned); }); @@ -396,7 +396,7 @@ test('isReviewRequested is set for pull.review_requested and suppressible', () = test('metadata carries the scalar fields of the thread, actor and event', () => { const { metadata } = tagsFor( event('review_comment.created', { - body: '@rayforce-clone see https://example.invalid/x', + body: '@example-agent see https://example.invalid/x', sha: 'e'.repeat(40), }), ctx({ isOwnThread: true, isReplyToSelf: true, maintainers: ['alice'] }), @@ -409,12 +409,12 @@ test('metadata carries the scalar fields of the thread, actor and event', () => isCiFailure: false, fromSelf: false, isMaintainer: true, - repo: 'anima-research/rayforce', + repo: 'anima-research/example-repo', number: 12, kind: 'pull', actor: 'alice', sha: 'e'.repeat(40), - htmlUrl: 'https://github.com/anima-research/rayforce/issues/7#issuecomment-1', + htmlUrl: 'https://github.com/anima-research/example-repo/issues/7#issuecomment-1', }); }); diff --git a/test/toolsComments.test.ts b/test/toolsComments.test.ts index fa01bc0..434aab6 100644 --- a/test/toolsComments.test.ts +++ b/test/toolsComments.test.ts @@ -1,6 +1,5 @@ /** - * tools-comments.ts — the `github.comments` tools and `state/rollback` - * (TASK-SRHKG.1.7). + * tools-comments.ts — the `github.comments` tools and `state/rollback`. * * Everything runs against `FakeGitHubApi` and an in-memory {@link GithubState}; * no octokit, no network. The tests drive the tools the way .9's routing will: @@ -116,16 +115,16 @@ test('comment posts and books the new comment under the current checkpoint', asy f.api.seedIssue({ number: 7 }); const base = f.state.createCheckpoint(); - const result = await call(f, TOOL_COMMENT, { thread: 'anima-research/rayforce#7', body: 'hello' }); + const result = await call(f, TOOL_COMMENT, { thread: 'anima-research/example-repo#7', body: 'hello' }); assert.notEqual(result.isError, true); assert.deepEqual(await bodies(f.api, ISSUE), ['hello']); const booked = f.state.snapshot().created[base.id]; assert.equal(booked.length, 1); - assert.equal(booked[0].repo, 'anima-research/rayforce'); + assert.equal(booked[0].repo, 'anima-research/example-repo'); assert.equal(booked[0].review, false); assert.deepEqual(booked[0].thread, { number: 7, kind: 'issue' }); - assert.match(text(result), /commented on anima-research\/rayforce#7/); + assert.match(text(result), /commented on anima-research\/example-repo#7/); }); test('comment accepts the short thread forms and refuses a foreign repository', async () => { @@ -200,7 +199,7 @@ test('edit_own_comment rewrites the bot\'s comment and books the previous body', assert.notEqual(result.isError, true); assert.deepEqual(await bodies(f.api, ISSUE), ['second draft']); assert.deepEqual(f.state.snapshot().edited[base.id], [ - { repo: 'anima-research/rayforce', commentId: own.id, previousBody: 'first draft', review: false, thread: { number: 7, kind: 'issue' } }, + { repo: 'anima-research/example-repo', commentId: own.id, previousBody: 'first draft', review: false, thread: { number: 7, kind: 'issue' } }, ]); }); @@ -212,7 +211,7 @@ test('edit_own_comment refuses a comment the bot did not write', async () => { const result = await call(f, TOOL_EDIT_OWN_COMMENT, { thread: 7, commentId: foreign.id, body: 'hijacked' }); assert.equal(result.isError, true); - assert.match(text(result), /written by human-one, not rayforce-clone\[bot\]/); + assert.match(text(result), /written by human-one, not example-agent\[bot\]/); assert.deepEqual(await bodies(f.api, ISSUE), ['a human wrote this']); assert.equal(f.api.callsTo('updateComment').length, 0); assert.deepEqual(f.state.snapshot().edited, {}); @@ -243,7 +242,7 @@ test('edit_own_comment reports a comment that is not on the thread', async () => f.state.createCheckpoint(); const result = await call(f, TOOL_EDIT_OWN_COMMENT, { thread: 7, commentId: 999, body: 'x' }); assert.equal(result.isError, true); - assert.match(text(result), /no comment 999 on anima-research\/rayforce#7/); + assert.match(text(result), /no comment 999 on anima-research\/example-repo#7/); }); test('delete_own_comment deletes the bot\'s comment and books an undeletable record', async () => { @@ -266,7 +265,7 @@ test('delete_own_comment deletes the bot\'s comment and books an undeletable rec assert.notEqual(result.isError, true); assert.deepEqual(await bodies(f.api, ISSUE), ['a human wrote this']); assert.deepEqual(f.state.snapshot().deleted[current as string], [ - { repo: 'anima-research/rayforce', commentId: own.id, previousBody: 'never mind', review: false, thread: { number: 7, kind: 'issue' } }, + { repo: 'anima-research/example-repo', commentId: own.id, previousBody: 'never mind', review: false, thread: { number: 7, kind: 'issue' } }, ]); }); @@ -380,7 +379,7 @@ test('rollback across a comment someone else removed reports what it could not u [['delete', gone.id]], ); assert.match(result.notUndone[0].reason ?? '', /no such comment/); - assert.match(result.reason ?? '', /undid 1 of 2 actions; could not undo anima-research\/rayforce comment/); + assert.match(result.reason ?? '', /undid 1 of 2 actions; could not undo anima-research\/example-repo comment/); assert.deepEqual(await bodies(f.api, ISSUE), []); assert.equal(f.logs.length, 1, 'the receipt also goes to stderr'); }); @@ -519,7 +518,7 @@ test('rollback refuses to touch a repository outside the allow-list, and keeps t const f = fixture(); f.api.seedIssue({ number: 7 }); const base = f.state.createCheckpoint(); - await call(f, TOOL_COMMENT, { thread: 7, body: 'written while rayforce was allowed' }); + await call(f, TOOL_COMMENT, { thread: 7, body: 'written while example-repo was allowed' }); const created = (await f.api.listComments(ISSUE))[0]; f.api.clearCalls(); @@ -533,9 +532,9 @@ test('rollback refuses to touch a repository outside the allow-list, and keeps t refused.notUndone.map((i) => [i.action, i.commentId]), [['delete', created.id]], ); - assert.match(refused.notUndone[0].reason ?? '', /anima-research\/rayforce is not in GITHUB_REPOS/); + assert.match(refused.notUndone[0].reason ?? '', /anima-research\/example-repo is not in GITHUB_REPOS/); assert.deepEqual(f.api.callLog(), [], 'no api call is made for a refused repository'); - assert.deepEqual(await bodies(f.api, ISSUE), ['written while rayforce was allowed']); + assert.deepEqual(await bodies(f.api, ISSUE), ['written while example-repo was allowed']); // The record survived the refusal, so a correctly configured retry still // undoes it: a refusal is not a silent discard. @@ -865,7 +864,7 @@ test('edit_own_comment refuses a colliding id until the caller names the endpoin ]); assert.deepEqual(reviewCase.f.state.snapshot().edited[reviewCp.id], [ { - repo: 'anima-research/rayforce', + repo: 'anima-research/example-repo', commentId: reviewCase.shared, previousBody: 'review comment', review: true, @@ -917,7 +916,7 @@ test('delete_own_comment refuses a colliding id until the caller names the endpo assert.deepEqual(reviewCase.f.api.callsTo('deleteComment')[0].args, [TEST_REPO, reviewCase.shared, true]); assert.deepEqual(reviewCase.f.state.snapshot().deleted[reviewCp.id], [ { - repo: 'anima-research/rayforce', + repo: 'anima-research/example-repo', commentId: reviewCase.shared, previousBody: 'review comment', review: true, diff --git a/test/toolsIssuesPr.test.ts b/test/toolsIssuesPr.test.ts index ec3aa1c..e733249 100644 --- a/test/toolsIssuesPr.test.ts +++ b/test/toolsIssuesPr.test.ts @@ -1,5 +1,5 @@ /** - * github.issues and github.pr (TASK-SRHKG.1.6). + * github.issues and github.pr. * * The two refusals the contract turns on are asserted twice over: a base * outside GITHUB_PR_BASE_ALLOW on creation and on a retarget, and a repository @@ -83,7 +83,7 @@ test('create_issue opens an issue in the default repository', async () => { const out = ok( await issuesToolModule.call('create_issue', { title: 'Flake in CI', body: 'reproduces on dev' }, ctx(api)), ); - assert.match(out, /issue anima-research\/rayforce#\d+ \[open\] Flake in CI/); + assert.match(out, /issue anima-research\/example-repo#\d+ \[open\] Flake in CI/); assert.match(out, /reproduces on dev/); assert.deepEqual(api.callsTo('createIssue')[0].args, [TEST_REPO, { title: 'Flake in CI', body: 'reproduces on dev' }]); }); @@ -118,7 +118,7 @@ test('close_issue closes with a reason', async () => { api.seedIssue({ number: 7 }); const out = ok(await issuesToolModule.call('close_issue', { number: 7, reason: 'not_planned' }, ctx(api))); - assert.match(out, /issue anima-research\/rayforce#7 \[closed \(not_planned\)\]/); + assert.match(out, /issue anima-research\/example-repo#7 \[closed \(not_planned\)\]/); assert.deepEqual(api.callLog(), ['updateIssue']); assert.deepEqual(api.callsTo('updateIssue')[0].args, [ISSUE, { state: 'closed', stateReason: 'not_planned' }]); }); @@ -167,7 +167,7 @@ test('an issue write outside GITHUB_REPOS is refused by name, before any call', await issuesToolModule.call('create_issue', { repo: 'someone-else/not-ours', title: 'hi' }, ctx(api)), ); assert.match(out, /create_issue refused: repository someone-else\/not-ours is not in GITHUB_REPOS/); - assert.match(out, /allowed: anima-research\/rayforce/); + assert.match(out, /allowed: anima-research\/example-repo/); assert.equal(api.calls.length, 0); }); @@ -197,7 +197,7 @@ test('create_pull_request opens a pull request against an allowed base', async ( ctx(api), ), ); - assert.match(out, /pull anima-research\/rayforce#\d+ \[open, draft\] Fix the flake/); + assert.match(out, /pull anima-research\/example-repo#\d+ \[open, draft\] Fix the flake/); assert.deepEqual(api.callsTo('createPullRequest')[0].args, [ TEST_REPO, { head: 'fix-flake', base: 'dev', title: 'Fix the flake', body: 'why', draft: true }, @@ -214,7 +214,7 @@ test('create_pull_request refuses a base outside GITHUB_PR_BASE_ALLOW', async () ), ); assert.match(out, /create_pull_request refused: base branch "main" is not in GITHUB_PR_BASE_ALLOW/); - assert.match(out, /allowed on anima-research\/rayforce: dev/); + assert.match(out, /allowed on anima-research\/example-repo: dev/); assert.equal(api.calls.length, 0, 'nothing reached the API'); }); @@ -227,7 +227,7 @@ test('an empty GITHUB_PR_BASE_ALLOW means no pull request may be opened', async ctx(api, { prBaseAllow: [] }), ), ); - assert.match(out, /GITHUB_PR_BASE_ALLOW allows no branch on anima-research\/rayforce, so none may be targeted/); + assert.match(out, /GITHUB_PR_BASE_ALLOW allows no branch on anima-research\/example-repo, so none may be targeted/); assert.equal(api.calls.length, 0); }); @@ -287,7 +287,7 @@ test('request_review requests exactly the logins given', async () => { const out = ok( await prToolModule.call('request_review', { number: 8, logins: ['human-one', 'human-two'] }, ctx(api)), ); - assert.equal(out, 'requested a review from human-one, human-two on anima-research/rayforce#8'); + assert.equal(out, 'requested a review from human-one, human-two on anima-research/example-repo#8'); assert.deepEqual(api.callsTo('requestReviewers')[0].args, [PULL, ['human-one', 'human-two']]); }); @@ -310,14 +310,14 @@ test('with several repositories, the base allow-list is read per repository', as const second = { owner: TEST_REPO.owner, name: 'chronicle' }; const two = ctx(api, { repos: [TEST_REPO, second], - // rayforce merges into dev, everything else into main. - prBaseAllow: ['anima-research/rayforce:dev', 'main'], + // example-repo merges into dev, everything else into main. + prBaseAllow: ['anima-research/example-repo:dev', 'main'], }); const create = (repo: string, base: string) => prToolModule.call('create_pull_request', { repo, head: 'fix', base, title: 'Fix' }, two); - ok(await create('anima-research/rayforce', 'dev')); - ok(await create('anima-research/rayforce', 'main')); + ok(await create('anima-research/example-repo', 'dev')); + ok(await create('anima-research/example-repo', 'main')); ok(await create('anima-research/chronicle', 'main')); const refused = failure(await create('anima-research/chronicle', 'dev')); assert.match(refused, /base branch "dev" is not in GITHUB_PR_BASE_ALLOW \(allowed on anima-research\/chronicle: main\)/); diff --git a/test/toolsRead.test.ts b/test/toolsRead.test.ts index 03de272..75718f0 100644 --- a/test/toolsRead.test.ts +++ b/test/toolsRead.test.ts @@ -1,5 +1,5 @@ /** - * github.read, and the feature-set declarations (TASK-SRHKG.1.6). + * github.read, and the feature-set declarations. * * Everything runs against FakeGitHubApi: no network, no octokit, no process * ever leaves the test. The declarations are asserted here rather than in a @@ -202,7 +202,7 @@ test('get_issue reads an issue and defaults the repository', async () => { api.seedIssue({ number: 7, title: 'Flaky test', body: 'it fails on CI', labels: ['bug'] }); const out = ok(await readToolModule.call('get_issue', { number: 7 }, ctx(api))); - assert.match(out, /issue anima-research\/rayforce#7 \[open\] Flaky test/); + assert.match(out, /issue anima-research\/example-repo#7 \[open\] Flaky test/); assert.match(out, /labels: bug/); assert.match(out, /it fails on CI/); // No repo argument: the single allowed repository is what reached the API. @@ -212,14 +212,14 @@ test('get_issue reads an issue and defaults the repository', async () => { test('with several repositories, a read has to name one', async () => { const api = new FakeGitHubApi(); const second = { owner: TEST_REPO.owner, name: 'chronicle' }; - api.seedIssue({ number: 7, title: 'In rayforce' }); + api.seedIssue({ number: 7, title: 'In example-repo' }); api.seedIssue({ repo: second, number: 7, title: 'In chronicle' }); const two = ctx(api, { repos: [TEST_REPO, second] }); assert.match(failure(await readToolModule.call('get_issue', { number: 7 }, two)), /"repo" is required: GITHUB_REPOS names several/); assert.match(failure(await readToolModule.call('get_issue', { thread: '#7' }, two)), /name the repository as owner\/name#7/); assert.match(ok(await readToolModule.call('get_issue', { repo: 'anima-research/chronicle', number: 7 }, two)), /In chronicle/); - assert.match(ok(await readToolModule.call('get_issue', { thread: 'anima-research/rayforce#7' }, two)), /In rayforce/); + assert.match(ok(await readToolModule.call('get_issue', { thread: 'anima-research/example-repo#7' }, two)), /In example-repo/); }); test('get_pull_request reports refs, mergeability and checks', async () => { @@ -234,7 +234,7 @@ test('get_pull_request reports refs, mergeability and checks', async () => { }); const out = ok(await readToolModule.call('get_pull_request', { number: 8 }, ctx(api))); - assert.match(out, /pull anima-research\/rayforce#8 \[open\] Fix the flake/); + assert.match(out, /pull anima-research\/example-repo#8 \[open\] Fix the flake/); assert.match(out, /fix-flake \(a{40}\) → dev/); assert.match(out, /mergeable: true/); assert.match(out, /reviewers requested: human-two/); @@ -249,7 +249,7 @@ test('list_comments passes since through and renders ids and authors', async () api.seedComment(ISSUE, { id: 1002, body: 'second', updatedAt: '2026-10-05T00:00:00.000Z' }); const all = ok(await readToolModule.call('list_comments', { number: 7 }, ctx(api))); - assert.match(all, /2 comments on anima-research\/rayforce#7/); + assert.match(all, /2 comments on anima-research\/example-repo#7/); assert.match(all, /id 1001 by human-one/); const since = ok( @@ -281,7 +281,7 @@ test('list_review_threads renders path, line, side and resolution', async () => api.seedReviewThread(PULL, { id: 'RT_2', path: 'src/old.rs', line: 3, side: 'LEFT', isResolved: true }); const out = ok(await readToolModule.call('list_review_threads', { number: 8 }, ctx(api))); - assert.match(out, /2 review threads on anima-research\/rayforce#8/); + assert.match(out, /2 review threads on anima-research\/example-repo#8/); assert.match(out, /RT_1 src\/lib\.rs:42 RIGHT unresolved/); assert.match(out, /RT_2 src\/old\.rs:3 LEFT resolved/); }); @@ -295,7 +295,7 @@ test('list_pull_request_files renders names and stats only', async () => { ]); const out = ok(await readToolModule.call('list_pull_request_files', { number: 8 }, ctx(api))); - assert.match(out, /2 changed files on anima-research\/rayforce#8/); + assert.match(out, /2 changed files on anima-research\/example-repo#8/); assert.match(out, /modified src\/lib\.rs \+10 -2/); assert.match(out, /removed docs\/old\.md \+0 -40/); }); @@ -321,7 +321,7 @@ test('search_issues bounds the limit to the rendered list bound', async () => { api.seedPullRequest({ number: 8, title: 'fix flaky test' }); const out = ok(await readToolModule.call('search_issues', { query: 'flaky' }, ctx(api))); - assert.match(out, /2 matches in anima-research\/rayforce for "flaky"/); + assert.match(out, /2 matches in anima-research\/example-repo for "flaky"/); assert.match(out, /issue #7 \[open\] flaky test/); assert.match(out, /pull #8 \[open\] fix flaky test/); assert.deepEqual(api.callsTo('searchIssues')[0].args, [TEST_REPO, 'flaky', LIMITS.listEntries]); @@ -334,7 +334,7 @@ test('search_issues bounds the limit to the rendered list bound', async () => { test('search_issues says so when nothing matches', async () => { const api = new FakeGitHubApi(); const out = ok(await readToolModule.call('search_issues', { query: 'nothing' }, ctx(api))); - assert.match(out, /no matches in anima-research\/rayforce for "nothing"/); + assert.match(out, /no matches in anima-research\/example-repo for "nothing"/); }); // ───────────────────────────────────────────────────────────────────────────── @@ -347,20 +347,20 @@ test('a repository outside GITHUB_REPOS is refused by name, before any call', as const out = failure(await readToolModule.call('get_issue', { repo: 'someone-else/not-ours', number: 7 }, ctx(api))); assert.match(out, /get_issue refused: repository someone-else\/not-ours is not in GITHUB_REPOS/); - assert.match(out, /allowed: anima-research\/rayforce/); + assert.match(out, /allowed: anima-research\/example-repo/); assert.equal(api.calls.length, 0); }); test('the allowed repository may be named explicitly', async () => { const api = new FakeGitHubApi(); api.seedIssue({ number: 7 }); - ok(await readToolModule.call('get_issue', { repo: 'anima-research/rayforce', number: 7 }, ctx(api))); + ok(await readToolModule.call('get_issue', { repo: 'anima-research/example-repo', number: 7 }, ctx(api))); assert.deepEqual(api.callsTo('getIssue')[0].args, [ISSUE]); }); test('a malformed repo argument is rejected as a bad argument, not as a refusal', async () => { const api = new FakeGitHubApi(); - const out = failure(await readToolModule.call('get_issue', { repo: 'rayforce', number: 7 }, ctx(api))); + const out = failure(await readToolModule.call('get_issue', { repo: 'example-repo', number: 7 }, ctx(api))); assert.match(out, /get_issue: "repo" must be owner\/name/); assert.equal(api.calls.length, 0); }); @@ -511,7 +511,7 @@ test('labels are bounded in count and in length', async () => { test('an echoed search query is bounded', async () => { const api = new FakeGitHubApi(); const out = ok(await readToolModule.call('search_issues', { query: 'q'.repeat(100_000) }, ctx(api))); - assert.match(out, /^no matches in anima-research\/rayforce for "q+… \[truncated \d+ chars\]"$/); + assert.match(out, /^no matches in anima-research\/example-repo for "q+… \[truncated \d+ chars\]"$/); assert.ok(out.length < 400, `the echo stayed bounded (${out.length} chars)`); }); @@ -530,7 +530,7 @@ test('a pull request bounds its reviewer list, its refs and its URL', async () = head: { ref: 'h'.repeat(5_000), sha: 's'.repeat(5_000) }, base: { ref: 'dev', sha: 'b'.repeat(40) }, author: { login: 'a'.repeat(5_000), isBot: false }, - html_url: `https://github.com/anima-research/rayforce/pull/8?${'q'.repeat(50_000)}`, + html_url: `https://github.com/anima-research/example-repo/pull/8?${'q'.repeat(50_000)}`, requestedReviewers: Array.from({ length: LIMITS.listEntries + 3 }, (_, i) => `reviewer-${i}${'y'.repeat(500)}`), }); diff --git a/test/toolsReview.test.ts b/test/toolsReview.test.ts index 5b906a1..c0186ae 100644 --- a/test/toolsReview.test.ts +++ b/test/toolsReview.test.ts @@ -1,5 +1,5 @@ /** - * github.review (TASK-SRHKG.1.6). + * github.review. * * Three things are load-bearing here and each is asserted directly: * @@ -98,7 +98,7 @@ test('create_review submits the inline comments with the exact positions', async { path: 'src/old.rs', line: 3, side: 'LEFT', body: 'why was this removed?' }, ], } satisfies CreateReviewInput); - assert.match(out, /submitted commented review \d+ on anima-research\/rayforce#8 with 2 inline comments/); + assert.match(out, /submitted commented review \d+ on anima-research\/example-repo#8 with 2 inline comments/); // The fake opened one review thread per inline comment, at those positions. const threads = await api.listReviewThreads(PULL); @@ -162,8 +162,8 @@ test('APPROVE on the bot’s own pull request is refused by name, nothing submit api.seedPullRequest({ number: 8, author: { login: TEST_BOT.login, isBot: true } }); const out = failure(await reviewToolModule.call('create_review', { number: 8, event: 'APPROVE' }, ctx(api))); - assert.match(out, /create_review refused: APPROVE on anima-research\/rayforce#8 is this bot's own pull request/); - assert.match(out, /author rayforce-clone\[bot\]/); + assert.match(out, /create_review refused: APPROVE on anima-research\/example-repo#8 is this bot's own pull request/); + assert.match(out, /author example-agent\[bot\]/); assert.deepEqual(api.callLog(), ['getPullRequest'], 'the review was never created'); assert.equal(api.submittedReviews().length, 0); }); @@ -175,13 +175,13 @@ test('REQUEST_CHANGES on the bot’s own pull request is refused too', async () const out = failure( await reviewToolModule.call('create_review', { number: 8, event: 'REQUEST_CHANGES', body: 'no' }, ctx(api)), ); - assert.match(out, /refused: REQUEST_CHANGES on anima-research\/rayforce#8 is this bot's own pull request/); + assert.match(out, /refused: REQUEST_CHANGES on anima-research\/example-repo#8 is this bot's own pull request/); assert.equal(api.submittedReviews().length, 0); }); test('the own-pull-request check is case-insensitive, as GitHub logins are', async () => { const api = new FakeGitHubApi(); - api.seedPullRequest({ number: 8, author: { login: 'RayForce-Clone[Bot]', isBot: true } }); + api.seedPullRequest({ number: 8, author: { login: 'Example-Agent[Bot]', isBot: true } }); const out = failure(await reviewToolModule.call('create_review', { number: 8, event: 'APPROVE' }, ctx(api))); assert.match(out, /refused: APPROVE on/); @@ -296,14 +296,14 @@ test('resolve_thread and unresolve_thread move a thread of the named pull reques // `repo` omitted: the single allowed repository is the one membership is // established in. const resolved = ok(await reviewToolModule.call('resolve_thread', { number: 8, threadId: 'RT_kwDO' }, ctx(api))); - assert.equal(resolved, 'resolved review thread RT_kwDO on anima-research/rayforce#8'); + assert.equal(resolved, 'resolved review thread RT_kwDO on anima-research/example-repo#8'); assert.deepEqual(api.callLog(), ['listReviewThreads', 'resolveReviewThread']); assert.deepEqual(api.callsTo('listReviewThreads')[0].args, [PULL]); assert.deepEqual(api.callsTo('resolveReviewThread')[0].args, ['RT_kwDO']); assert.equal((await api.listReviewThreads(PULL))[0].isResolved, true); const unresolved = ok(await reviewToolModule.call('unresolve_thread', { number: 8, threadId: 'RT_kwDO' }, ctx(api))); - assert.equal(unresolved, 'unresolved review thread RT_kwDO on anima-research/rayforce#8'); + assert.equal(unresolved, 'unresolved review thread RT_kwDO on anima-research/example-repo#8'); assert.deepEqual(api.callsTo('unresolveReviewThread')[0].args, ['RT_kwDO']); assert.equal((await api.listReviewThreads(PULL))[0].isResolved, false); }); @@ -316,11 +316,11 @@ test('resolve_thread accepts the allowed repository named explicitly', async () const out = ok( await reviewToolModule.call( 'resolve_thread', - { repo: 'anima-research/rayforce', number: 8, threadId: 'RT_kwDO' }, + { repo: 'anima-research/example-repo', number: 8, threadId: 'RT_kwDO' }, ctx(api), ), ); - assert.equal(out, 'resolved review thread RT_kwDO on anima-research/rayforce#8'); + assert.equal(out, 'resolved review thread RT_kwDO on anima-research/example-repo#8'); assert.equal((await api.listReviewThreads(PULL))[0].isResolved, true); }); @@ -337,7 +337,7 @@ test('resolve_thread refuses a node id that is not on the pull request', async ( const out = failure(await reviewToolModule.call('resolve_thread', { number: 8, threadId: 'RT_foreign' }, ctx(api))); assert.match( out, - /resolve_thread refused: review thread "RT_foreign" is not one of the 1 review thread on anima-research\/rayforce#8/, + /resolve_thread refused: review thread "RT_foreign" is not one of the 1 review thread on anima-research\/example-repo#8/, ); // Nothing was mutated: not the foreign thread, not the allowed one. assert.deepEqual(api.callLog(), ['listReviewThreads']); @@ -366,7 +366,7 @@ test('a thread id on another pull request of the allowed repository is refused', api.seedReviewThread({ repo: TEST_REPO, number: 9, kind: 'pull' }, { id: 'RT_nine', isResolved: false }); const out = failure(await reviewToolModule.call('resolve_thread', { number: 8, threadId: 'RT_nine' }, ctx(api))); - assert.match(out, /refused: review thread "RT_nine" is not one of the 0 review threads on anima-research\/rayforce#8/); + assert.match(out, /refused: review thread "RT_nine" is not one of the 0 review threads on anima-research\/example-repo#8/); assert.equal(api.callsTo('resolveReviewThread').length, 0); }); diff --git a/test/webhook.test.ts b/test/webhook.test.ts index dc3affb..b5bc4c5 100644 --- a/test/webhook.test.ts +++ b/test/webhook.test.ts @@ -1,5 +1,5 @@ /** - * Webhook listener (TASK-SRHKG.1.3): the order of the checks is the test. + * Webhook listener: the order of the checks is the test. * * Every case below but the dedup ring's goes through a real `node:http` server * on a loopback port and a real `fetch`, because the properties under test — "rejected before @@ -331,7 +331,7 @@ test('a check_suite delivery reaches the listener with the failed job names', as sha: HEAD, conclusion: 'failure', total: 2, - failed: [{ name: 'clippy', url: 'https://github.com/anima-research/rayforce/runs/2700001235' }], + failed: [{ name: 'clippy', url: 'https://github.com/anima-research/example-repo/runs/2700001235' }], }); const r = await rig(t, { api }); await r.deliver('check_suite', fixture('check_suite.completed.json'), 'suite-1'); @@ -340,7 +340,7 @@ test('a check_suite delivery reaches the listener with the failed job names', as assert.equal(r.events[0].kind, 'checks.completed'); assert.equal(r.events[0].sha, HEAD); assert.deepEqual(r.events[0].checks?.failed, [ - { name: 'clippy', url: 'https://github.com/anima-research/rayforce/runs/2700001235' }, + { name: 'clippy', url: 'https://github.com/anima-research/example-repo/runs/2700001235' }, ]); }); @@ -395,7 +395,7 @@ function config(overrides: Partial = {}): Config { repos: [TEST_REPO], prBaseAllow: ['dev'], stateFile: null, - botLogin: 'rayforce-clone[bot]', + botLogin: 'example-agent[bot]', maintainers: [], ...overrides, }; @@ -438,7 +438,7 @@ test('a check_run and a check_suite delivery for one sha produce one checks even sha: HEAD, conclusion: 'failure', total: 2, - failed: [{ name: 'test (ubuntu-latest)', url: 'https://github.com/anima-research/rayforce/runs/2700001234' }], + failed: [{ name: 'test (ubuntu-latest)', url: 'https://github.com/anima-research/example-repo/runs/2700001234' }], }); const r = await rig(t, { api }); await r.deliver('check_suite', fixture('check_suite.completed.json'), 'suite-c'); @@ -509,8 +509,8 @@ test('two failing suites finishing separately deliver one event naming every fai // The regression the reviewer reproduced: the first suite's delivery found // the sha still pending with [lint], emitted it, and the sha gate then // suppressed the final [lint, test] because it ranked the same. - const LINT = { name: 'lint', url: 'https://github.com/anima-research/rayforce/runs/1' }; - const TEST_JOB = { name: 'test', url: 'https://github.com/anima-research/rayforce/runs/2' }; + const LINT = { name: 'lint', url: 'https://github.com/anima-research/example-repo/runs/1' }; + const TEST_JOB = { name: 'test', url: 'https://github.com/anima-research/example-repo/runs/2' }; const r = await rig(t, { getCheckRuns: async (_repo, sha, call) => call === 1 @@ -538,8 +538,8 @@ test('two failing suites finishing separately deliver one event naming every fai test('a settled sha that later reports more failed jobs supersedes, once', async (t) => { // The other order: the first lookup already looks settled because the // second suite has not been created yet. - const LINT = { name: 'lint', url: 'https://github.com/anima-research/rayforce/runs/1' }; - const TEST_JOB = { name: 'test', url: 'https://github.com/anima-research/rayforce/runs/2' }; + const LINT = { name: 'lint', url: 'https://github.com/anima-research/example-repo/runs/1' }; + const TEST_JOB = { name: 'test', url: 'https://github.com/anima-research/example-repo/runs/2' }; const r = await rig(t, { getCheckRuns: async (_repo, sha, call) => call === 1 @@ -628,7 +628,7 @@ function suiteFor(sha: string): string { return JSON.stringify(suite); } -const CLIPPY = { name: 'clippy', url: 'https://github.com/anima-research/rayforce/runs/2700001235' }; +const CLIPPY = { name: 'clippy', url: 'https://github.com/anima-research/example-repo/runs/2700001235' }; test('an overlapping poll and webhook for one head sha deliver it exactly once', async (t) => { // The round-2 regression: the poller decided to emit, awaited its listener,