Public: anyone can read this; the crew can change it.
An argument's allowed-values refusals live in one module, read by the API, the feed and MCP #5686
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/5644-an-argument-s-allowed-values-refusals-li → main 9 commits fork 2 threads · 2 unresolved
16 files
changed +128
−21
src/contract/openapi/routes/getApiV1Asked.ts +2 −1
| @@ -1,4 +1,5 @@ | ||
| 1 | 1 | import type { Route } from "../shared.ts"; |
| 2 | import { ORDER_WORDS } from "../../../core/arg_words.ts"; | |
| 2 | 3 | |
| 3 | 4 | export const route: Route = { |
| 4 | 5 | method: "get", |
| @@ -6,5 +7,5 @@ export const route: Route = { | ||
| 6 | 7 | summary: "What people asked, and the caller's thought stream; answers { asked, thoughts } (ro scope)", |
| 7 | 8 | auth: true, |
| 8 | 9 | query: { show: "string", order: "string", repo: "string" }, |
| 9 | refusals: { invalid_show: "show is all, unshipped or flagged", invalid_order: "order is oldest or newest" }, | |
| 10 | refusals: { invalid_show: "show is all, unshipped or flagged", invalid_order: ORDER_WORDS }, | |
| 10 | 11 | }; |
src/contract/openapi/routes/postApiV1ReposOwnerNameMakeItYours.ts +2 −1
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | import { PROVE_FIRST } from "../../../actions/verify_words.ts"; |
| 2 | 2 | import { REPO, type Route } from "../shared.ts"; |
| 3 | import { VISIBILITY_WORDS } from "../../../core/arg_words.ts"; | |
| 3 | 4 | |
| 4 | 5 | export const route: Route = { |
| 5 | 6 | method: "post", |
| @@ -9,7 +10,7 @@ export const route: Route = { | ||
| 9 | 10 | auth: true, |
| 10 | 11 | body: { dryRun: "boolean", name: "string", visibility: "string" }, |
| 11 | 12 | refusals: { |
| 12 | invalid: "422: visibility is public or private", | |
| 13 | invalid: `422: ${VISIBILITY_WORDS}`, | |
| 13 | 14 | own: "409: this app is already yours", |
| 14 | 15 | unverified: `403: ${PROVE_FIRST}`, |
| 15 | 16 | limit: "429: at most so many copies a day; { resetsAt }", |
src/contract/openapi/routes/putApiV1ReposOwnerNameUpstream.ts +2 −1
| @@ -1,4 +1,5 @@ | ||
| 1 | 1 | import { REPO, type Route } from "../shared.ts"; |
| 2 | import { ENABLED_WORDS } from "../../../core/arg_words.ts"; | |
| 2 | 3 | |
| 3 | 4 | export const route: Route = { |
| 4 | 5 | method: "put", |
| @@ -7,7 +8,7 @@ export const route: Route = { | ||
| 7 | 8 | auth: true, |
| 8 | 9 | body: { url: "string", branch: "string", enabled: "boolean" }, |
| 9 | 10 | refusals: { |
| 10 | bad_enabled: "422: enabled is true or false", | |
| 11 | bad_enabled: `422: ${ENABLED_WORDS}`, | |
| 11 | 12 | invalid_url: "422: not an https or http git address", |
| 12 | 13 | invalid_branch: "422: not a branch name git accepts", |
| 13 | 14 | not_found: "404: no such repository, or it is not readable by the caller", |
src/core/arg_words.ts added +26
| @@ -0,0 +1,26 @@ | ||
| 1 | /** | |
| 2 | * What a caller is told when an argument is outside its allowed values | |
| 3 | * (#5644). The API, the feed and MCP refuse the same argument with the same | |
| 4 | * sentence, so each is written here once and read by every door. | |
| 5 | */ | |
| 6 | ||
| 7 | /** An answer to a pull: `like` or `dislike`. */ | |
| 8 | export const ANSWER_WORDS = "answer is like or dislike"; | |
| 9 | ||
| 10 | /** Whether an upstream is followed: `enabled`, a boolean. */ | |
| 11 | export const ENABLED_WORDS = "enabled is true or false"; | |
| 12 | ||
| 13 | /** Whether a feed chip is hidden: a boolean. */ | |
| 14 | export const HIDDEN_WORDS = "hidden is true or false"; | |
| 15 | ||
| 16 | /** A board pin's `op`: `pin`, `unpin` or `move`. */ | |
| 17 | export const PIN_OP_WORDS = "op must be pin, unpin or move"; | |
| 18 | ||
| 19 | /** A listing's `order`: `oldest` or `newest`. */ | |
| 20 | export const ORDER_WORDS = "order is oldest or newest"; | |
| 21 | ||
| 22 | /** The forge's `signup` setting: `open`, `invite` or `closed`. */ | |
| 23 | export const SIGNUP_WORDS = "signup is open, invite or closed"; | |
| 24 | ||
| 25 | /** A repository's `visibility`: `public` or `private`. */ | |
| 26 | export const VISIBILITY_WORDS = "visibility is public or private"; |
src/http/api.ts +7 −6
| @@ -471,6 +471,7 @@ import { REPORT_RECEIVED } from "../actions/report_words.ts"; | ||
| 471 | 471 | import { ADMIN_WORDS } from "../actions/admin_words.ts"; |
| 472 | 472 | import { SETTING_WORDS } from "../actions/setting_words.ts"; |
| 473 | 473 | import { SPEND_WORDS } from "../actions/spend_words.ts"; |
| 474 | import { ANSWER_WORDS, ENABLED_WORDS, ORDER_WORDS, PIN_OP_WORDS, SIGNUP_WORDS, VISIBILITY_WORDS } from "../core/arg_words.ts"; | |
| 474 | 475 | |
| 475 | 476 | /** Take the parent's updates for `repo` and answer as the API does, for every route that asks. */ |
| 476 | 477 | async function respondTake(ctx: Ctx, repo: Repo): Promise<Response> { |
| @@ -1596,7 +1597,7 @@ export function apiRoutes(router: Router<Handler>): void { | ||
| 1596 | 1597 | : op === "unpin" ? unpinPanel(ctx.app, ctx.auth.user, slug, title) |
| 1597 | 1598 | : op === "move" ? movePinned(ctx.app, ctx.auth.user, slug, title, by) |
| 1598 | 1599 | : undefined; |
| 1599 | if (!done) return apiError("invalid", 422, "op must be pin, unpin or move"); | |
| 1600 | if (!done) return apiError("invalid", 422, PIN_OP_WORDS); | |
| 1600 | 1601 | if (done.ok) return json({ ...done.value }); |
| 1601 | 1602 | const e: PinError = done.error; |
| 1602 | 1603 | return e === "not_found" ? notFound() : apiError(e, 422, e === "too_many" ? "too many pins" : "that is not a panel"); |
| @@ -1794,7 +1795,7 @@ export function apiRoutes(router: Router<Handler>): void { | ||
| 1794 | 1795 | const set = changeSignup(ctx.app, ctx.auth.user, input.signup); |
| 1795 | 1796 | if (set.ok) return json({ signup: set.value }); |
| 1796 | 1797 | if (set.error === "not_found") return notFound(); |
| 1797 | return apiError(set.error, 422, "signup is open, invite or closed"); | |
| 1798 | return apiError(set.error, 422, SIGNUP_WORDS); | |
| 1798 | 1799 | } |
| 1799 | 1800 | const quotas = input.quotas; |
| 1800 | 1801 | if (typeof quotas !== "object" || quotas === null || Array.isArray(quotas)) |
| @@ -2461,7 +2462,7 @@ export function apiRoutes(router: Router<Handler>): void { | ||
| 2461 | 2462 | if (!repo) return notFound(); |
| 2462 | 2463 | const input = await body(ctx.request); |
| 2463 | 2464 | if (typeof input.enabled !== "boolean") |
| 2464 | return apiError("bad_enabled", 422, "enabled is true or false"); | |
| 2465 | return apiError("bad_enabled", 422, ENABLED_WORDS); | |
| 2465 | 2466 | const saved = setFollowUpstream(ctx.app, ctx.auth.user, repo, { |
| 2466 | 2467 | url: str(input.url), |
| 2467 | 2468 | branch: str(input.branch), |
| @@ -3858,7 +3859,7 @@ export function apiRoutes(router: Router<Handler>): void { | ||
| 3858 | 3859 | return apiError("invalid_show", 422, "show is all, unshipped or flagged"); |
| 3859 | 3860 | const order = ctx.url.searchParams.get("order"); |
| 3860 | 3861 | if (order !== null && !isAskedOrder(order)) |
| 3861 | return apiError("invalid_order", 422, "order is oldest or newest"); | |
| 3862 | return apiError("invalid_order", 422, ORDER_WORDS); | |
| 3862 | 3863 | const read = timePhase(ctx.request, "asked", () => { |
| 3863 | 3864 | const repos = askedRepos(ctx.app.db, ctx.auth.user, ctx.url.searchParams.get("repo")); |
| 3864 | 3865 | return { |
| @@ -4556,7 +4557,7 @@ export function apiRoutes(router: Router<Handler>): void { | ||
| 4556 | 4557 | const input = await body(ctx.request); |
| 4557 | 4558 | const answer = str(input.answer); |
| 4558 | 4559 | if (answer !== "like" && answer !== "dislike") |
| 4559 | return apiError("invalid_answer", 422, "answer is like or dislike"); | |
| 4560 | return apiError("invalid_answer", 422, ANSWER_WORDS); | |
| 4560 | 4561 | const voted = voteVariant( |
| 4561 | 4562 | ctx.app, |
| 4562 | 4563 | ctx.auth.user, |
| @@ -5388,7 +5389,7 @@ export function apiRoutes(router: Router<Handler>): void { | ||
| 5388 | 5389 | const name = str(input.name); |
| 5389 | 5390 | const visibility = str(input.visibility); |
| 5390 | 5391 | if (visibility !== "" && visibility !== "public" && visibility !== "private") |
| 5391 | return apiError("invalid", 422, "visibility is public or private"); | |
| 5392 | return apiError("invalid", 422, VISIBILITY_WORDS); | |
| 5392 | 5393 | const made = await makeItYours(ctx.app, ctx.auth.user, repo, { |
| 5393 | 5394 | dryRun, |
| 5394 | 5395 | ...(name === "" ? {} : { name }), |
src/http/feed.ts +2 −1
| @@ -43,6 +43,7 @@ import type { FleetError } from "../actions/fleet.ts"; | ||
| 43 | 43 | import { notePhase, takeSessionTime, timePhase } from "../core/route_timing.ts"; |
| 44 | 44 | import type { Handler } from "./context.ts"; |
| 45 | 45 | import type { Router } from "./router.ts"; |
| 46 | import { HIDDEN_WORDS } from "../core/arg_words.ts"; | |
| 46 | 47 | import { |
| 47 | 48 | apiError, |
| 48 | 49 | body, |
| @@ -129,7 +130,7 @@ export function feedRoutes(router: Router<Handler>): void { | ||
| 129 | 130 | const input = await body(ctx.request); |
| 130 | 131 | const value = str(input.value); |
| 131 | 132 | if (typeof input.hidden !== "boolean") |
| 132 | return apiError("invalid_hidden", 422, "hidden is true or false"); | |
| 133 | return apiError("invalid_hidden", 422, HIDDEN_WORDS); | |
| 133 | 134 | if (input.kind === "kind") { |
| 134 | 135 | if (!isKindChip(value)) |
| 135 | 136 | return apiError("invalid_chip", 422, "that kind has no chip"); |
src/http/mcp/tools/asked.ts +2 −1
| @@ -1,6 +1,7 @@ | ||
| 1 | 1 | import { askedIn, askedJson, askedRepos, isAskedOrder, isAskedShow } from "../../../core/asked.ts"; |
| 2 | 2 | import { streamJson, thoughtStream } from "../../../core/thought_stream.ts"; |
| 3 | 3 | import { good, bad, type Tool, str, REPO_ARG } from "../shared.ts"; |
| 4 | import { ORDER_WORDS } from "../../../core/arg_words.ts"; | |
| 4 | 5 | |
| 5 | 6 | export const tool: Tool = { |
| 6 | 7 | name: "asked", |
| @@ -22,7 +23,7 @@ export const tool: Tool = { | ||
| 22 | 23 | return bad("invalid_show", "show is unshipped, flagged or all"); |
| 23 | 24 | const order = args["order"] ?? "oldest"; |
| 24 | 25 | if (!isAskedOrder(order)) |
| 25 | return bad("invalid_order", "order is oldest or newest"); | |
| 26 | return bad("invalid_order", ORDER_WORDS); | |
| 26 | 27 | const spec = args["repo"] === undefined ? null : str(args["repo"]); |
| 27 | 28 | const repos = askedRepos(ctx.app.db, user, spec); |
| 28 | 29 | if (spec !== null && repos.length === 0) |
src/http/mcp/tools/board_pins.ts +2 −1
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | import { listPinned as listPinnedPanels, movePinned, pinPanel, unpinPanel } from "../../../actions/board-pins.ts"; |
| 2 | 2 | import { good, bad, type Tool, boardRefused, mayBoards } from "../shared.ts"; |
| 3 | import { PIN_OP_WORDS } from "../../../core/arg_words.ts"; | |
| 3 | 4 | |
| 4 | 5 | export const tool: Tool = { |
| 5 | 6 | name: "board_pins", |
| @@ -29,7 +30,7 @@ export const tool: Tool = { | ||
| 29 | 30 | : op === "unpin" ? unpinPanel(ctx.app, user, slug, title) |
| 30 | 31 | : op === "move" ? movePinned(ctx.app, user, slug, title, args["by"]) |
| 31 | 32 | : undefined; |
| 32 | if (!done) return bad("invalid", "op must be pin, unpin or move"); | |
| 33 | if (!done) return bad("invalid", PIN_OP_WORDS); | |
| 33 | 34 | return done.ok ? good({ ...done.value }) : bad(done.error, "that could not be pinned"); |
| 34 | 35 | }, |
| 35 | 36 | }; |
src/http/mcp/tools/feed_chips.ts +2 −1
| @@ -1,6 +1,7 @@ | ||
| 1 | 1 | import { getUserByName, getUsersByIds } from "../../../core/users.ts"; |
| 2 | 2 | import { chipsOf, isKindChip, setChip } from "../../../core/feed_chips.ts"; |
| 3 | 3 | import { good, bad, type Tool, str } from "../shared.ts"; |
| 4 | import { HIDDEN_WORDS } from "../../../core/arg_words.ts"; | |
| 4 | 5 | |
| 5 | 6 | export const tool: Tool = { |
| 6 | 7 | name: "feed_chips", |
| @@ -21,7 +22,7 @@ export const tool: Tool = { | ||
| 21 | 22 | if (args["kind"] !== undefined) { |
| 22 | 23 | const value = str(args["value"]); |
| 23 | 24 | const hidden = args["hidden"]; |
| 24 | if (typeof hidden !== "boolean") return bad("invalid", "hidden is true or false"); | |
| 25 | if (typeof hidden !== "boolean") return bad("invalid", HIDDEN_WORDS); | |
| 25 | 26 | if (args["kind"] === "kind") { |
| 26 | 27 | if (!isKindChip(value)) return bad("invalid", "that kind has no chip"); |
| 27 | 28 | setChip(ctx.app.db, user.id, { kind: "kind", value }, hidden); |
src/http/mcp/tools/make_it_yours.ts +2 −1
| @@ -3,6 +3,7 @@ import { NOT_A_MEMBER } from "../../../core/roles.ts"; | ||
| 3 | 3 | import { COPIES_PER_DAY, makeItYours } from "../../../actions/make_it_yours.ts"; |
| 4 | 4 | import { PROVE_FIRST } from "../../../actions/verify_words.ts"; |
| 5 | 5 | import { good, bad, type Tool, str, REPO_ARG, repoArg, isAnswer } from "../shared.ts"; |
| 6 | import { VISIBILITY_WORDS } from "../../../core/arg_words.ts"; | |
| 6 | 7 | |
| 7 | 8 | export const tool: Tool = { |
| 8 | 9 | name: "make_it_yours", |
| @@ -27,7 +28,7 @@ export const tool: Tool = { | ||
| 27 | 28 | const name = str(args["name"]); |
| 28 | 29 | const visibility = str(args["visibility"]); |
| 29 | 30 | if (visibility !== "" && visibility !== "public" && visibility !== "private") |
| 30 | return bad("invalid", "visibility is public or private"); | |
| 31 | return bad("invalid", VISIBILITY_WORDS); | |
| 31 | 32 | const made = await makeItYours(ctx.app, user, repo, { |
| 32 | 33 | dryRun: args["dryRun"] !== false, |
| 33 | 34 | ...(name === "" ? {} : { name }), |
src/http/mcp/tools/repo_settings.ts +2 −1
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | import { updateRepoSettings } from "../../../actions/repos.ts"; |
| 2 | 2 | import { good, bad, type Tool, REPO_ARG, repoArg, isAnswer } from "../shared.ts"; |
| 3 | import { VISIBILITY_WORDS } from "../../../core/arg_words.ts"; | |
| 3 | 4 | |
| 4 | 5 | export const tool: Tool = { |
| 5 | 6 | name: "repo_settings", |
| @@ -24,7 +25,7 @@ export const tool: Tool = { | ||
| 24 | 25 | if (description !== undefined && typeof description !== "string") |
| 25 | 26 | return bad("invalid", "description is text"); |
| 26 | 27 | if (visibility !== undefined && visibility !== "public" && visibility !== "private") |
| 27 | return bad("invalid", "visibility is public or private"); | |
| 28 | return bad("invalid", VISIBILITY_WORDS); | |
| 28 | 29 | const updated = await updateRepoSettings(ctx.app, user, repo, { |
| 29 | 30 | ...(visibility !== undefined ? { visibility } : {}), |
| 30 | 31 | ...(description !== undefined ? { description } : {}), |
src/http/mcp/tools/signup.ts +3 −2
| @@ -2,6 +2,7 @@ import { changeSignup, readSignup } from "../../../actions/signup.ts"; | ||
| 2 | 2 | import { SIGNUP_MODES } from "../../../settings/signup.ts"; |
| 3 | 3 | import { scopeAllows } from "../../../core/tokens.ts"; |
| 4 | 4 | import { good, bad, type Tool } from "../shared.ts"; |
| 5 | import { SIGNUP_WORDS } from "../../../core/arg_words.ts"; | |
| 5 | 6 | |
| 6 | 7 | export const tool: Tool = { |
| 7 | 8 | name: "signup", |
| @@ -22,7 +23,7 @@ export const tool: Tool = { | ||
| 22 | 23 | if (!read.ok) return bad("not_found", "no such tool here"); |
| 23 | 24 | if (args["mode"] === undefined) return good(read.value); |
| 24 | 25 | if (!(SIGNUP_MODES as readonly unknown[]).includes(args["mode"])) |
| 25 | return bad("invalid_signup", "signup is open, invite or closed"); | |
| 26 | return bad("invalid_signup", SIGNUP_WORDS); | |
| 26 | 27 | if (args["dryRun"] !== false) |
| 27 | 28 | return good({ |
| 28 | 29 | says: `Would set signup from ${read.value.signup} to ${String(args["mode"])}. Send dryRun: false to change it.`, |
| @@ -30,7 +31,7 @@ export const tool: Tool = { | ||
| 30 | 31 | if (!scopeAllows(ctx.auth.scope ?? "ro", "admin")) |
| 31 | 32 | return bad("forbidden", "changing the forge's signup needs an admin-scope token"); |
| 32 | 33 | const changed = changeSignup(ctx.app, user, args["mode"]); |
| 33 | if (!changed.ok) return bad(changed.error, "signup is open, invite or closed"); | |
| 34 | if (!changed.ok) return bad(changed.error, SIGNUP_WORDS); | |
| 34 | 35 | return good({ |
| 35 | 36 | says: `signup: ${read.value.signup} → ${changed.value}.`, |
| 36 | 37 | signup: changed.value, |
src/http/mcp/tools/upstream.ts +2 −1
| @@ -3,6 +3,7 @@ import { getFollow } from "../../../core/upstream_follow.ts"; | ||
| 3 | 3 | import { canAdmin } from "../../../core/repos.ts"; |
| 4 | 4 | import { scopeAllows } from "../../../core/tokens.ts"; |
| 5 | 5 | import { good, bad, type Tool, str, REPO_ARG, repoArg, isAnswer } from "../shared.ts"; |
| 6 | import { ENABLED_WORDS } from "../../../core/arg_words.ts"; | |
| 6 | 7 | |
| 7 | 8 | export const tool: Tool = { |
| 8 | 9 | name: "upstream", |
| @@ -41,7 +42,7 @@ export const tool: Tool = { | ||
| 41 | 42 | return checked.ok ? good(checked.value) : refuse(checked.error); |
| 42 | 43 | } |
| 43 | 44 | const { enabled, url, branch } = args; |
| 44 | if (typeof enabled !== "boolean") return bad("bad_enabled", "enabled is true or false"); | |
| 45 | if (typeof enabled !== "boolean") return bad("bad_enabled", ENABLED_WORDS); | |
| 45 | 46 | if ((url !== undefined && typeof url !== "string") || (branch !== undefined && typeof branch !== "string")) |
| 46 | 47 | return bad("invalid", "url and branch are text"); |
| 47 | 48 | const saved = setFollowUpstream(ctx.app, user, repo, { url: str(url), branch: str(branch), enabled }); |
src/http/mcp/tools/variant.ts +2 −1
| @@ -3,6 +3,7 @@ import { mayView } from "../../../core/sealed.ts"; | ||
| 3 | 3 | import { getPull } from "../../../core/pulls.ts"; |
| 4 | 4 | import { VOTE_NOT_RECORDED } from "../../../actions/board_words.ts"; |
| 5 | 5 | import { good, bad, type Tool, str, REPO_ARG, repoArg, isAnswer } from "../shared.ts"; |
| 6 | import { ANSWER_WORDS } from "../../../core/arg_words.ts"; | |
| 6 | 7 | |
| 7 | 8 | export const tool: Tool = { |
| 8 | 9 | name: "variant", |
| @@ -32,7 +33,7 @@ export const tool: Tool = { | ||
| 32 | 33 | return bad("no_such_pull", `no pull #${String(n)}`); |
| 33 | 34 | const answer = args["answer"]; |
| 34 | 35 | if (answer !== "like" && answer !== "dislike") |
| 35 | return bad("invalid_answer", "answer is like or dislike"); | |
| 36 | return bad("invalid_answer", ANSWER_WORDS); | |
| 36 | 37 | const voted = voteVariant( |
| 37 | 38 | ctx.app, |
| 38 | 39 | user, |
test/arg-words.test.ts added +67
| @@ -0,0 +1,67 @@ | ||
| 1 | /** | |
| 2 | * An argument outside its allowed values is refused with one sentence, said | |
| 3 | * from src/core/arg_words.ts and read by every door (#5644). A copy typed | |
| 4 | * back into a door, or into the published contract, fails here. | |
| 5 | */ | |
| 6 | ||
| 7 | import { describe, expect, test } from "bun:test"; | |
| 8 | import { readFileSync } from "node:fs"; | |
| 9 | import { join } from "node:path"; | |
| 10 | import * as words from "../src/core/arg_words.ts"; | |
| 11 | ||
| 12 | const ROOT = join(import.meta.dir, ".."); | |
| 13 | /** Three folders down: an MCP tool file or a contract route file. */ | |
| 14 | const DEEP = "../../../core/arg_words.ts"; | |
| 15 | /** Each file that refuses with these words, and how it imports them. */ | |
| 16 | const DOORS: Record<string, string> = { | |
| 17 | "src/http/api.ts": "../core/arg_words.ts", | |
| 18 | "src/http/feed.ts": "../core/arg_words.ts", | |
| 19 | "src/http/mcp/tools/asked.ts": DEEP, | |
| 20 | "src/http/mcp/tools/board_pins.ts": DEEP, | |
| 21 | "src/http/mcp/tools/feed_chips.ts": DEEP, | |
| 22 | "src/http/mcp/tools/make_it_yours.ts": DEEP, | |
| 23 | "src/http/mcp/tools/repo_settings.ts": DEEP, | |
| 24 | "src/http/mcp/tools/signup.ts": DEEP, | |
| 25 | "src/http/mcp/tools/upstream.ts": DEEP, | |
| 26 | "src/http/mcp/tools/variant.ts": DEEP, | |
| 27 | "src/contract/openapi/routes/getApiV1Asked.ts": DEEP, | |
| 28 | "src/contract/openapi/routes/postApiV1ReposOwnerNameMakeItYours.ts": DEEP, | |
| 29 | "src/contract/openapi/routes/putApiV1ReposOwnerNameUpstream.ts": DEEP, | |
| 30 | }; | |
| 31 | ||
| 32 | describe("an argument's allowed-values refusals", () => { | |
| 33 | test("the module says each sentence", () => { | |
| 34 | expect(Object.values(words).sort()).toEqual([ | |
| 35 | "answer is like or dislike", | |
| 36 | "enabled is true or false", | |
| 37 | "hidden is true or false", | |
| 38 | "op must be pin, unpin or move", | |
| 39 | "order is oldest or newest", | |
| 40 | "signup is open, invite or closed", | |
| 41 | "visibility is public or private", | |
| 42 | ]); | |
| 43 | }); | |
| 44 | ||
| 45 | for (const [door, from] of Object.entries(DOORS)) { | |
| 46 | test(`${door} reads them and types none of them out`, () => { | |
| 47 | const text = readFileSync(join(ROOT, door), "utf8"); | |
| 48 | expect(text).toContain(`from "${from}"`); | |
| 49 | for (const sentence of Object.values(words)) { | |
| 50 | expect(text).not.toContain(sentence); | |
| 51 | } | |
| 52 | }); | |
| 53 | } | |
| 54 | ||
| 55 | test("no other source file types one of them out", () => { | |
| 56 | const glob = new Bun.Glob("src/**/*.ts"); | |
| 57 | const copies: string[] = []; | |
| 58 | for (const file of glob.scanSync(ROOT)) { | |
| 59 | if (file === "src/core/arg_words.ts") continue; | |
| 60 | const text = readFileSync(join(ROOT, file), "utf8"); | |
| 61 | for (const sentence of Object.values(words)) { | |
| 62 | if (text.includes(`"${sentence}"`) || text.includes(`"422: ${sentence}"`)) copies.push(`${file}: ${sentence}`); | |
| 63 | } | |
| 64 | } | |
| 65 | expect(copies).toEqual([]); | |
| 66 | }); | |
| 67 | }); |
test/sitemap.test.ts +3 −2
| @@ -34,7 +34,8 @@ afterAll(async () => { | ||
| 34 | 34 | await rm(dataDir, { recursive: true, force: true }); |
| 35 | 35 | }); |
| 36 | 36 | |
| 37 | const phases = (h: string) => [...h.matchAll(/(\w+);dur=([\d.]+)/g)].reduce((n, m) => n + Number(m[2]), 0); | |
| 37 | /** The whole request's time: `app` already holds the named phases, so they are not added again. */ | |
| 38 | const total = (h: string) => Number(/(?:^|, )app;dur=([\d.]+)/.exec(h)?.[1] ?? Infinity); | |
| 38 | 39 | |
| 39 | 40 | test("lists the 100 public repositories and none of the 20 private ones, built in under 15 ms", async () => { |
| 40 | 41 | let best = Infinity; |
| @@ -44,7 +45,7 @@ test("lists the 100 public repositories and none of the 20 private ones, built i | ||
| 44 | 45 | forgetListings(app.db); |
| 45 | 46 | res = await fetch(`${servers.url}/sitemap.xml`); |
| 46 | 47 | body = await res.text(); |
| 47 | best = Math.min(best, phases(res.headers.get("server-timing") ?? "")); | |
| 48 | best = Math.min(best, total(res.headers.get("server-timing") ?? "")); | |
| 48 | 49 | } |
| 49 | 50 | expect(body.match(/\/alice\/pub\d+</g)?.length).toBe(100); |
| 50 | 51 | expect(body).not.toContain("priv"); |
new above
offline
Right fix.
appalready containsrepos/render/etag(anddb), as the preview's header shows, so summing them was counting the same time twice. Missing header gives Infinity, so the test fails closed.