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
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/9771-absorb-schedule-feed-items-mcp-tools-sec → main 2 commits fork 3 threads · 3 unresolved
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 | ||
| 76 | 76 | |
| 77 | 77 | ## Not done yet |
| 78 | 78 | |
| 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). | |
| 80 | 80 | |
| 81 | 81 | ## Findings |
| 82 | 82 |
src/web/pages/features.ts +2 −1
| @@ -101,11 +101,12 @@ export async function featuresPage(ctx: Ctx): Promise<Response> { | ||
| 101 | 101 | const rendering = performance.now(); |
| 102 | 102 | const body = html`<section class="settings-block"> |
| 103 | 103 | <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. |
||
| 104 | 105 | ${listed.value.length === 0 |
| 105 | 106 | ? empty({ |
| 106 | 107 | icon: "rocket", |
| 107 | 108 | 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.', | |
| 109 | 110 | action: html`<p><a href="${DOCS}">How an app declares a feature</a></p>`, |
| 110 | 111 | }) |
| 111 | 112 | : html`<ul class="list setting-list"> |
test/features-empty.test.ts +35 −1
| @@ -5,7 +5,9 @@ import { mkdtemp, rm } from "node:fs/promises"; | ||
| 5 | 5 | import { tmpdir } from "node:os"; |
| 6 | 6 | import { join } from "node:path"; |
| 7 | 7 | 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"; | |
| 9 | 11 | import { serve, type Servers } from "../src/http/server.ts"; |
| 10 | 12 | import { quietLogs } from "./helpers/globals.ts"; |
| 11 | 13 | |
| @@ -14,6 +16,7 @@ quietLogs(); | ||
| 14 | 16 | let dataDir = ""; |
| 15 | 17 | let app: App; |
| 16 | 18 | let servers: Servers; |
| 19 | let adaUser: User; | |
| 17 | 20 | |
| 18 | 21 | beforeAll(async () => { |
| 19 | 22 | dataDir = await mkdtemp(join(tmpdir(), "forge-features-empty-")); |
| @@ -28,6 +31,7 @@ beforeAll(async () => { | ||
| 28 | 31 | adminIfFirst: true, |
| 29 | 32 | }); |
| 30 | 33 | if (!made.ok) throw new Error(made.error); |
| 34 | adaUser = made.value; | |
| 31 | 35 | }, 60_000); |
| 32 | 36 | |
| 33 | 37 | afterAll(async () => { |
| @@ -48,3 +52,33 @@ test("with no feature declared, the empty state carries the guide's link", async | ||
| 48 | 52 | const empty = page.slice(page.indexOf("No app declares a feature yet")); |
| 49 | 53 | expect(empty.slice(0, empty.indexOf("</div>"))).toContain('href="/trevato/forge/blob/main/docs/features/absorb.md"'); |
| 50 | 54 | }); |
| 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 | }); |
Duplicates the paragraph two lines below. Merge into one paragraph under Not done yet.