Public: anyone can read this; the crew can change it.
An ask that moved on and an unknown bulk operation are said from one module #5682
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/5654-an-ask-that-moved-on-and-an-unknown-bulk → main 2 commits fork 1 thread · 1 unresolved
8 files
changed +65
−10
src/actions/asks_bulk.ts +2 −1
| @@ -12,6 +12,7 @@ import type { App } from "../app.ts"; | ||
| 12 | 12 | import { listOperations, type Operation } from "../core/fleet_ops.ts"; |
| 13 | 13 | import type { User } from "../core/users.ts"; |
| 14 | 14 | import { err, ok, type Result } from "../kernel/result.ts"; |
| 15 | import { NO_SUCH_OPERATION } from "./asks_words.ts"; | |
| 15 | 16 | import { askTargetsFor } from "../web/studio/asks.ts"; |
| 16 | 17 | import { |
| 17 | 18 | applyFleetIds, |
| @@ -191,6 +192,6 @@ export async function undoAsksBulk( | ||
| 191 | 192 | ): Promise<Result<Operation, FleetError>> { |
| 192 | 193 | const op = ownAsksOperation(app, actor, operationId); |
| 193 | 194 | if (op === undefined) |
| 194 | return err({ code: "not_found", message: "no such operation here" }); | |
| 195 | return err({ code: "not_found", message: NO_SUCH_OPERATION }); | |
| 195 | 196 | return undoFleetOp(app, actor, op.id); |
| 196 | 197 | } |
src/actions/asks_decide.ts +2 −1
| @@ -14,6 +14,7 @@ import type { User } from "../core/users.ts"; | ||
| 14 | 14 | import { guardedHand, letDeciderHandle, undoHandled } from "../crew/handover.ts"; |
| 15 | 15 | import { err, ok, type Result } from "../kernel/result.ts"; |
| 16 | 16 | import { askTargetsFor } from "../web/studio/asks.ts"; |
| 17 | import { NO_LONGER_ASKS } from "./asks_words.ts"; | |
| 17 | 18 | import type { FleetError } from "./fleet.ts"; |
| 18 | 19 | import { SIGN_IN_TO_ACT } from "./asks_words.ts"; |
| 19 | 20 | |
| @@ -69,7 +70,7 @@ export async function decideAsk( | ||
| 69 | 70 | const issue = issueIds[0] === undefined ? undefined : getIssueById(app.db, issueIds[0]); |
| 70 | 71 | const repo = issue === undefined ? undefined : getRepoById(app.db, issue.repoId); |
| 71 | 72 | if (issue === undefined || repo === undefined) |
| 72 | return fail("not_found", "it no longer asks you"); | |
| 73 | return fail("not_found", NO_LONGER_ASKS); | |
| 73 | 74 | const handed = await guardedHand(app, issue.id, now, () => |
| 74 | 75 | letDeciderHandle(app, actor, repo, issue, repo.decisionsBy), |
| 75 | 76 | ); |
src/actions/asks_words.ts +11 −1
| @@ -1,8 +1,18 @@ | ||
| 1 | 1 | /** |
| 2 | 2 | * What acting on asks says, said from one place (#5614): bulk decide, |
| 3 | * decide and proposals read it here, so the web, the API, MCP and the | |
| 3 | * decide and proposals read it here, and an ask or a bulk operation whose | |
| 4 | * subject is gone answers from here too, so the web, the API, MCP and the | |
| 4 | 5 | * terminal word it the same. |
| 5 | 6 | */ |
| 6 | 7 | |
| 7 | 8 | /** Acting on an ask while signed out. */ |
| 8 | 9 | export const SIGN_IN_TO_ACT = "sign in to act on your asks"; |
| 10 | ||
| 11 | /** Deciding an ask that has moved on: it no longer waits on this person. */ | |
| 12 | export const NO_LONGER_ASKS = "it no longer asks you"; | |
| 13 | ||
| 14 | /** A bulk operation, or a fleet verb, this place does not know. */ | |
| 15 | export const NO_SUCH_OPERATION = "no such operation here"; | |
| 16 | ||
| 17 | /** A milestone that is not here, or not readable by the one asking. */ | |
| 18 | export const NO_SUCH_MILESTONE = "no such milestone here"; |
src/actions/fleet.ts +3 −2
| @@ -57,6 +57,7 @@ import { | ||
| 57 | 57 | type FleetKind, |
| 58 | 58 | } from "../fleet/selection.ts"; |
| 59 | 59 | import { VERBS, type Target, type Verb } from "../fleet/verbs.ts"; |
| 60 | import { NO_SUCH_OPERATION } from "./asks_words.ts"; | |
| 60 | 61 | |
| 61 | 62 | export interface FleetError { |
| 62 | 63 | readonly code: |
| @@ -599,7 +600,7 @@ export async function undoFleetOp( | ||
| 599 | 600 | op.actorId !== person.value.id && |
| 600 | 601 | (verb.personal || !parseQuery(op.query).ok) |
| 601 | 602 | ) |
| 602 | return fail("not_found", "no such operation here"); | |
| 603 | return fail("not_found", NO_SUCH_OPERATION); | |
| 603 | 604 | // The day is the same on every door: the card, the API, MCP. |
| 604 | 605 | if (keepsADay(op) && Date.now() - Date.parse(op.createdAt) > UNDO_WINDOW_MS) |
| 605 | 606 | return fail("expired", "an undo is kept for a day, and this one's day is over"); |
| @@ -661,7 +662,7 @@ export function getFleetOp( | ||
| 661 | 662 | ): Result<Operation, FleetError> { |
| 662 | 663 | const op = getOperation(app.db, operationId); |
| 663 | 664 | if (!op || !actor || (op.actorId !== actor.id && !actor.isAdmin)) |
| 664 | return fail("not_found", "no such operation here"); | |
| 665 | return fail("not_found", NO_SUCH_OPERATION); | |
| 665 | 666 | return ok(op); |
| 666 | 667 | } |
| 667 | 668 |
src/actions/limb_replan.ts +3 −2
| @@ -22,6 +22,7 @@ import { canAdmin, canRead, type Repo } from "../core/repos.ts"; | ||
| 22 | 22 | import type { User } from "../core/users.ts"; |
| 23 | 23 | import { err, ok, type Result } from "../kernel/result.ts"; |
| 24 | 24 | import { getIssueById } from "../core/issues.ts"; |
| 25 | import { NO_SUCH_MILESTONE } from "./asks_words.ts"; | |
| 25 | 26 | import type { FleetError } from "./fleet.ts"; |
| 26 | 27 | import type { LimbIssue } from "./limbs.ts"; |
| 27 | 28 | import { IN_HAND } from "./plan_words.ts"; |
| @@ -64,10 +65,10 @@ function find( | ||
| 64 | 65 | number: number, |
| 65 | 66 | ): Result<ReplanPreview, FleetError> { |
| 66 | 67 | if (!actor) return fail("unauthorized", "sign in to hand a limb to the planner"); |
| 67 | if (!canRead(app.db, repo, actor)) return fail("not_found", "no such milestone here"); | |
| 68 | if (!canRead(app.db, repo, actor)) return fail("not_found", NO_SUCH_MILESTONE); | |
| 68 | 69 | const issues = openBeneath(app.db, repo, number); |
| 69 | 70 | const limb = issues.find((i) => i.number === number); |
| 70 | if (limb === undefined) return fail("not_found", "no such milestone here"); | |
| 71 | if (limb === undefined) return fail("not_found", NO_SUCH_MILESTONE); | |
| 71 | 72 | const pending = pendingFor(app.db, repo, limb.id); |
| 72 | 73 | const blocked = |
| 73 | 74 | mayAct(app, actor, repo)?.message ?? |
src/actions/limbs.ts +3 −2
| @@ -34,6 +34,7 @@ import { parseSeat, seating } from "../core/limb_seats.ts"; | ||
| 34 | 34 | import { readPlan } from "../core/milestones.ts"; |
| 35 | 35 | import { isStructural, pillarsOf } from "../core/pillars.ts"; |
| 36 | 36 | import { canRead, type Repo } from "../core/repos.ts"; |
| 37 | import { NO_SUCH_MILESTONE } from "./asks_words.ts"; | |
| 37 | 38 | import type { User } from "../core/users.ts"; |
| 38 | 39 | import { err, ok, type Result } from "../kernel/result.ts"; |
| 39 | 40 | import { |
| @@ -165,11 +166,11 @@ function shape( | ||
| 165 | 166 | if (!actor) return fail("unauthorized", "sign in to act on a limb"); |
| 166 | 167 | // One answer for no such repository and not yours to see. |
| 167 | 168 | if (!canRead(app.db, repo, actor)) |
| 168 | return fail("not_found", "no such milestone here"); | |
| 169 | return fail("not_found", NO_SUCH_MILESTONE); | |
| 169 | 170 | const plan = readPlan(app.db, repo); |
| 170 | 171 | const rows = plan.limb(number); |
| 171 | 172 | if (rows === undefined || rows.own[0] === undefined) |
| 172 | return fail("not_found", "no such milestone here"); | |
| 173 | return fail("not_found", NO_SUCH_MILESTONE); | |
| 173 | 174 | const limb = brief(rows.own[0]); |
| 174 | 175 | const where = `issue repo:${repo.ownerName}/${repo.name}`; |
| 175 | 176 | if (request.action === "raise" || request.action === "lower") |
src/web/studio/asks.ts +2 −1
| @@ -66,6 +66,7 @@ import { btn, row } from "../ui.ts"; | ||
| 66 | 66 | import { lookUrl } from "../../core/looks.ts"; |
| 67 | 67 | import { thumbLook } from "../pages/looks.ts"; |
| 68 | 68 | import { changeHref } from "./nouns.ts"; |
| 69 | import { NO_LONGER_ASKS } from "../../actions/asks_words.ts"; | |
| 69 | 70 | import { testsClause } from "./words.ts"; |
| 70 | 71 | |
| 71 | 72 | /** Repos narrowed to those with an app record, with the records — the one |
| @@ -1506,7 +1507,7 @@ export function askTargetsFor( | ||
| 1506 | 1507 | const ask = asks.get(item); |
| 1507 | 1508 | const mine = assigned.get(item); |
| 1508 | 1509 | if (mine !== undefined) issueIds.push(mine.id); |
| 1509 | else if (ask === undefined) refused.push({ item, reason: "it no longer asks you" }); | |
| 1510 | else if (ask === undefined) refused.push({ item, reason: NO_LONGER_ASKS }); | |
| 1510 | 1511 | else if (ask.kind === "paused") |
| 1511 | 1512 | refused.push({ item, reason: "a ceiling is raised in the app's settings" }); |
| 1512 | 1513 | else if (ask.kind === "proposal") |
test/asks-op-words.test.ts added +39
| @@ -0,0 +1,39 @@ | ||
| 1 | /** | |
| 2 | * An ask that moved on, an unknown bulk operation and a milestone not here | |
| 3 | * are said from src/actions/asks_words.ts (#5654): the files that answer | |
| 4 | * with them import the sentence and never type a copy of it. | |
| 5 | */ | |
| 6 | ||
| 7 | import { expect, test } from "bun:test"; | |
| 8 | import { readFileSync } from "node:fs"; | |
| 9 | import { join } from "node:path"; | |
| 10 | import { | |
| 11 | NO_LONGER_ASKS, | |
| 12 | NO_SUCH_MILESTONE, | |
| 13 | NO_SUCH_OPERATION, | |
| 14 | } from "../src/actions/asks_words.ts"; | |
| 15 | ||
| 16 | const root = join(import.meta.dir, ".."); | |
| 17 | const read = (path: string) => readFileSync(join(root, path), "utf8"); | |
| 18 | ||
| 19 | const SAID_BY: [string, string, string[]][] = [ | |
| 20 | ["NO_LONGER_ASKS", NO_LONGER_ASKS, ["src/actions/asks_decide.ts", "src/web/studio/asks.ts"]], | |
| 21 | ["NO_SUCH_OPERATION", NO_SUCH_OPERATION, ["src/actions/asks_bulk.ts", "src/actions/fleet.ts"]], | |
| 22 | ["NO_SUCH_MILESTONE", NO_SUCH_MILESTONE, ["src/actions/limb_replan.ts", "src/actions/limbs.ts"]], | |
| 23 | ]; | |
| 24 | ||
| 25 | test("the sentences read as they always have", () => { | |
| 26 | expect(NO_LONGER_ASKS).toBe("it no longer asks you"); | |
| 27 | expect(NO_SUCH_OPERATION).toBe("no such operation here"); | |
| 28 | expect(NO_SUCH_MILESTONE).toBe("no such milestone here"); | |
| 29 | }); | |
| 30 | ||
| 31 | for (const [name, sentence, files] of SAID_BY) { | |
| 32 | for (const file of files) { | |
| 33 | test(`${file} imports ${name} and types no copy of it`, () => { | |
| 34 | const text = read(file); | |
| 35 | expect(text).not.toContain(sentence); | |
| 36 | expect(text).toMatch(new RegExp(`import \\{[^}]*\\b${name}\\b[^}]*\\} from "[./]+(actions/)?asks_words\\.ts"`)); | |
| 37 | }); | |
| 38 | } | |
| 39 | } |
new above
offline
This greps source files for the sentence. Add a case that calls getFleetOp with an unknown id and checks the error message equals NO_SUCH_OPERATION, so the test follows behaviour rather than file text.