diff --git a/.github/todo/milestones-v2.md b/.github/todo/milestones-v2.md deleted file mode 100644 index 5c17042..0000000 --- a/.github/todo/milestones-v2.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -title: Implement Milestones domain in the GitHub API simulator ---- - -## Summary - -Implement the Milestones domain in the `@counterfact/github` simulator. Milestones -are used to group issues into sprints or releases and are a natural complement to -the already-implemented Issues domain. - -All state and logic must live in **`routes/repos/_.context.ts`** — the -domain-specific context file for repository features — rather than in the root -`routes/_.context.ts`. Follow the pattern established by `routes/gists/_.context.ts` -and `routes/users/_.context.ts`. - -## Scope - -### Route files to implement (currently returning `$.response[200].random()`) - -| File | Handlers | Description | -|------|----------|-------------| -| `routes/repos/{owner}/{repo}/milestones.ts` | GET, POST | List milestones; Create a milestone | -| `routes/repos/{owner}/{repo}/milestones/{milestone_number}.ts` | GET, PATCH, DELETE | Get / update / delete a milestone | - -`/milestones/{milestone_number}/labels` may remain a stub. - -## Implementation plan - -### 1. Extend `RepoState` in `routes/repos/_.context.ts` - -Add: - -- `milestones: Map` (keyed by milestone number) -- `nextMilestoneNumber: number` (start at 1) - -> If `routes/repos/_.context.ts` does not yet exist, create it following the -> same pattern as `routes/gists/_.context.ts` and wire it into the root context -> and `test-support/create-context.ts`. See the "Refactor repos domain" issue -> for a detailed guide. - -### 2. Add context methods to `Context` in `routes/repos/_.context.ts` - -```ts -saveMilestone(owner, repo, input: Partial & { title: string }): milestone -getMilestone(owner, repo, number: number): milestone | undefined -updateMilestone(owner, repo, number: number, patch: Partial): milestone | undefined -deleteMilestone(owner, repo, number: number): boolean -listMilestones(owner, repo, query?: { - state?: 'open' | 'closed' | 'all'; - direction?: string; - sort?: string; - per_page?: unknown; - page?: unknown; -}): milestone[] -``` - -`saveMilestone` auto-generates `id`, `node_id`, `number`, `url`, `html_url`, `labels_url`, -`created_at`, `updated_at`, `creator` (default user), `open_issues: 0`, `closed_issues: 0`, -`state: 'open'`. If an input `number` is provided (for seeding), use it directly. - -Extend `saveIssue` and `savePullRequest` so that when `milestone` is set, the milestone's -`open_issues` / `closed_issues` counts update atomically. - -Also add forwarding methods in `routes/_.context.ts` so that route files can call -`$.context.saveMilestone(...)` without changes. - -### 3. Implement route handlers - -Validate that the repo exists. For the list endpoint, filter by `state` query parameter. - -### 4. Seed scenario data - -Add a `milestones` scenario function in `scenarios/index.ts` that seeds two milestones on -`counterfact/platform-api` (e.g. `v1.0` open with a due date and `v0.9` closed). Associate -the existing open issue with the `v1.0` milestone. Add `milestones($)` inside `seedGitHub`. - -### 5. Write tests - -**Unit tests** (`test/repos.context.test.ts` or a new `test/milestones.context.test.ts` -that instantiates `routes/repos/_.context.ts` `Context` directly): - -- `saveMilestone` creates a milestone with auto-generated fields. -- `listMilestones` filters by `state`. -- `updateMilestone` merges patch fields. -- `deleteMilestone` returns `false` when not found. -- `open_issues` count updates when an issue is assigned to a milestone. - -**HTTP-level tests** (`test/routes.test.ts`): - -- `GET /repos/:owner/:repo/milestones` returns the seeded list (filterable by state). -- `POST /repos/:owner/:repo/milestones` creates and returns 201. -- `GET/PATCH/DELETE /repos/:owner/:repo/milestones/:number` work correctly. -- All routes return 404 when the repository does not exist. - -## Relevant types - -- `types/components/schemas/milestone.ts` — `milestone` -- `types/paths/repos/{owner}/{repo}/milestones.types.ts` -- `types/paths/repos/{owner}/{repo}/milestones/{milestone_number}.types.ts` diff --git a/github/routes/_.context.ts b/github/routes/_.context.ts index e90f282..c786fc7 100644 --- a/github/routes/_.context.ts +++ b/github/routes/_.context.ts @@ -232,6 +232,26 @@ export class Context { return this.reposContext().listLabels(...args); } + saveMilestone(...args: Parameters) { + return this.reposContext().saveMilestone(...args); + } + + getMilestone(...args: Parameters) { + return this.reposContext().getMilestone(...args); + } + + updateMilestone(...args: Parameters) { + return this.reposContext().updateMilestone(...args); + } + + deleteMilestone(...args: Parameters) { + return this.reposContext().deleteMilestone(...args); + } + + listMilestones(...args: Parameters) { + return this.reposContext().listMilestones(...args); + } + saveIssue(...args: Parameters) { return this.reposContext().saveIssue(...args); } diff --git a/github/routes/repos/_.context.ts b/github/routes/repos/_.context.ts index 2f0df6f..5dfe12f 100644 --- a/github/routes/repos/_.context.ts +++ b/github/routes/repos/_.context.ts @@ -12,6 +12,7 @@ import type { issue } from "../../types/components/schemas/issue.js"; import type { issue_comment } from "../../types/components/schemas/issue-comment.js"; import type { job } from "../../types/components/schemas/job.js"; import type { label } from "../../types/components/schemas/label.js"; +import type { milestone } from "../../types/components/schemas/milestone.js"; import type { minimal_repository } from "../../types/components/schemas/minimal-repository.js"; import type { organization_full } from "../../types/components/schemas/organization-full.js"; import type { organization_simple } from "../../types/components/schemas/organization-simple.js"; @@ -42,6 +43,7 @@ type RepoState = { labels: Map; issues: Map; issueComments: Map>; + milestones: Map; pulls: Map; reviews: Map>; workflows: Map; @@ -50,6 +52,7 @@ type RepoState = { releases: Map; nextCommitCommentId: number; nextIssueNumber: number; + nextMilestoneNumber: number; nextPullNumber: number; nextReleaseId: number; }; @@ -244,6 +247,7 @@ export class Context { private nextRepoId = 1000; private nextIssueId = 2000; + private nextMilestoneId = 2500; private nextIssueCommentId = 3000; private nextPullId = 4000; private nextReviewId = 5000; @@ -431,6 +435,44 @@ export class Context { state.repository.open_issues = openIssues; } + private milestoneNumberFrom( + value: issue["milestone"] | pull_request["milestone"] | undefined, + ): number | undefined { + return value && typeof value === "object" ? value.number : undefined; + } + + private syncMilestoneCounts(state: RepoState) { + for (const milestoneItem of state.milestones.values()) { + milestoneItem.open_issues = 0; + milestoneItem.closed_issues = 0; + } + + const collect = ( + stateValue: string, + milestoneNumber: number | undefined, + ) => { + if (milestoneNumber == null) { + return; + } + const milestoneItem = state.milestones.get(milestoneNumber); + if (!milestoneItem) { + return; + } + if (stateValue === "closed") { + milestoneItem.closed_issues += 1; + return; + } + milestoneItem.open_issues += 1; + }; + + for (const issueItem of state.issues.values()) { + collect(issueItem.state, this.milestoneNumberFrom(issueItem.milestone)); + } + for (const pullItem of state.pulls.values()) { + collect(pullItem.state, this.milestoneNumberFrom(pullItem.milestone)); + } + } + private syncIssueLabels( state: RepoState, previousName: string, @@ -447,6 +489,23 @@ export class Context { } } + private syncMilestoneReferences( + state: RepoState, + number: number, + nextMilestone?: milestone, + ) { + for (const issueItem of state.issues.values()) { + if (this.milestoneNumberFrom(issueItem.milestone) === number) { + issueItem.milestone = nextMilestone ?? null; + } + } + for (const pullItem of state.pulls.values()) { + if (this.milestoneNumberFrom(pullItem.milestone) === number) { + pullItem.milestone = nextMilestone ?? null; + } + } + } + private resolveIssueLabels( owner: string, repo: string, @@ -704,6 +763,7 @@ export class Context { labels: new Map(), issues: new Map(), issueComments: new Map(), + milestones: new Map(), pulls: new Map(), reviews: new Map(), workflows: new Map(), @@ -712,6 +772,7 @@ export class Context { releases: new Map(), nextCommitCommentId: 1, nextIssueNumber: 1, + nextMilestoneNumber: 1, nextPullNumber: 1, nextReleaseId: 1, }; @@ -721,6 +782,7 @@ export class Context { state.commitStatuses ??= new Map(); state.commitComments ??= new Map(); state.labels ??= new Map(); + state.milestones ??= new Map(); state.nextCommitCommentId ??= 1; const branches = repository.branches ?? [defaultBranch]; @@ -1204,6 +1266,144 @@ export class Context { ); } + saveMilestone( + owner: string, + repo: string, + input: Partial & { title: string }, + ): milestone { + const state = this.getRepoState(owner, repo); + if (!state) { + throw new Error(`Repository ${owner}/${repo} does not exist`); + } + + const now = isoNow(); + const number = input.number ?? state.nextMilestoneNumber++; + const existing = state.milestones.get(number); + const id = input.id ?? existing?.id ?? this.nextMilestoneId++; + const creator = + input.creator ?? + existing?.creator ?? + toSimpleUser(this.ensureDefaultUser()); + const nextState = input.state ?? existing?.state ?? "open"; + const closedAtProvided = "closed_at" in input; + const closedAt = + nextState === "closed" + ? closedAtProvided + ? (input.closed_at ?? "") + : existing?.state === "closed" && existing.closed_at + ? existing.closed_at + : now + : closedAtProvided + ? (input.closed_at ?? "") + : ""; + const milestoneItem: milestone = { + ...(existing ?? {}), + ...input, + id, + node_id: input.node_id ?? existing?.node_id ?? `MS_${id}`, + number, + url: `${API_URL}/repos/${owner}/${repo}/milestones/${number}`, + html_url: `${APP_URL}/${owner}/${repo}/milestone/${number}`, + labels_url: `${API_URL}/repos/${owner}/${repo}/milestones/${number}/labels`, + state: nextState, + title: input.title, + description: input.description ?? existing?.description ?? "", + creator, + open_issues: input.open_issues ?? existing?.open_issues ?? 0, + closed_issues: input.closed_issues ?? existing?.closed_issues ?? 0, + created_at: existing?.created_at ?? input.created_at ?? now, + updated_at: now, + closed_at: closedAt, + due_on: input.due_on ?? existing?.due_on ?? "", + }; + + state.milestones.set(number, milestoneItem); + this.syncMilestoneReferences(state, number, milestoneItem); + state.nextMilestoneNumber = Math.max(state.nextMilestoneNumber, number + 1); + this.nextMilestoneId = Math.max(this.nextMilestoneId, id + 1); + this.syncMilestoneCounts(state); + return milestoneItem; + } + + getMilestone( + owner: string, + repo: string, + number: number, + ): milestone | undefined { + return this.getRepoState(owner, repo)?.milestones.get(number); + } + + updateMilestone( + owner: string, + repo: string, + number: number, + patch: Partial, + ): milestone | undefined { + const existing = this.getMilestone(owner, repo, number); + if (!existing) { + return undefined; + } + return this.saveMilestone(owner, repo, { + ...patch, + number, + title: patch.title ?? existing.title, + }); + } + + deleteMilestone(owner: string, repo: string, number: number): boolean { + const state = this.getRepoState(owner, repo); + if (!state || !state.milestones.has(number)) { + return false; + } + + state.milestones.delete(number); + this.syncMilestoneReferences(state, number); + this.syncMilestoneCounts(state); + return true; + } + + listMilestones( + owner: string, + repo: string, + query?: { + state?: "open" | "closed" | "all"; + direction?: string; + sort?: string; + per_page?: unknown; + page?: unknown; + }, + ): milestone[] { + const state = this.getRepoState(owner, repo); + if (!state) { + return []; + } + + let milestones = [...state.milestones.values()]; + if (query?.state && query.state !== "all") { + milestones = milestones.filter((item) => item.state === query.state); + } + + const direction = query?.direction === "asc" ? 1 : -1; + milestones.sort((left, right) => { + if (query?.sort === "completeness") { + const leftTotal = left.open_issues + left.closed_issues; + const rightTotal = right.open_issues + right.closed_issues; + const leftCompleteness = + leftTotal === 0 ? 0 : left.closed_issues / leftTotal; + const rightCompleteness = + rightTotal === 0 ? 0 : right.closed_issues / rightTotal; + return (leftCompleteness - rightCompleteness) * direction; + } + return ( + (new Date(left.due_on || left.created_at).getTime() - + new Date(right.due_on || right.created_at).getTime()) * + direction + ); + }); + + return paginate(milestones, query); + } + saveIssue( owner: string, repo: string, @@ -1300,6 +1500,7 @@ export class Context { state.nextIssueNumber = Math.max(state.nextIssueNumber, number + 1); this.nextIssueId = Math.max(this.nextIssueId, id + 1); this.syncRepoCounts(owner, repo); + this.syncMilestoneCounts(state); return nextIssue; } @@ -1747,6 +1948,7 @@ export class Context { state.pulls.set(number, pullRequest); state.nextPullNumber = Math.max(state.nextPullNumber, number + 1); this.nextPullId = Math.max(this.nextPullId, id + 1); + this.syncMilestoneCounts(state); return pullRequest; } diff --git a/github/routes/repos/{owner}/{repo}/milestones.ts b/github/routes/repos/{owner}/{repo}/milestones.ts index fc90190..1d79e93 100644 --- a/github/routes/repos/{owner}/{repo}/milestones.ts +++ b/github/routes/repos/{owner}/{repo}/milestones.ts @@ -2,9 +2,19 @@ import type { issuesListMilestones } from "../../../../types/paths/repos/{owner} import type { issuesCreateMilestone } from "../../../../types/paths/repos/{owner}/{repo}/milestones.types.js"; export const GET: issuesListMilestones = async ($) => { - return $.response[200].random(); + if (!$.context.hasRepository($.path.owner, $.path.repo)) { + return $.response[404].empty(); + } + return $.response[200].json( + $.context.listMilestones($.path.owner, $.path.repo, $.query), + ); }; export const POST: issuesCreateMilestone = async ($) => { - return $.response[201].random(); + if (!$.context.hasRepository($.path.owner, $.path.repo)) { + return $.response[404].empty(); + } + return $.response[201].json( + $.context.saveMilestone($.path.owner, $.path.repo, $.body), + ); }; diff --git a/github/routes/repos/{owner}/{repo}/milestones/{milestone_number}.ts b/github/routes/repos/{owner}/{repo}/milestones/{milestone_number}.ts index 46ec549..4cd2d72 100644 --- a/github/routes/repos/{owner}/{repo}/milestones/{milestone_number}.ts +++ b/github/routes/repos/{owner}/{repo}/milestones/{milestone_number}.ts @@ -3,13 +3,39 @@ import type { issuesUpdateMilestone } from "../../../../../types/paths/repos/{ow import type { issuesDeleteMilestone } from "../../../../../types/paths/repos/{owner}/{repo}/milestones/{milestone_number}.types.js"; export const GET: issuesGetMilestone = async ($) => { - return $.response[200].random(); + const milestone = $.context.getMilestone( + $.path.owner, + $.path.repo, + $.path.milestone_number, + ); + if (!milestone) { + return $.response[404].empty(); + } + return $.response[200].json(milestone); }; export const PATCH: issuesUpdateMilestone = async ($) => { - return $.response[200].random(); + const milestone = $.context.updateMilestone( + $.path.owner, + $.path.repo, + $.path.milestone_number, + $.body, + ); + if (!milestone) { + return $.response[404].empty(); + } + return $.response[200].json(milestone); }; export const DELETE: issuesDeleteMilestone = async ($) => { + if ( + !$.context.deleteMilestone( + $.path.owner, + $.path.repo, + $.path.milestone_number, + ) + ) { + return $.response[404].empty(); + } return $.response[204].empty(); }; diff --git a/github/scenarios/index.ts b/github/scenarios/index.ts index 4ca6944..4995266 100644 --- a/github/scenarios/index.ts +++ b/github/scenarios/index.ts @@ -165,6 +165,36 @@ export const issues: Scenario = ($) => { }); }; +export const milestones: Scenario = ($) => { + const v1 = $.context.saveMilestone("counterfact", "platform-api", { + title: "v1.0", + description: "Track v1.0 launch", + due_on: "2024-05-01T00:00:00Z", + state: "open", + }); + $.context.saveMilestone("counterfact", "platform-api", { + title: "v0.9", + description: "Pre-release cleanup", + due_on: "2024-03-01T00:00:00Z", + state: "closed", + closed_at: "2024-03-05T00:00:00Z", + }); + + const existingOpenIssue = $.context.getIssue( + "counterfact", + "platform-api", + 1, + ); + if (existingOpenIssue) { + $.context.saveIssue("counterfact", "platform-api", { + ...existingOpenIssue, + number: existingOpenIssue.number, + title: existingOpenIssue.title, + milestone: v1, + }); + } +}; + export const labels: Scenario = ($) => { $.context.saveLabel("counterfact", "platform-api", { name: "bug", @@ -324,6 +354,7 @@ export const seedGitHub: Scenario = ($) => { void repositories($); void labels($); void issues($); + void milestones($); void pullRequests($); void actions($); void commitStatuses($); diff --git a/github/test/repos.context.test.ts b/github/test/repos.context.test.ts index 7d86205..9ec59ff 100644 --- a/github/test/repos.context.test.ts +++ b/github/test/repos.context.test.ts @@ -318,6 +318,134 @@ test("Context label methods add, remove, and replace issue labels", () => { ); }); +test("Context.saveMilestone creates milestone fields and supports querying/updating/deleting", () => { + const context = createContext(); + context.saveUser({ id: 1, login: "octocat", name: "Octocat" }); + context.saveRepository({ id: 101, owner: "octocat", name: "hello-world" }); + + const milestone = context.saveMilestone("octocat", "hello-world", { + title: "v1.0", + description: "First release", + }); + assert.ok(milestone.id > 0); + assert.match(milestone.node_id, /^MS_/); + assert.equal(milestone.number, 1); + assert.equal(milestone.state, "open"); + assert.equal(milestone.open_issues, 0); + assert.equal(milestone.closed_issues, 0); + assert.match(milestone.url, /\/repos\/octocat\/hello-world\/milestones\/1$/); + assert.match( + milestone.labels_url, + /\/repos\/octocat\/hello-world\/milestones\/1\/labels$/, + ); + + context.saveMilestone("octocat", "hello-world", { + title: "v0.9", + state: "closed", + }); + assert.equal( + context.listMilestones("octocat", "hello-world", { state: "all" }).length, + 2, + ); + assert.equal( + context.listMilestones("octocat", "hello-world", { state: "open" }).length, + 1, + ); + assert.equal( + context.listMilestones("octocat", "hello-world", { state: "closed" }) + .length, + 1, + ); + + const updated = context.updateMilestone("octocat", "hello-world", 1, { + description: "Updated description", + state: "closed", + }); + assert.equal(updated?.title, "v1.0"); + assert.equal(updated?.description, "Updated description"); + assert.equal(updated?.state, "closed"); + assert.notEqual(updated?.closed_at, ""); + + const reopened = context.updateMilestone("octocat", "hello-world", 1, { + state: "open", + }); + assert.equal(reopened?.closed_at, ""); + + context.saveIssue("octocat", "hello-world", { + number: 1, + title: "Use milestone", + milestone: reopened, + }); + context.savePullRequest("octocat", "hello-world", { + number: 1, + title: "Use milestone in PR", + head: "feature/milestone", + base: "main", + milestone: reopened, + }); + + const retitled = context.updateMilestone("octocat", "hello-world", 1, { + title: "v1.1", + }); + assert.equal(retitled?.title, "v1.1"); + assert.equal( + context.getIssue("octocat", "hello-world", 1)?.milestone?.title, + "v1.1", + ); + assert.equal( + context.getPullRequest("octocat", "hello-world", 1)?.milestone?.title, + "v1.1", + ); + + assert.equal(context.deleteMilestone("octocat", "hello-world", 999), false); + assert.equal(context.deleteMilestone("octocat", "hello-world", 1), true); + assert.equal(context.getMilestone("octocat", "hello-world", 1), undefined); +}); + +test("Context milestone issue counters update when issue state or milestone assignment changes", () => { + const context = createContext(); + context.saveUser({ id: 1, login: "octocat", name: "Octocat" }); + context.saveRepository({ id: 101, owner: "octocat", name: "hello-world" }); + + const milestone = context.saveMilestone("octocat", "hello-world", { + title: "v1.0", + }); + + context.saveIssue("octocat", "hello-world", { + number: 1, + title: "Track work", + state: "open", + milestone, + }); + assert.equal( + context.getMilestone("octocat", "hello-world", milestone.number) + ?.open_issues, + 1, + ); + assert.equal( + context.getMilestone("octocat", "hello-world", milestone.number) + ?.closed_issues, + 0, + ); + + context.saveIssue("octocat", "hello-world", { + number: 1, + title: "Track work", + state: "closed", + milestone, + }); + assert.equal( + context.getMilestone("octocat", "hello-world", milestone.number) + ?.open_issues, + 0, + ); + assert.equal( + context.getMilestone("octocat", "hello-world", milestone.number) + ?.closed_issues, + 1, + ); +}); + test("Context commit methods resolve refs, paginate, and manage statuses/comments", () => { const context = createContext(); diff --git a/github/test/routes.test.ts b/github/test/routes.test.ts index 89623cb..4296482 100644 --- a/github/test/routes.test.ts +++ b/github/test/routes.test.ts @@ -33,6 +33,15 @@ import { GET as getIssues, POST as postIssue, } from "../routes/repos/{owner}/{repo}/issues.ts"; +import { + GET as getMilestone, + PATCH as patchMilestone, + DELETE as deleteMilestone, +} from "../routes/repos/{owner}/{repo}/milestones/{milestone_number}.ts"; +import { + GET as getMilestones, + POST as postMilestone, +} from "../routes/repos/{owner}/{repo}/milestones.ts"; import { GET as getLabel, PATCH as patchLabel, @@ -393,6 +402,76 @@ test("issue routes manage issue lifecycle and comments", async () => { assert.equal((comments.body as Array).length, 2); }); +test("milestone routes manage milestone lifecycle", async () => { + const context = createSeededContext(); + + const listed = (await getMilestones( + create$({ + context, + path: { owner: "counterfact", repo: "platform-api" }, + query: { state: "all" }, + }) as never, + )) as RouteResult; + assert.equal((listed.body as Array).length, 2); + + const openOnly = (await getMilestones( + create$({ + context, + path: { owner: "counterfact", repo: "platform-api" }, + query: { state: "open" }, + }) as never, + )) as RouteResult; + assert.equal((openOnly.body as Array).length, 1); + + const created = (await postMilestone( + create$({ + context, + path: { owner: "counterfact", repo: "platform-api" }, + body: { title: "v1.1", description: "Next milestone" }, + }) as never, + )) as RouteResult; + assert.equal(created.status, 201); + assert.equal((created.body as { number: number }).number, 3); + + const fetched = (await getMilestone( + create$({ + context, + path: { + owner: "counterfact", + repo: "platform-api", + milestone_number: 1, + }, + }) as never, + )) as RouteResult; + assert.equal((fetched.body as { title: string }).title, "v1.0"); + + const patched = (await patchMilestone( + create$({ + context, + path: { + owner: "counterfact", + repo: "platform-api", + milestone_number: 1, + }, + body: { title: "v1.0.1", state: "closed" }, + }) as never, + )) as RouteResult; + assert.equal((patched.body as { title: string }).title, "v1.0.1"); + assert.equal((patched.body as { state: string }).state, "closed"); + + const deleted = (await deleteMilestone( + create$({ + context, + path: { + owner: "counterfact", + repo: "platform-api", + milestone_number: 3, + }, + }) as never, + )) as RouteResult; + assert.equal(deleted.status, 204); +}); + test("label routes manage repository and issue labels", async () => { const context = createSeededContext(); @@ -640,6 +719,80 @@ test("label routes return 404 when the repository or issue does not exist", asyn ); }); +test("milestone routes return 404 when repository or milestone does not exist", async () => { + const context = createSeededContext(); + + assert.equal( + ( + (await getMilestones( + create$({ + context, + path: { owner: "nobody", repo: "missing" }, + }) as never, + )) as RouteResult + ).status, + 404, + ); + assert.equal( + ( + (await postMilestone( + create$({ + context, + path: { owner: "nobody", repo: "missing" }, + body: { title: "v1.0" }, + }) as never, + )) as RouteResult + ).status, + 404, + ); + assert.equal( + ( + (await getMilestone( + create$({ + context, + path: { + owner: "counterfact", + repo: "platform-api", + milestone_number: 999, + }, + }) as never, + )) as RouteResult + ).status, + 404, + ); + assert.equal( + ( + (await patchMilestone( + create$({ + context, + path: { + owner: "counterfact", + repo: "platform-api", + milestone_number: 999, + }, + body: { title: "x" }, + }) as never, + )) as RouteResult + ).status, + 404, + ); + assert.equal( + ( + (await deleteMilestone( + create$({ + context, + path: { + owner: "counterfact", + repo: "platform-api", + milestone_number: 999, + }, + }) as never, + )) as RouteResult + ).status, + 404, + ); +}); + test("pull request routes manage reviews and updates", async () => { const context = createSeededContext(); diff --git a/github/test/scenarios.test.ts b/github/test/scenarios.test.ts index 95d4857..d7f62f6 100644 --- a/github/test/scenarios.test.ts +++ b/github/test/scenarios.test.ts @@ -7,6 +7,7 @@ import { identities, issues, labels, + milestones, pullRequests, releases, repositories, @@ -97,6 +98,7 @@ test("issues, pull requests, and actions scenarios seed related data", () => { repositories($); labels($); issues($); + milestones($); pullRequests($); actions($); @@ -135,6 +137,11 @@ test("issues, pull requests, and actions scenarios seed related data", () => { .map((item) => item.name), ["bug", "enhancement", "documentation", "question"], ); + assert.equal( + $.context.listMilestones("counterfact", "platform-api", { state: "all" }) + .length, + 2, + ); }); test("seedGitHub seeds all new GitHub domains together", () => { @@ -162,6 +169,35 @@ test("seedGitHub seeds all new GitHub domains together", () => { assert.equal($.context.listComments("aa5a315d61ae9438b18d").length, 1); assert.equal($.context.listReleases("counterfact", "platform-api").length, 3); assert.equal($.context.listLabels("counterfact", "platform-api").length, 4); + assert.equal( + $.context.listMilestones("counterfact", "platform-api", { state: "all" }) + .length, + 2, + ); +}); + +test("milestones scenario seeds milestones and links the open issue", () => { + const $ = createScenario$(); + + identities($); + repositories($); + labels($); + issues($); + milestones($); + + const all = $.context.listMilestones("counterfact", "platform-api", { + state: "all", + }); + assert.equal(all.length, 2); + + const open = $.context.listMilestones("counterfact", "platform-api", { + state: "open", + }); + assert.equal(open.length, 1); + assert.equal(open[0].title, "v1.0"); + + const seededIssue = $.context.getIssue("counterfact", "platform-api", 1); + assert.equal(seededIssue?.milestone?.title, "v1.0"); }); test("releases scenario seeds stable, pre-release, and draft releases", () => {