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

The studio tells you to add an agent below, and there is nothing below #204

Merged opened this

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

Merged

scout/forge:scout/196-the-studio-tells-you-to-add-an-agent-bel → main 1 commit fork

  • Merged. scout/196-the-studio-tells-you-to-add-an-agent-bel was merged into main as 8e61948.

Review

Sign in to review this change.
2 files changed +165 −5
src/web/studio/crew.ts +25 −5
@@ -52,13 +52,33 @@ function crewCard(ctx: Ctx, repo: Repo, base: string, admin: boolean): Html {
5252 (agent) => !onCrew.has(agent.user.id),
5353 );
5454
55 const forgeAdmin = ctx.auth.user?.isAdmin === true;
5556 const rows =
5657 crew.length === 0
57 ? empty({
58 icon: "bot",
59 title: "Nobody yet",
60 note: "Add an agent below and tell it what you want.",
61 })
58 ? empty(
59 available.length > 0
60 ? {
61 icon: "bot",
62 title: "Nobody yet",
63 note: "Add an agent below and tell it what you want.",
64 }
65 : forgeAdmin
66 ? {
67 icon: "bot",
68 title: "Nobody yet",
69 note: "No agents exist on this forge yet.",
70 action: btn("Add an agent", {
71 href: "/settings/crew",
72 variant: "primary",
73 icon: "plus",
74 }),
75 }
76 : {
77 icon: "bot",
78 title: "Nobody yet",
79 note: "No agents exist on this forge yet. An admin can add one.",
80 },
81 )
6282 : listCard(
6383 crew.map((c) =>
6484 row({
test/studio.test.ts +140
@@ -13,6 +13,7 @@ import { previewEnv, type EnvName } from "../src/apps/data.ts";
1313 import { AppsRuntime, type EnvStatus } from "../src/apps/runtime.ts";
1414 import { getAppByRepo, type AppRecord } from "../src/core/apps.ts";
1515 import { addToCrew, createAgent } from "../src/actions/crew.ts";
16 import { addTeamMember, createOrg, createTeam } from "../src/actions/orgs.ts";
1617 import { linkIssue } from "../src/actions/dependencies.ts";
1718 import {
1819 assignIssue,
@@ -4357,6 +4358,145 @@ describe("a stranger sees the app, not the owner's books", () => {
43574358 });
43584359 });
43594360
4361 describe("the studio's crew panel on a forge with no agents yet", () => {
4362 let dataDir: string;
4363 let app: App;
4364 let servers: Servers;
4365 let base: string;
4366 let ownerCookie: string;
4367 let teamAdminCookie: string;
4368
4369 const signIn = async (username: string): Promise<string> =>
4370 (
4371 (
4372 await fetch(`${base}/signin`, {
4373 method: "POST",
4374 body: new URLSearchParams({
4375 username,
4376 password: "a-good-passphrase",
4377 }),
4378 redirect: "manual",
4379 })
4380 ).headers.get("set-cookie") ?? ""
4381 ).split(";")[0]!;
4382
4383 const get = (path: string, cookie: string) =>
4384 fetch(`${base}${path}`, {
4385 headers: { cookie: `${cookie}; forge_mode=studio` },
4386 redirect: "manual",
4387 });
4388
4389 beforeAll(async () => {
4390 dataDir = await mkdtemp(join(tmpdir(), "forge-studio-no-agents-"));
4391 app = createApp({ dataDir, port: 0 });
4392 servers = await serve(app);
4393 base = servers.url;
4394 const a = await createUser(app.db, {
4395 username: "alice",
4396 email: "[email protected]",
4397 password: "a-good-passphrase",
4398 adminIfFirst: true,
4399 });
4400 if (!a.ok) throw new Error(a.error);
4401 const alice = a.value;
4402 const b = await createUser(app.db, {
4403 username: "bob",
4404 email: "[email protected]",
4405 password: "a-good-passphrase",
4406 });
4407 if (!b.ok) throw new Error(b.error);
4408 ownerCookie = await signIn("alice");
4409 teamAdminCookie = await signIn("bob");
4410
4411 // alice's own app: she is both the repo's admin and the forge's.
4412 const pad = await createRepo(app, alice, {
4413 name: "pad",
4414 visibility: "public",
4415 });
4416 if (!pad.ok) throw new Error(String(pad.error));
4417 const padSeeded = await commitToBranch(app, alice, pad.value, {
4418 message: "Start",
4419 files: [{ path: "README.md", content: "pad\n" }],
4420 });
4421 if (!padSeeded.ok) throw new Error(JSON.stringify(padSeeded.error));
4422 const padEnabled = await enableApp(app, alice, pad.value, { slug: "pad" });
4423 if (!padEnabled.ok) throw new Error(String(padEnabled.error));
4424
4425 // an org app whose team admin, bob, is not a forge admin.
4426 const org = await createOrg(app, alice, "acme");
4427 if (!org.ok) throw new Error(org.error);
4428 const team = await createTeam(app, alice, org.value, "leads", "admin");
4429 if (!team.ok) throw new Error(String(team.error));
4430 const added = await addTeamMember(app, alice, org.value, team.value, "bob");
4431 if (!added.ok) throw new Error(String(added.error));
4432 const vault = await createRepo(app, alice, {
4433 name: "vault",
4434 owner: "acme",
4435 visibility: "public",
4436 });
4437 if (!vault.ok) throw new Error(String(vault.error));
4438 const vaultSeeded = await commitToBranch(app, alice, vault.value, {
4439 message: "Start",
4440 files: [{ path: "README.md", content: "vault\n" }],
4441 });
4442 if (!vaultSeeded.ok) throw new Error(JSON.stringify(vaultSeeded.error));
4443 const vaultEnabled = await enableApp(app, alice, vault.value, {
4444 slug: "vault",
4445 });
4446 if (!vaultEnabled.ok) throw new Error(String(vaultEnabled.error));
4447 });
4448
4449 afterAll(async () => {
4450 servers.stop();
4451 app.close();
4452 await rm(dataDir, { recursive: true, force: true });
4453 });
4454
4455 test("a forge admin sees the crew panel name the door an agent is made through, and reaches it in one click without leaving studio mode", async () => {
4456 const settings = await (
4457 await get("/alice/pad/settings", ownerCookie)
4458 ).text();
4459 expect(settings).toContain("No agents exist on this forge yet.");
4460 expect(settings).not.toContain("Add an agent below");
4461 expect(settings).toContain('href="/settings/crew"');
4462
4463 const doorResponse = await get("/settings/crew", ownerCookie);
4464 expect(doorResponse.status).toBe(200);
4465 const crewPage = await doorResponse.text();
4466 expect(crewPage).toContain("New agent");
4467 expect(crewPage).toContain('action="/settings/crew"');
4468 });
4469
4470 test("a repository admin who is not a forge admin is told who can add an agent, not sent to a page that refuses them", async () => {
4471 const settings = await (
4472 await get("/acme/vault/settings", teamAdminCookie)
4473 ).text();
4474 expect(settings).toContain(
4475 "No agents exist on this forge yet. An admin can add one.",
4476 );
4477 expect(settings).not.toContain('href="/settings/crew"');
4478
4479 const refused = await get("/settings/crew", teamAdminCookie);
4480 expect(refused.status).toBe(404);
4481 });
4482
4483 test("the app page's 'Add someone' leads unbroken to the door an agent is made through, with nothing dev-only in the way", async () => {
4484 const appPage = await (await get("/alice/pad", ownerCookie)).text();
4485 expect(appPage).toContain("Nobody yet");
4486 expect(appPage).toContain('href="/alice/pad/settings">Add someone.');
4487
4488 const settings = await (
4489 await get("/alice/pad/settings", ownerCookie)
4490 ).text();
4491 expect(settings).toContain('href="/settings/crew"');
4492
4493 const door = await get("/settings/crew", ownerCookie);
4494 expect(door.status).toBe(200);
4495 const doorBody = await door.text();
4496 expect(doorBody).toContain("New agent");
4497 });
4498 });
4499
43604500 /**
43614501 * The one function every page words a deployment's speed with. The pages
43624502 * above prove it reaches the studio; these prove what it says at the edges,

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs