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

New app promises a first tell from your sentence and drops it when you pick nobody #316

Merged opened this

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

Merged

scout/forge:scout/282-new-app-promises-a-first-tell-from-your → main 1 commit fork

  • Merged. scout/282-new-app-promises-a-first-tell-from-your was merged into main as b129df4.

Review

Sign in to review this change.
4 files changed +88 −16
src/actions/apps.ts +11 −6
@@ -331,12 +331,17 @@ export async function newApp(
331331 actorId: user.value.id,
332332 payload: { agent: chosen.user.username },
333333 });
334 if (body !== "") {
335 const opened = openIssue(app, user.value, repo.value, {
336 title: `Build ${input.name}`,
337 body,
338 });
339 if (opened.ok) {
334 }
335 // The sentence is the one thing the person wrote: it opens the first
336 // issue whether or not anyone was picked for it, unassigned when not.
337 if (body !== "") {
338 const opened = openIssue(app, user.value, repo.value, {
339 title: `Build ${input.name}`,
340 body,
341 });
342 if (opened.ok) {
343 issue = opened.value;
344 if (chosen) {
340345 const assigned = assignIssue(
341346 app,
342347 user.value,
src/tui/screens/apps.ts +3 −1
@@ -287,7 +287,9 @@ export function newAppFlow(f: Session, ctx: Ctx, parent: Screen): void {
287287 const who = made.value.plan
288288 ? ` · ${made.value.plan.agent} was picked — ${made.value.plan.summary}`
289289 : input.agent === undefined
290 ? ""
290 ? made.value.issue === undefined
291 ? ""
292 : ` · #${made.value.issue.number} is waiting for someone`
291293 : made.value.issue === undefined
292294 ? ` · ${input.agent} is on the crew`
293295 : ` · ${input.agent} is on #${made.value.issue.number}`;
src/web/pages/apps.ts +6 −4
@@ -525,11 +525,13 @@ export async function newAppSubmit(ctx: Ctx): Promise<Response> {
525525 ? `${repo.name} is an app, but nothing is serving apps on this forge.`
526526 : `${repo.name} is starting at ${url}.`;
527527 const second =
528 issue === undefined || issue.assigneeName === null
528 issue === undefined
529529 ? ""
530 : plan
531 ? ` ${plan.agent} was picked for #${issue.number} — ${plan.summary}`
532 : ` #${issue.number} is assigned to ${issue.assigneeName}.`;
530 : issue.assigneeName === null
531 ? ` #${issue.number} is waiting for someone.`
532 : plan
533 ? ` ${plan.agent} was picked for #${issue.number} — ${plan.summary}`
534 : ` #${issue.number} is assigned to ${issue.assigneeName}.`;
533535 // The studio's app page (repoHome, once the record exists) is where this
534536 // moment belongs; dev mode keeps the controls' page.
535537 const target = ctx.mode === "studio" ? repoBase(repo) : `${repoBase(repo)}/app`;
test/apps.test.ts +68 −5
@@ -1351,7 +1351,7 @@ printf '%s\\n' '{"is_error":false,"result":"{\\"summary\\":\\"Build a notes app\
13511351 expect(made.value.issue?.body).toContain("- [ ] Add a form");
13521352 });
13531353
1354 test("a plan naming an agent not on the forge is refused; nothing changes", async () => {
1354 test("a plan naming an agent not on the forge is refused; the sentence still opens unassigned", async () => {
13551355 const made = await newApp(app, alice, {
13561356 name: "notes-b",
13571357 description: "BAD-AGENT: pick someone who does not exist",
@@ -1360,12 +1360,15 @@ printf '%s\\n' '{"is_error":false,"result":"{\\"summary\\":\\"Build a notes app\
13601360 expect(made.ok).toBe(true);
13611361 if (!made.ok) return;
13621362 expect(made.value.plan).toBeUndefined();
1363 expect(made.value.issue).toBeUndefined();
1363 expect(made.value.issue?.assigneeName).toBeNull();
1364 expect(made.value.issue?.body).toBe(
1365 "BAD-AGENT: pick someone who does not exist",
1366 );
13641367 const crew = listCrew(app, alice, made.value.repo);
13651368 expect(crew.ok && crew.value.length).toBe(0);
13661369 });
13671370
1368 test("a plan that times out changes nothing", async () => {
1371 test("a plan that times out still opens the sentence, unassigned", async () => {
13691372 const made = await newApp(app, alice, {
13701373 name: "notes-c",
13711374 description: "HANG-ME: never answer",
@@ -1374,7 +1377,7 @@ printf '%s\\n' '{"is_error":false,"result":"{\\"summary\\":\\"Build a notes app\
13741377 expect(made.ok).toBe(true);
13751378 if (!made.ok) return;
13761379 expect(made.value.plan).toBeUndefined();
1377 expect(made.value.issue).toBeUndefined();
1380 expect(made.value.issue?.assigneeName).toBeNull();
13781381 const crew = listCrew(app, alice, made.value.repo);
13791382 expect(crew.ok && crew.value.length).toBe(0);
13801383 }, 10_000);
@@ -1390,7 +1393,7 @@ printf '%s\\n' '{"is_error":false,"result":"{\\"summary\\":\\"Build a notes app\
13901393 expect(made.ok).toBe(true);
13911394 if (!made.ok) return;
13921395 expect(made.value.plan).toBeUndefined();
1393 expect(made.value.issue).toBeUndefined();
1396 expect(made.value.issue?.assigneeName).toBeNull();
13941397 });
13951398
13961399 test("no claude on PATH behaves exactly as without pickAgent", async () => {
@@ -1405,10 +1408,70 @@ printf '%s\\n' '{"is_error":false,"result":"{\\"summary\\":\\"Build a notes app\
14051408 expect(made.ok).toBe(true);
14061409 if (!made.ok) return;
14071410 expect(made.value.plan).toBeUndefined();
1411 expect(made.value.issue?.assigneeName).toBeNull();
1412 });
1413
1414 test("agent: \"\" — nobody picked — still opens the sentence, unassigned", async () => {
1415 const made = await newApp(app, alice, {
1416 name: "notes-f",
1417 description: "Keep a list of notes",
1418 agent: "",
1419 });
1420 expect(made.ok).toBe(true);
1421 if (!made.ok) return;
1422 expect(made.value.issue?.title).toBe("Build notes-f");
1423 expect(made.value.issue?.body).toBe("Keep a list of notes");
1424 expect(made.value.issue?.assigneeName).toBeNull();
1425 expect(made.value.issue?.state).toBe("open");
1426 const crew = listCrew(app, alice, made.value.repo);
1427 expect(crew.ok && crew.value.length).toBe(0);
1428 });
1429
1430 test("an empty sentence still opens nothing", async () => {
1431 const made = await newApp(app, alice, { name: "notes-g" });
1432 expect(made.ok).toBe(true);
1433 if (!made.ok) return;
14081434 expect(made.value.issue).toBeUndefined();
14091435 });
14101436 });
14111437
1438 describe("New app on a forge with no agents at all: the sentence still opens", () => {
1439 let dataDir: string;
1440 let app: App;
1441 let alice: User;
1442
1443 beforeAll(async () => {
1444 dataDir = await mkdtemp(join(tmpdir(), "forge-noagents-"));
1445 app = createApp({ dataDir, port: 0 });
1446 const created = await createUser(app.db, {
1447 username: "alice",
1448 email: "[email protected]",
1449 password: "a-good-passphrase",
1450 adminIfFirst: true,
1451 });
1452 if (!created.ok) throw new Error(created.error);
1453 alice = created.value;
1454 });
1455
1456 afterAll(async () => {
1457 app.close();
1458 await rm(dataDir, { recursive: true, force: true });
1459 });
1460
1461 test("the sentence opens the first issue, unassigned", async () => {
1462 const made = await newApp(app, alice, {
1463 name: "solo",
1464 description: "Keep a list of notes",
1465 });
1466 expect(made.ok).toBe(true);
1467 if (!made.ok) return;
1468 expect(made.value.issue?.title).toBe("Build solo");
1469 expect(made.value.issue?.body).toBe("Keep a list of notes");
1470 expect(made.value.issue?.assigneeName).toBeNull();
1471 expect(made.value.issue?.state).toBe("open");
1472 });
1473 });
1474
14121475 describe("New app: the form asks for the sentence, and names it (issue #60)", () => {
14131476 let dataDir: string;
14141477 let app: App;

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs