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

Absorb: schedule, feed items, MCP tools, secret settings and /system health (the doc's Not done yet) #9888

Merged opened this

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

Merged

mason/forge:mason/9771-absorb-schedule-feed-items-mcp-tools-sec → main 2 commits fork 3 threads · 3 unresolved

  • Merged. mason/9771-absorb-schedule-feed-items-mcp-tools-sec was merged into main as feb7455.

Review

Sign in to review this change.
3 files changed +38 −3
docs/features/absorb.md +1 −1
@@ -76,7 +76,7 @@ Settings stay behind on a fork. A fork of another forge's app carries its `featu
7676
7777 ## Not done yet
7878
79 The forge does not yet fire a feature's schedule, show its feed items, serve its MCP tools inside the forge's `/mcp`, keep `secret` settings or show its health on `/system`.
79 Until then an absorbed app is a menu link with kept settings (Settings › Features says so). The forge does not yet fire a feature's schedule (#9883), show its feed items (#9884), serve its MCP tools inside `/mcp` (#9885), keep `secret` settings (#9886) or show its health on `/system` (#9887).
outdated

Duplicates the paragraph two lines below. Merge into one paragraph under Not done yet.

8080
8181 ## Findings
8282
src/web/pages/features.ts +2 −1
@@ -101,11 +101,12 @@ export async function featuresPage(ctx: Ctx): Promise<Response> {
101101 const rendering = performance.now();
102102 const body = html`<section class="settings-block">
103103 <h2>Features</h2>
104 <p class="muted">An absorbed app is a menu link with its settings kept here.</p>
outdated

Principle 1 and 8: this repeats the empty-state note and the doc. One plain sentence, shown once; e.g. "An absorbed app is a menu link with its settings kept here." and drop the list of five, which the doc carries. Also check the terminal Features screen says the same.

104105 ${listed.value.length === 0
105106 ? empty({
106107 icon: "rocket",
107108 title: "No app declares a feature yet",
108 note: 'An app declares one with a "feature" block in its forge.json: a name, a page and typed settings. Absorbing it puts it in the forge\'s nav and keeps its settings here.',
109 note: 'An app declares one with a "feature" block in its forge.json: a name, a page and typed settings.',
109110 action: html`<p><a href="${DOCS}">How an app declares a feature</a></p>`,
110111 })
111112 : html`<ul class="list setting-list">
test/features-empty.test.ts +35 −1
@@ -5,7 +5,9 @@ import { mkdtemp, rm } from "node:fs/promises";
55 import { tmpdir } from "node:os";
66 import { join } from "node:path";
77 import { createApp, type App } from "../src/app.ts";
8 import { createUser } from "../src/core/users.ts";
8 import { commitToBranch, createRepo } from "../src/actions/repos.ts";
9 import { createApp as insertApp } from "../src/core/apps.ts";
10 import { createUser, type User } from "../src/core/users.ts";
911 import { serve, type Servers } from "../src/http/server.ts";
1012 import { quietLogs } from "./helpers/globals.ts";
1113
@@ -14,6 +16,7 @@ quietLogs();
1416 let dataDir = "";
1517 let app: App;
1618 let servers: Servers;
19 let adaUser: User;
1720
1821 beforeAll(async () => {
1922 dataDir = await mkdtemp(join(tmpdir(), "forge-features-empty-"));
@@ -28,6 +31,7 @@ beforeAll(async () => {
2831 adminIfFirst: true,
2932 });
3033 if (!made.ok) throw new Error(made.error);
34 adaUser = made.value;
3135 }, 60_000);
3236
3337 afterAll(async () => {
@@ -48,3 +52,33 @@ test("with no feature declared, the empty state carries the guide's link", async
4852 const empty = page.slice(page.indexOf("No app declares a feature yet"));
4953 expect(empty.slice(0, empty.indexOf("</div>"))).toContain('href="/trevato/forge/blob/main/docs/features/absorb.md"');
5054 });
55
56 test("the page says an absorbed app is a menu link for now", async () => {
57 const signed = await fetch(`${servers.url}/signin`, {
58 method: "POST",
59 body: new URLSearchParams({ username: "ada", password: "a-good-passphrase" }),
60 redirect: "manual",
outdated

Only the empty list is covered. Add a case with a declared feature so the line is proven on both paths.

61 });
62 const cookie = `${(signed.headers.get("set-cookie") ?? "").split(";")[0]!}; forge_mode=dev`;
63 const page = await (await fetch(`${servers.url}/settings/features`, { headers: { cookie } })).text();
64 expect(page).toContain("An absorbed app is a menu link");
65 expect(page.split("An absorbed app is a menu link").length).toBe(2);
66 });
67
68 test("the same line shows once when a feature is listed", async () => {
69 const repo = await createRepo(app, adaUser, { name: "radar", visibility: "private", description: "" });
70 if (!repo.ok) throw new Error(String(repo.error));
71 const forgeJson = JSON.stringify({ app: { run: "bun run x.ts" }, feature: { name: "Radar", page: "/", settings: {} } });
72 await commitToBranch(app, adaUser, repo.value, { message: "feature", files: [{ path: "forge.json", content: forgeJson }] });
73 insertApp(app.db, { repoId: repo.value.id, slug: "radar", access: "private" });
74 const signed = await fetch(`${servers.url}/signin`, {
75 method: "POST",
76 body: new URLSearchParams({ username: "ada", password: "a-good-passphrase" }),
77 redirect: "manual",
78 });
79 const cookie = `${(signed.headers.get("set-cookie") ?? "").split(";")[0]!}; forge_mode=dev`;
80 const page = await (await fetch(`${servers.url}/settings/features`, { headers: { cookie } })).text();
81 expect(page).not.toContain("No app declares a feature yet");
82 expect(page).toContain("Radar");
83 expect(page.split("An absorbed app is a menu link").length).toBe(2);
84 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs