Public: anyone can read this; the crew can change it.

The feed's cursor and chip refusals live in one module, read by the feed routes and MCP #5675

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/5648-the-feed-s-cursor-and-chip-refusals-live → main 7 commits fork 3 threads · 3 unresolved

  • Merged. mason/5648-the-feed-s-cursor-and-chip-refusals-live was merged into main as afb7e29.

Review

Sign in to review this change.
6 files changed +53 −5
src/actions/feed_words.ts added +11
@@ -0,0 +1,11 @@
1 /**
2 * The feed's refusals in words (#5648): a cursor from elsewhere and a chip
3 * of an unknown kind read the same from every door — the feed routes and
4 * MCP — because they are said here, once.
5 */
6
7 /** A feed cursor this forge did not hand out. */
8 export const CURSOR_NOT_ISSUED = "that cursor was not issued here";
9
10 /** A chip asked for a kind the feed has no chip for. */
11 export const KIND_HAS_NO_CHIP = "that kind has no chip";
src/http/feed.ts +3 −2
@@ -20,6 +20,7 @@ import {
2020 recordInteraction,
2121 } from "../core/interactions.ts";
2222 import { chipsOf, isKindChip, setChip } from "../core/feed_chips.ts";
23 import { CURSOR_NOT_ISSUED, KIND_HAS_NO_CHIP } from "../actions/feed_words.ts";
2324 import { getUserByName, getUsersByIds } from "../core/users.ts";
2425 import { drainQueue } from "../core/shares.ts";
2526 import { feedTag, hasQueuedShares } from "../feed/feed_tag.ts";
@@ -70,7 +71,7 @@ export function feedRoutes(router: Router<Handler>): void {
7071 const rawCursor = q.get("cursor");
7172 const cursor = rawCursor === null ? undefined : parseCursor(rawCursor);
7273 if (rawCursor !== null && cursor === undefined)
73 return apiError("invalid_cursor", 422, "that cursor was not issued here");
74 return apiError("invalid_cursor", 422, CURSOR_NOT_ISSUED);
7475 // A 304 only when nothing the viewer would read has changed (`feedTag`).
7576 // The queue drains first, as `readFeed` would; shares still waiting
7677 // depend on the clock, so they always get a page.
@@ -133,7 +134,7 @@ export function feedRoutes(router: Router<Handler>): void {
133134 return apiError("invalid_hidden", 422, HIDDEN_WORDS);
134135 if (input.kind === "kind") {
135136 if (!isKindChip(value))
136 return apiError("invalid_chip", 422, "that kind has no chip");
137 return apiError("invalid_chip", 422, KIND_HAS_NO_CHIP);
137138 setChip(ctx.app.db, ctx.auth.user!.id, { kind: "kind", value }, input.hidden);
138139 } else if (input.kind === "person") {
139140 const who = getUserByName(ctx.app.db, value);
src/http/mcp/tools/events.ts +2 −1
@@ -1,4 +1,5 @@
11 import { EVENTS_LIMIT_MAX, eventsLimit, eventsPage, validEventsCursor } from "../../../core/events_page.ts";
2 import { CURSOR_NOT_ISSUED } from "../../../actions/feed_words.ts";
23 import { good, bad, type Tool, REPO_ARG, repoArg, isAnswer } from "../shared.ts";
34
45 export const tool: Tool = {
@@ -22,7 +23,7 @@ export const tool: Tool = {
2223 if (limit === undefined) return bad("invalid", `limit is 1 to ${EVENTS_LIMIT_MAX}`);
2324 const given = args["cursor"];
2425 if (given !== undefined && (typeof given !== "string" || !validEventsCursor(given)))
25 return bad("invalid", "that cursor was not issued here");
26 return bad("invalid", CURSOR_NOT_ISSUED);

Non-blocking: the events cursor borrows the feed's sentence. That is fine while the wording is shared, but accounts.ts says the same thing with its own copy. Either rename this constant to a general cursor sentence and import it there too, or give each door its own constant.

2627 return good(eventsPage(ctx.app.db, repo.id, user, limit, given));
2728 },
2829 };
src/http/mcp/tools/feed.ts +2 −1
@@ -1,4 +1,5 @@
11 import { FEED_LIMIT_DEFAULT, FEED_LIMIT_MAX, parseCursor, readFeed } from "../../../feed/feed.ts";
2 import { CURSOR_NOT_ISSUED } from "../../../actions/feed_words.ts";
23 import { good, bad, type Tool, CHIP_ARGS, chipArgs, str } from "../shared.ts";
34
45 export const tool: Tool = {
@@ -25,7 +26,7 @@ export const tool: Tool = {
2526 const given = args["cursor"];
2627 const cursor = given === undefined ? undefined : parseCursor(str(given));
2728 if (given !== undefined && cursor === undefined)
28 return bad("invalid", "that cursor was not issued here");
29 return bad("invalid", CURSOR_NOT_ISSUED);
2930 return good(
3031 readFeed(ctx.app.db, user, {
3132 limit,
src/http/mcp/tools/feed_chips.ts +2 −1
@@ -1,5 +1,6 @@
11 import { getUserByName, getUsersByIds } from "../../../core/users.ts";
22 import { chipsOf, isKindChip, setChip } from "../../../core/feed_chips.ts";
3 import { KIND_HAS_NO_CHIP } from "../../../actions/feed_words.ts";
34 import { good, bad, type Tool, str } from "../shared.ts";
45 import { HIDDEN_WORDS } from "../../../core/arg_words.ts";
56
@@ -24,7 +25,7 @@ export const tool: Tool = {
2425 const hidden = args["hidden"];
2526 if (typeof hidden !== "boolean") return bad("invalid", HIDDEN_WORDS);
2627 if (args["kind"] === "kind") {
27 if (!isKindChip(value)) return bad("invalid", "that kind has no chip");
28 if (!isKindChip(value)) return bad("invalid", KIND_HAS_NO_CHIP);
2829 setChip(ctx.app.db, user.id, { kind: "kind", value }, hidden);
2930 } else if (args["kind"] === "person") {
3031 const who = getUserByName(ctx.app.db, value);
test/feed-words.test.ts added +33
@@ -0,0 +1,33 @@
1 /**
2 * The feed's refusals are said from one place (#5648): src/actions/feed_words.ts
3 * holds each sentence, and the feed routes and MCP import it rather than
4 * typing a copy that could drift.
5 */
6
7 import { expect, test } from "bun:test";
8 import { readFileSync } from "node:fs";
9 import { join } from "node:path";
10 import { CURSOR_NOT_ISSUED, KIND_HAS_NO_CHIP } from "../src/actions/feed_words.ts";
11
12 const ROOT = join(import.meta.dir, "..");
13 const READERS = [
14 "src/http/feed.ts",
15 "src/http/mcp/tools/events.ts",
16 "src/http/mcp/tools/feed.ts",
17 "src/http/mcp/tools/feed_chips.ts",
18 ];
19
20 test("the module says each sentence as it always read", () => {
21 expect(CURSOR_NOT_ISSUED).toBe("that cursor was not issued here");
22 expect(KIND_HAS_NO_CHIP).toBe("that kind has no chip");
23 });
24
25 for (const file of READERS) {
26 test(`${file} imports the sentences and types no copy`, () => {
27 const text = readFileSync(join(ROOT, file), "utf8");
28 expect(text).toMatch(/from "(\.\.\/)+actions\/feed_words\.ts"/);
29 for (const sentence of [CURSOR_NOT_ISSUED, KIND_HAS_NO_CHIP]) {
30 expect(text).not.toContain(sentence);
31 }
32 });
33 }

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs