Public: anyone can read this; the crew can change it.
On a fork, the page manifest offers its writer how it follows its parent: read it, take the parent's updates, propose its changes up #7864
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/7856-on-a-fork-the-page-manifest-offers-its-w → main 2 commits fork 1 thread · 1 unresolved
2 files
changed +42
−0
src/actions/here.ts +16
| @@ -383,5 +383,21 @@ function repoActions( | ||
| 383 | 383 | out.push({ do: "merge a pull", http: `POST ${r}/pulls/{number}/merge` }); |
| 384 | 384 | if (canAdmin(db, repo, viewer)) |
| 385 | 385 | out.push({ do: "change settings", http: `PATCH ${r}` }); |
| 386 | // How a native fork follows its parent: a writer's moves, the same rule | |
| 387 | // `takeOrTell` and `proposeToParentForge` ask (`requireWriter`). | |
| 388 | if ( | |
| 389 | repo.forkParentId !== null && | |
| 390 | repo.origin === null && | |
| 391 | canWrite(db, repo, viewer) | |
| 392 | ) | |
| 393 | out.push( | |
| 394 | { do: "read how it follows its parent", http: `GET ${r}/following`, mcp: "following" }, | |
| 395 | { do: "take the parent's updates", http: `POST ${r}/following/take`, mcp: "take_updates" }, | |
| 396 | { | |
| 397 | do: "propose its changes to the parent", | |
| 398 | http: `POST ${r}/following/propose-parent`, | |
| 399 | mcp: "propose_to_parent_forge", | |
| 400 | }, | |
| 401 | ); | |
| 386 | 402 | return out; |
| 387 | 403 | } |
test/here-manifest.test.ts +26
| @@ -193,6 +193,32 @@ describe("the manifest: what you can do here, as who you are", () => { | ||
| 193 | 193 | expect(list).not.toContain("change settings"); |
| 194 | 194 | }); |
| 195 | 195 | |
| 196 | test("a writer of a fork is offered how it follows its parent; a reader and a plain repository's writer are not", async () => { | |
| 197 | const fork = await forkRepo(app, usersByName["wes"]!, getRepo(app.db, "acme", "game")!); | |
| 198 | if (!fork.ok) throw new Error(String(fork.error)); | |
| 199 | const path = "/wes/game"; | |
| 200 | const moves = [ | |
| 201 | "read how it follows its parent", | |
| 202 | "take the parent's updates", | |
| 203 | "propose its changes to the parent", | |
| 204 | ]; | |
| 205 | const { body } = await manifest(path, "wes"); | |
| 206 | for (const d of moves) expect(does(body)).toContain(d); | |
| 207 | expect(body.actions.find((a: any) => a.do === moves[1]).http).toBe( | |
| 208 | "POST /api/v1/repos/wes/game/following/take", | |
| 209 | ); | |
| 210 | expect(moves.map((d) => body.actions.find((a: any) => a.do === d).mcp)).toEqual([ | |
| 211 | "following", | |
| 212 | "take_updates", | |
| 213 | "propose_to_parent_forge", | |
| 214 | ]); | |
| 215 | for (const who of ["carol", undefined]) | |
| 216 | for (const d of moves) | |
| 217 | expect(does((await manifest(path, who)).body)).not.toContain(d); | |
| 218 | for (const d of moves) | |
| 219 | expect(does((await manifest("/acme/game", "wes")).body)).not.toContain(d); | |
| 220 | }); | |
| 221 | ||
| 196 | 222 | test("the owner also changes the settings", async () => { |
| 197 | 223 | const { body } = await manifest("/acme/game", "alice"); |
| 198 | 224 | expect(body.you.as).toBe("owner"); |
new above
offline
Add mcp: "following" here, "take_updates" on the take entry and "propose_to_parent_forge" on the propose entry, and spread
...unprovenon the two writes if their actions require a verified email. Assert the mcp names in the test.