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

An agent's page under Settings › Crew speaks the home page's language #9847

Merged opened this

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

Merged

scout/forge:scout/9831-an-agent-s-page-under-settings-crew-spea → main 2 commits fork

  • Merged. scout/9831-an-agent-s-page-under-settings-crew-spea was merged into main as 97f6a0d.

Review

Sign in to review this change.
2 files changed +99 −32
src/web/pages/crew.ts +25 −32
@@ -1013,43 +1013,36 @@ export async function agentCreate(ctx: Ctx): Promise<Response> {
10131013
10141014 function agentView(agent: Agent, values: AgentValues, error?: string): Html {
10151015 const at = `/settings/crew/${encodeURIComponent(agent.user.username)}`;
1016 return html`${card(
1017 html`<form method="post" action="${at}">
1018 <div class="field">
1019 <label for="f-username">Username</label>
1020 <input
1021 class="input"
1022 id="f-username"
1023 value="${agent.user.username}"
1024 readonly
1025 />
1026 </div>
1027 ${agentFields(values, error)}
1028 <div class="form-actions">
1029 ${btn("Save changes", { variant: "primary" })}
1030 ${btn("Back to the crew", { href: "/settings/crew", variant: "ghost" })}
1031 </div>
1032 </form>`,
1033 {
1034 head: html`${icon("bot")}
1035 <h2>${agent.user.username}</h2>
1036 <span class="muted small">added ${time(agent.createdAt)}</span>`,
1037 },
1038 )}
1039 ${card(
1040 html`<p>
1016 return html`<section class="settings-block">
1017 <h2>Settings</h2>
1018 <p class="muted small">added ${time(agent.createdAt)}</p>
1019 <form method="post" action="${at}">
1020 <div class="field">
1021 <label for="f-username">Username</label>
1022 <input
1023 class="input"
1024 id="f-username"
1025 value="${agent.user.username}"
1026 readonly
1027 />
1028 </div>
1029 ${agentFields(values, error)}
1030 <div class="form-actions">
1031 ${btn("Save changes", { variant: "primary" })}
1032 ${btn("Back to the crew", { href: "/settings/crew", variant: "ghost" })}
1033 </div>
1034 </form>
1035 </section>
1036 <section class="settings-block">
1037 <h2>Retire</h2>
1038 <p>
10411039 Retiring <strong>${agent.user.username}</strong> destroys its tokens and
10421040 takes it off every crew. What it wrote stays, under its name.
10431041 </p>
10441042 <div class="form-actions">
10451043 ${confirmAction({ action: `${at}/delete`, verb: "Retire this agent" })}
1046 </div>`,
1047 {
1048 tone: "danger",
1049 head: html`${icon("alert")}
1050 <h2>Retire</h2>`,
1051 },
1052 )}`;
1044 </div>
1045 </section>`;
10531046 }
10541047
10551048 function loadAgent(ctx: Ctx): Agent | undefined {
test/agent-language.test.ts added +74
@@ -0,0 +1,74 @@
1 /** An agent's page under Settings › Crew speaks the home page's language (#9831): headings and sections, no card. */
2 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { mkdtemp, rm } from "node:fs/promises";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createApp } from "../src/app.ts";
7 import { createAgent } from "../src/actions/crew.ts";
8 import { createSession } from "../src/core/sessions.ts";
9 import { createUser } from "../src/core/users.ts";
10 import { serve } from "../src/http/server.ts";
11 import { quietLogs } from "./helpers/globals.ts";
12
13 quietLogs();
14
15 let dir = "";
16 let app: ReturnType<typeof createApp>;
17 let servers: Awaited<ReturnType<typeof serve>>;
18 let admin = "";
19 let member = "";
20
21 const person = async (username: string, adminIfFirst: boolean) => {
22 const made = await createUser(app.db, {
23 origin: "seed",
24 trusted: true,
25 username,
26 email: `${username}@forge.test`,
27 adminIfFirst,
28 });
29 if (!made.ok) throw new Error(String(made.error));
30 return made.value;
31 };
32
33 beforeAll(async () => {
34 dir = await mkdtemp(join(tmpdir(), "forge-agent-lang-"));
35 app = createApp({ dataDir: dir, port: 0 });
36 servers = await serve(app);
37 const root = await person("alice", true);
38 admin = `forge_session=${createSession(app.db, root.id).token}`;
39 const bob = await person("bob", false);
40 member = `forge_session=${createSession(app.db, bob.id).token}`;
41 const made = await createAgent(app, root, {
42 username: "scoutly",
43 model: "claude-sonnet-5-5",
44 brief: "Ships small things.",
45 roles: ["builder"],
46 });
47 if (!made.ok) throw new Error(String(made.error));
48 });
49 afterAll(async () => {
50 servers.stop();
51 app.close();
52 await rm(dir, { recursive: true, force: true });
53 });
54
55 const fetchPage = (who: string) =>
56 fetch(servers.url + "/settings/crew/scoutly", { headers: { cookie: who }, redirect: "manual" });
57
58 test("an agent's page: one h1, plain sections, both forms, no card", async () => {
59 const res = await fetchPage(admin);
60 expect(res.status).toBe(200);
61 const html = await res.text();
62 const page = html.slice(html.indexOf("<main"), html.indexOf("</main>"));
63 expect(page.match(/<h1[ >]/g)?.length).toBe(1);
64 expect(page).toContain("scoutly");
65 expect(page).not.toContain('class="card');
66 expect(page).not.toContain("card-body");
67 expect(page).toContain('action="/settings/crew/scoutly"');
68 // the delete action is the confirm door, as before: it leads to the POST
69 expect(page).toContain("to=%2Fsettings%2Fcrew%2Fscoutly%2Fdelete");
70 });
71
72 test("a non-admin still gets 404", async () => {
73 expect((await fetchPage(member)).status).toBe(404);
74 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs