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

test/parity.test.ts and test/public-url.test.ts sign up their own accounts and run in the gate #11094

Merged opened this

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

Merged

scout/forge:scout/11082-test-parity-test-ts-and-test-public-url → main 2 commits fork

  • Merged. scout/11082-test-parity-test-ts-and-test-public-url was merged into main as 6655cbb.

Review

Sign in to review this change.
3 files changed +105 −60
scripts/gate.ts −10
@@ -54,11 +54,6 @@ export const EXCLUDED: readonly Exclusion[] = [
5454 reason: "not yet admitted",
5555 issue: 261,
5656 },
57 {
58 file: "test/parity.test.ts",
59 reason: "red alone under --randomize (2026-11-10): a pull reopened, team listings and profile edits depend on earlier tests' state",
60 issue: 11049,
61 },
6257 {
6358 file: "test/probe.test.ts",
6459 reason: "red alone under --randomize (2026-11-10): a scaffolded app obeys every cell: verdict mismatch, not ok",
@@ -70,11 +65,6 @@ export const EXCLUDED: readonly Exclusion[] = [
7065 issue: 11049,
7166 },
7267 { file: "test/prompts.test.ts", reason: "not yet admitted", issue: 261 },
73 {
74 file: "test/public-url.test.ts",
75 reason: "red alone under --randomize (2026-11-10): clone URLs name the public address: create answers 401 when run before signup",
76 issue: 11049,
77 },
7868 {
7969 file: "test/rehearsal-real.test.ts",
8070 reason: "red on main@accaa65 under the gate: the second real roll's old forge does not come up",
test/parity.test.ts +91 −42
@@ -184,6 +184,7 @@ describe("parity: full control through actions and the API", () => {
184184 expect(profile.json.email).toBeUndefined();
185185 expect(typeof profile.json.createdAt).toBe("string");
186186 expect((await call("GET", "/api/v1/users/nobody")).status).toBe(404);
187 const updatedBefore = kinds().filter((k) => k === "user.updated").length;
187188
188189 // An rw token is not the account; an admin token or the session is.
189190 expect(
@@ -219,7 +220,7 @@ describe("parity: full control through actions and the API", () => {
219220 })
220221 ).json.error,
221222 ).toBe("invalid_email");
222 expect(kinds().filter((k) => k === "user.updated")).toHaveLength(2);
223 expect(kinds().filter((k) => k === "user.updated")).toHaveLength(updatedBefore + 2);
223224 });
224225
225226 test("changing the password needs the current one and never an rw token", async () => {
@@ -368,10 +369,21 @@ describe("parity: full control through actions and the API", () => {
368369 // ── labels ───────────────────────────────────────────────────────────
369370
370371 test("labels: admin-managed, writer-assigned, visible to readers, filterable", async () => {
372 // A repository of its own, so no other test's labels or issues show here.
373 // Alice may have changed her email in another test: taken as proven.
374 proveAll(app);
375 expect(
376 (
377 await call("POST", "/api/v1/repos", {
378 token: aliceRw,
379 body: { name: "labeled", visibility: "public" },
380 })
381 ).status,
382 ).toBe(201);
371383 // Creating a label is repo administration.
372384 expect(
373385 (
374 await call("POST", "/api/v1/repos/alice/pub/labels", {
386 await call("POST", "/api/v1/repos/alice/labeled/labels", {
375387 token: aliceRw,
376388 body: { name: "bug", color: "ff0000" },
377389 })
@@ -379,33 +391,33 @@ describe("parity: full control through actions and the API", () => {
379391 ).toBe(403);
380392 expect(
381393 (
382 await call("POST", "/api/v1/repos/alice/pub/labels", {
394 await call("POST", "/api/v1/repos/alice/labeled/labels", {
383395 token: bobRw,
384396 body: { name: "bug", color: "ff0000" },
385397 })
386398 ).status,
387399 ).toBe(403);
388 const bug = await call("POST", "/api/v1/repos/alice/pub/labels", {
400 const bug = await call("POST", "/api/v1/repos/alice/labeled/labels", {
389401 token: aliceAdmin,
390402 body: { name: "bug", color: "ff0000" },
391403 });
392404 expect(bug.status).toBe(201);
393 const queue = await call("POST", "/api/v1/repos/alice/pub/labels", {
405 const queue = await call("POST", "/api/v1/repos/alice/labeled/labels", {
394406 token: aliceAdmin,
395407 body: { name: "queue", color: "00ff00" },
396408 });
397409 expect(queue.status).toBe(201);
398410 expect(
399 ((await call("GET", "/api/v1/repos/alice/pub/labels")).json.items as Json[]).map(
411 ((await call("GET", "/api/v1/repos/alice/labeled/labels")).json.items as Json[]).map(
400412 (l) => l.name,
401413 ),
402414 ).toEqual(["bug", "queue"]);
403415
404 const one = await call("POST", "/api/v1/repos/alice/pub/issues", {
416 const one = await call("POST", "/api/v1/repos/alice/labeled/issues", {
405417 token: bobRw,
406418 body: { title: "first issue" },
407419 });
408 const two = await call("POST", "/api/v1/repos/alice/pub/issues", {
420 const two = await call("POST", "/api/v1/repos/alice/labeled/issues", {
409421 token: aliceRw,
410422 body: { title: "second issue" },
411423 });
@@ -416,7 +428,7 @@ describe("parity: full control through actions and the API", () => {
416428 // Bob can read (and even authored #1) but is not a writer: forbidden.
417429 expect(
418430 (
419 await call("PUT", `/api/v1/repos/alice/pub/issues/${n1}/labels`, {
431 await call("PUT", `/api/v1/repos/alice/labeled/issues/${n1}/labels`, {
420432 token: bobRw,
421433 body: { labels: ["bug"] },
422434 })
@@ -425,7 +437,7 @@ describe("parity: full control through actions and the API", () => {
425437 // A read-only token fails the scope floor before anything else.
426438 expect(
427439 (
428 await call("PUT", `/api/v1/repos/alice/pub/issues/${n1}/labels`, {
440 await call("PUT", `/api/v1/repos/alice/labeled/issues/${n1}/labels`, {
429441 token: aliceRo,
430442 body: { labels: ["bug"] },
431443 })
@@ -434,7 +446,7 @@ describe("parity: full control through actions and the API", () => {
434446 // Unknown labels reject the whole set.
435447 const unknown = await call(
436448 "PUT",
437 `/api/v1/repos/alice/pub/issues/${n1}/labels`,
449 `/api/v1/repos/alice/labeled/issues/${n1}/labels`,
438450 {
439451 token: aliceRw,
440452 body: { labels: ["bug", "ghost"] },
@@ -443,13 +455,13 @@ describe("parity: full control through actions and the API", () => {
443455 expect(unknown.status).toBe(422);
444456 expect(unknown.json.error).toBe("unknown_label");
445457 expect(
446 (await call("GET", `/api/v1/repos/alice/pub/issues/${n1}`)).json.labels,
458 (await call("GET", `/api/v1/repos/alice/labeled/issues/${n1}`)).json.labels,
447459 ).toEqual([]);
448460
449461 // By name and by id, case-insensitive, de-duplicated.
450462 const set = await call(
451463 "PUT",
452 `/api/v1/repos/alice/pub/issues/${n1}/labels`,
464 `/api/v1/repos/alice/labeled/issues/${n1}/labels`,
453465 {
454466 token: aliceRw,
455467 body: { labels: ["BUG", bug.json.id, "queue"] },
@@ -457,16 +469,16 @@ describe("parity: full control through actions and the API", () => {
457469 );
458470 expect(set.status).toBe(200);
459471 expect(set.list.map((l) => l.name)).toEqual(["bug", "queue"]);
460 await call("PUT", `/api/v1/repos/alice/pub/issues/${n2}/labels`, {
472 await call("PUT", `/api/v1/repos/alice/labeled/issues/${n2}/labels`, {
461473 token: aliceRw,
462474 body: { labels: ["queue"] },
463475 });
464476
465 const bugs = await call("GET", "/api/v1/repos/alice/pub/issues?label=bug");
477 const bugs = await call("GET", "/api/v1/repos/alice/labeled/issues?label=bug");
466478 expect((bugs.json.items as Json[]).map((i) => i.number)).toEqual([n1]);
467479 const queued = await call(
468480 "GET",
469 "/api/v1/repos/alice/pub/issues?label=queue",
481 "/api/v1/repos/alice/labeled/issues?label=queue",
470482 );
471483 expect((queued.json.items as Json[]).map((i) => i.number)).toEqual([
472484 n2,
@@ -477,7 +489,7 @@ describe("parity: full control through actions and the API", () => {
477489 ).toEqual(["queue"]);
478490 const byBob = await call(
479491 "GET",
480 "/api/v1/repos/alice/pub/issues?author=bob",
492 "/api/v1/repos/alice/labeled/issues?author=bob",
481493 );
482494 expect((byBob.json.items as Json[]).map((i) => i.number)).toEqual([n1]);
483495
@@ -486,7 +498,7 @@ describe("parity: full control through actions and the API", () => {
486498 (
487499 await call(
488500 "DELETE",
489 `/api/v1/repos/alice/pub/labels/${queue.json.id}`,
501 `/api/v1/repos/alice/labeled/labels/${queue.json.id}`,
490502 {
491503 token: aliceAdmin,
492504 },
@@ -495,7 +507,7 @@ describe("parity: full control through actions and the API", () => {
495507 ).toBe(204);
496508 expect(
497509 (
498 (await call("GET", `/api/v1/repos/alice/pub/issues/${n2}`)).json
510 (await call("GET", `/api/v1/repos/alice/labeled/issues/${n2}`)).json
499511 .labels as Json[]
500512 ).length,
501513 ).toBe(0);
@@ -513,6 +525,14 @@ describe("parity: full control through actions and the API", () => {
513525 });
514526 expect(pr.status).toBe(201);
515527 const n = pr.json.number as number;
528 expect(
529 (
530 await call("POST", "/api/v1/repos/alice/pub/labels", {
531 token: aliceAdmin,
532 body: { name: "bug", color: "ff0000" },
533 })
534 ).status,
535 ).toBe(201);
516536
517537 // The pull number works on the labels endpoint (one table).
518538 const labeled = await call(
@@ -855,11 +875,13 @@ describe("parity: full control through actions and the API", () => {
855875
856876 const myOrgs = await call("GET", "/api/v1/user/orgs", { token: aliceRo });
857877 const orgItems = (r: { json: Json }) => (r.json.items ?? []) as Json[];
858 expect(orgItems(myOrgs).map((o) => o.username)).toEqual(["acme"]);
878 expect(orgItems(myOrgs).map((o) => o.username)).toContain("acme");
859879 expect(myOrgs.json.next_cursor).toBeNull();
860880 expect(
861 orgItems(await call("GET", "/api/v1/user/orgs", { token: bobRw })),
862 ).toEqual([]);
881 orgItems(await call("GET", "/api/v1/user/orgs", { token: bobRw })).map(
882 (o) => o.username,
883 ),
884 ).not.toContain("acme");
863885 for (const kind of [
864886 "team.created",
865887 "team.deleted",
@@ -870,49 +892,63 @@ describe("parity: full control through actions and the API", () => {
870892 });
871893
872894 test("teams and members page as { items, next_cursor }", async () => {
873 await call("POST", "/api/v1/orgs/acme/teams", {
895 // An org of its own: Owners, devs and zeta, nothing another test made.
896 proveAll(app);
897 expect(
898 (await call("POST", "/api/v1/orgs", { token: aliceRw, body: { name: "paging" } }))
899 .status,
900 ).toBe(201);
901 expect(
902 (
903 await call("POST", "/api/v1/orgs/paging/teams", {
904 token: aliceAdmin,
905 body: { name: "devs", permission: "write" },
906 })
907 ).status,
908 ).toBe(201);
909 await call("POST", "/api/v1/orgs/paging/teams", {
874910 token: aliceAdmin,
875911 body: { name: "zeta", permission: "read" },
876912 });
877 const all = await call("GET", "/api/v1/orgs/acme/teams", { token: aliceAdmin });
913 const all = await call("GET", "/api/v1/orgs/paging/teams", { token: aliceAdmin });
878914 const names = (all.json.items as Json[]).map((t) => t.name as string);
879915 expect(names.length).toBeGreaterThanOrEqual(3);
880916 expect(all.json.next_cursor).toBeNull();
881 const first = await call("GET", "/api/v1/orgs/acme/teams?limit=2", { token: aliceAdmin });
917 const first = await call("GET", "/api/v1/orgs/paging/teams?limit=2", { token: aliceAdmin });
882918 expect((first.json.items as Json[]).map((t) => t.name)).toEqual(names.slice(0, 2));
883919 expect(first.json.next_cursor).toBe(names[1]!);
884920 const second = await call(
885921 "GET",
886 `/api/v1/orgs/acme/teams?limit=200&cursor=${names[1]}`,
922 `/api/v1/orgs/paging/teams?limit=200&cursor=${names[1]}`,
887923 { token: aliceAdmin },
888924 );
889925 expect((second.json.items as Json[]).map((t) => t.name)).toEqual(names.slice(2));
890926 expect(second.json.next_cursor).toBeNull();
891927
892928 for (const user of ["bob", "carol"])
893 await call("POST", "/api/v1/orgs/acme/teams/devs/members", {
929 await call("POST", "/api/v1/orgs/paging/teams/devs/members", {
894930 token: aliceAdmin,
895931 body: { user },
896932 });
897 const m1 = await call("GET", "/api/v1/orgs/acme/teams/devs/members?limit=1", {
933 const m1 = await call("GET", "/api/v1/orgs/paging/teams/devs/members?limit=1", {
898934 token: aliceAdmin,
899935 });
900936 expect((m1.json.items as Json[]).map((m) => m.username)).toEqual(["bob"]);
901937 expect(m1.json.next_cursor).toBe("bob");
902 const m2 = await call("GET", "/api/v1/orgs/acme/teams/devs/members?limit=1&cursor=bob", {
938 const m2 = await call("GET", "/api/v1/orgs/paging/teams/devs/members?limit=1&cursor=bob", {
903939 token: aliceAdmin,
904940 });
905941 expect((m2.json.items as Json[]).map((m) => m.username)).toEqual(["carol"]);
906942 expect(m2.json.next_cursor).toBeNull();
907943
908944 // A member of a private team is not shown to an outsider.
909 const outsider = await call("GET", "/api/v1/orgs/acme/teams/zeta/members", { token: bobRw });
945 const outsider = await call("GET", "/api/v1/orgs/paging/teams/zeta/members", { token: bobRw });
910946 expect(outsider.status).toBe(404);
911947 expect(outsider.json.items).toBeUndefined();
912948
913949 for (const path of ["teams", "teams/devs/members"])
914950 for (const bad of ["0", "201", "-1", "abc", "1.5"]) {
915 const res = await call("GET", `/api/v1/orgs/acme/${path}?limit=${bad}`, {
951 const res = await call("GET", `/api/v1/orgs/paging/${path}?limit=${bad}`, {
916952 token: aliceAdmin,
917953 });
918954 expect(res.status).toBe(422);
@@ -922,7 +958,7 @@ describe("parity: full control through actions and the API", () => {
922958 // The cursor is only compared: any 1..160 characters, nothing else.
923959 for (const path of ["teams", "teams/devs/members"])
924960 for (const bad of ["", "x".repeat(161)]) {
925 const res = await call("GET", `/api/v1/orgs/acme/${path}?cursor=${bad}`, {
961 const res = await call("GET", `/api/v1/orgs/paging/${path}?cursor=${bad}`, {
926962 token: aliceAdmin,
927963 });
928964 expect(res.status).toBe(422);
@@ -930,21 +966,21 @@ describe("parity: full control through actions and the API", () => {
930966 }
931967
932968 // A name with a control character pages on: its own cursor is accepted.
933 const odd = await call("POST", "/api/v1/orgs/acme/teams", {
969 const odd = await call("POST", "/api/v1/orgs/paging/teams", {
934970 token: aliceAdmin,
935971 body: { name: "a\u0001b", permission: "read" },
936972 });
937973 expect(odd.status).toBe(201);
938974 {
939 const list = await call("GET", "/api/v1/orgs/acme/teams", { token: aliceAdmin });
975 const list = await call("GET", "/api/v1/orgs/paging/teams", { token: aliceAdmin });
940976 const at = (list.json.items as Json[]).findIndex((t) => t.name === "a\u0001b");
941 const page = await call("GET", `/api/v1/orgs/acme/teams?limit=${at + 1}`, {
977 const page = await call("GET", `/api/v1/orgs/paging/teams?limit=${at + 1}`, {
942978 token: aliceAdmin,
943979 });
944980 expect(page.json.next_cursor).toBe("a\u0001b");
945981 const next = await call(
946982 "GET",
947 `/api/v1/orgs/acme/teams?cursor=${encodeURIComponent("a\u0001b")}`,
983 `/api/v1/orgs/paging/teams?cursor=${encodeURIComponent("a\u0001b")}`,
948984 { token: aliceAdmin },
949985 );
950986 expect(next.status).toBe(200);
@@ -952,21 +988,34 @@ describe("parity: full control through actions and the API", () => {
952988
953989 // The default page is 50.
954990 for (let i = 0; i < 50; i++)
955 await call("POST", "/api/v1/orgs/acme/teams", {
991 await call("POST", "/api/v1/orgs/paging/teams", {
956992 token: aliceAdmin,
957993 body: { name: `p${String(i).padStart(2, "0")}`, permission: "read" },
958994 });
959 const dflt = await call("GET", "/api/v1/orgs/acme/teams", { token: aliceAdmin });
995 const dflt = await call("GET", "/api/v1/orgs/paging/teams", { token: aliceAdmin });
960996 expect((dflt.json.items as Json[]).length).toBe(50);
961997 expect(dflt.json.next_cursor).not.toBeNull();
962998 });
963999
9641000 test("a team member sees the org's private repos in listings", async () => {
1001 proveAll(app);
1002 expect(
1003 (await call("POST", "/api/v1/orgs", { token: aliceRw, body: { name: "corp" } }))
1004 .status,
1005 ).toBe(201);
1006 expect(
1007 (
1008 await call("POST", "/api/v1/orgs/corp/teams", {
1009 token: aliceAdmin,
1010 body: { name: "devs", permission: "write" },
1011 })
1012 ).status,
1013 ).toBe(201);
9651014 await call("POST", "/api/v1/repos", {
9661015 token: aliceRw,
967 body: { owner: "acme", name: "internal", visibility: "private" },
1016 body: { owner: "corp", name: "internal", visibility: "private" },
9681017 });
969 await call("POST", "/api/v1/orgs/acme/teams/devs/members", {
1018 await call("POST", "/api/v1/orgs/corp/teams/devs/members", {
9701019 token: aliceAdmin,
9711020 body: { user: "bob" },
9721021 });
@@ -974,9 +1023,9 @@ describe("parity: full control through actions and the API", () => {
9741023 const names = (bobSees.json.items as Json[]).map(
9751024 (r) => `${r.ownerName}/${r.name}`,
9761025 );
977 expect(names).toContain("acme/internal");
1026 expect(names).toContain("corp/internal");
9781027 expect(names).not.toContain("alice/secret");
979 const carolSees = await call("GET", "/api/v1/users/acme/repos");
1028 const carolSees = await call("GET", "/api/v1/users/corp/repos");
9801029 expect((carolSees.json.items as Json[]).length).toBe(0);
9811030 });
9821031
test/public-url.test.ts +14 −8
@@ -6,7 +6,7 @@ import { createApp, type App } from "../src/app.ts";
66 import { crossOrigin, publicUrl } from "../src/http/context.ts";
77 import { serve, type Servers } from "../src/http/server.ts";
88 import { promote } from "./helpers/members.ts";
9 import { proveAll } from "./helpers/verified.ts";
9 import { signUpProven } from "./helpers/verified.ts";
1010
1111 describe("publicUrl", () => {
1212 test("without a configured origin the request's URL is the truth", () => {
@@ -89,14 +89,20 @@ describe("a forge behind a TLS-terminating proxy", () => {
8989 });
9090
9191 test("clone URLs name the public address", async () => {
92 // Ada signed up above: an owner proves and promotes her before she makes things.
93 proveAll(app);
92 // Grace signs up here, so this test does not lean on the one above:
93 // an owner proves and promotes her before she makes things.
94 const grace = await signUpProven(app, {
95 username: "grace",
96 email: "[email protected]",
97 password: "compiler-pioneer",
98 });
99 if (!grace.ok) throw new Error("grace could not sign up");
94100 promote(app.db);
95101 const token = await fetch(`${base}/api/v1/tokens`, {
96102 method: "POST",
97103 body: JSON.stringify({ adminConfirm: true,
98 username: "ada",
99 password: "analytical-engine",
104 username: "grace",
105 password: "compiler-pioneer",
100106 name: "cli",
101107 scope: "rw",
102108 }),
@@ -109,8 +115,8 @@ describe("a forge behind a TLS-terminating proxy", () => {
109115 body: JSON.stringify({ name: "hello", visibility: "public" }),
110116 });
111117 expect(created.status).toBe(201);
112 const page = await fetch(`${base}/ada/hello`).then((r) => r.text());
113 expect(page).toContain("https://forge.example/ada/hello.git");
114 expect(page).not.toContain(`${base}/ada/hello.git`);
118 const page = await fetch(`${base}/grace/hello`).then((r) => r.text());
119 expect(page).toContain("https://forge.example/grace/hello.git");
120 expect(page).not.toContain(`${base}/grace/hello.git`);
115121 });
116122 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs