The planner's refusals live in one module, read by dependencies, plan and limb replan #5632
Merged
masoncrew
opened this
Check · Review · Land · Live done
— Landed and live in production.
Merged
mason/forge:mason/5595-the-planner-s-refusals-live-in-one-modul → main
·
1 commit
·fork
-
Merged.
mason/5595-the-planner-s-refusals-live-in-one-modul was merged into
main as 7d09241.
Review
Sign in to review this change.
7 files
changed +56
−5
src/actions/dependencies.ts
+2
−1
|
|
@@ -18,11 +18,12 @@ import { canRead, canWrite, type Repo } from "../core/repos.ts"; |
| 18 | 18 |
import type { User } from "../core/users.ts"; |
| 19 | 19 |
import { err, ok, type Result } from "../kernel/result.ts"; |
| 20 | 20 |
import type { AccessError } from "./access.ts"; |
| 21 |
import { SELF_WAIT } from "./plan_words.ts"; |
| 21 | 22 |
import { followLink } from "./pillars.ts"; |
| 22 | 23 |
|
| 23 | 24 |
|
| 24 | 25 |
export const dependencyWords: Record<string, string> = { |
| 25 | |
self: "an issue cannot wait for itself", |
| 26 |
self: SELF_WAIT, |
| 26 | 27 |
cycle: "that would make a loop", |
| 27 | 28 |
cross_repo: "dependencies stay inside one repository", |
| 28 | 29 |
not_an_issue: "only issues can depend on issues", |
src/actions/limb_replan.ts
+2
−1
|
|
@@ -24,6 +24,7 @@ import { err, ok, type Result } from "../kernel/result.ts"; |
| 24 | 24 |
import { getIssueById } from "../core/issues.ts"; |
| 25 | 25 |
import type { FleetError } from "./fleet.ts"; |
| 26 | 26 |
import type { LimbIssue } from "./limbs.ts"; |
| 27 |
import { IN_HAND } from "./plan_words.ts"; |
| 27 | 28 |
|
| 28 | 29 |
export const REPLAN_ACTIONS = ["replan", "takeback"] as const; |
| 29 | 30 |
export type ReplanAction = (typeof REPLAN_ACTIONS)[number]; |
|
|
@@ -145,7 +146,7 @@ export function takeBackLimb( |
| 145 | 146 |
const pending = pendingFor(app.db, repo, limb.id); |
| 146 | 147 |
if (pending === undefined) return fail("bad_action", "it is not with the planner"); |
| 147 | 148 |
if (!seen.value.takeBack) |
| 148 | |
return fail("bad_action", "the planner has it in hand already"); |
| 149 |
return fail("bad_action", IN_HAND); |
| 149 | 150 |
recordTakenBack(app.db, pending.id); |
| 150 | 151 |
app.bus.append({ |
| 151 | 152 |
kind: "limb.replan_taken_back", |
src/actions/plan.ts
+2
−1
|
|
@@ -28,6 +28,7 @@ import { transact } from "../kernel/db.ts"; |
| 28 | 28 |
import { id as newId } from "../kernel/id.ts"; |
| 29 | 29 |
import { err, ok, type Result } from "../kernel/result.ts"; |
| 30 | 30 |
import { isMirror } from "../origins/write-through.ts"; |
| 31 |
import { SELF_WAIT } from "./plan_words.ts"; |
| 31 | 32 |
import { followLink } from "./pillars.ts"; |
| 32 | 33 |
|
| 33 | 34 |
export const PLAN_MAX = 12; |
|
|
@@ -226,7 +227,7 @@ function vet( |
| 226 | 227 |
if (w === item.key) |
| 227 | 228 |
return refuse( |
| 228 | 229 |
"self", |
| 229 | |
`"${item.key}": ${"an issue cannot wait for itself"}`, |
| 230 |
`"${item.key}": ${SELF_WAIT}`, |
| 230 | 231 |
); |
| 231 | 232 |
} else { |
| 232 | 233 |
const found = getIssue(app.db, repo.id, w); |
src/actions/plan_words.ts
added
+12
|
|
@@ -0,0 +1,12 @@ |
| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
export const SELF_WAIT = "an issue cannot wait for itself"; |
| 10 |
|
| 11 |
|
| 12 |
export const IN_HAND = "the planner has it in hand already"; |
src/contract/openapi.ts
+2
−1
|
|
@@ -9,6 +9,7 @@ |
| 9 | 9 |
|
| 10 | 10 |
import { CAPACITY_KEYS } from "../core/capacity.ts"; |
| 11 | 11 |
import { LIMIT_KEYS } from "../settings/limits.ts"; |
| 12 |
import { SELF_WAIT } from "../actions/plan_words.ts"; |
| 12 | 13 |
|
| 13 | 14 |
interface Route { |
| 14 | 15 |
readonly method: "get" | "post" | "put" | "patch" | "delete"; |
|
|
@@ -1009,7 +1010,7 @@ export const ROUTES: readonly Route[] = [ |
| 1009 | 1010 |
body: { blockedBy: "integer" }, |
| 1010 | 1011 |
refusals: { |
| 1011 | 1012 |
no_such_issue: "`blockedBy` names no issue in this repository (422)", |
| 1012 | |
self: "an issue cannot wait for itself (422)", |
| 1013 |
self: `${SELF_WAIT} (422)`, |
| 1013 | 1014 |
cycle: "that would make a loop (422)", |
| 1014 | 1015 |
cross_repo: "dependencies stay inside one repository (422)", |
| 1015 | 1016 |
not_an_issue: "only issues can depend on issues (422)", |
src/tui/screens/limbs.ts
+2
−1
|
|
@@ -25,6 +25,7 @@ |
| 25 | 25 |
|
| 26 | 26 |
import { undoFleetOp } from "../../actions/fleet.ts"; |
| 27 | 27 |
import { previewReplan, replanLimb, takeBackLimb } from "../../actions/limb_replan.ts"; |
| 28 |
import { IN_HAND } from "../../actions/plan_words.ts"; |
| 28 | 29 |
import { pendingReplans } from "../../core/limb_replan.ts"; |
| 29 | 30 |
import { applyLimb, previewLimb, type LimbPreview } from "../../actions/limbs.ts"; |
| 30 | 31 |
import { canAdmin, canRead, canSeeOwnerFacts, type Repo } from "../../core/repos.ts"; |
|
|
@@ -197,7 +198,7 @@ export function limbsScreen( |
| 197 | 198 |
} |
| 198 | 199 |
const p = seen.value; |
| 199 | 200 |
if (p.withPlanner && !p.takeBack) { |
| 200 | |
ctx.toast("the planner has it in hand already", "danger"); |
| 201 |
ctx.toast(IN_HAND, "danger"); |
| 201 | 202 |
return; |
| 202 | 203 |
} |
| 203 | 204 |
if (!p.withPlanner && p.blocked !== undefined) { |
test/plan-words.test.ts
added
+34
|
|
@@ -0,0 +1,34 @@ |
| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
import { describe, expect, test } from "bun:test"; |
| 8 |
import { readFileSync } from "node:fs"; |
| 9 |
import { join } from "node:path"; |
| 10 |
import { IN_HAND, SELF_WAIT } from "../src/actions/plan_words.ts"; |
| 11 |
|
| 12 |
const READERS = [ |
| 13 |
"src/actions/dependencies.ts", |
| 14 |
"src/actions/plan.ts", |
| 15 |
"src/actions/limb_replan.ts", |
| 16 |
"src/tui/screens/limbs.ts", |
| 17 |
"src/contract/openapi.ts", |
| 18 |
]; |
| 19 |
|
| 20 |
const source = (path: string) => |
| 21 |
readFileSync(join(import.meta.dir, "..", path), "utf8"); |
| 22 |
|
| 23 |
describe("the planner's words", () => { |
| 24 |
for (const [name, words] of Object.entries({ SELF_WAIT, IN_HAND })) { |
| 25 |
test(`${name} says something`, () => { |
| 26 |
expect(words.trim().length).toBeGreaterThan(0); |
| 27 |
}); |
| 28 |
|
| 29 |
for (const path of READERS) |
| 30 |
test(`${path} does not type ${name} out again`, () => { |
| 31 |
expect(source(path)).not.toContain(words); |
| 32 |
}); |
| 33 |
} |
| 34 |
}); |