Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge branch 'worktree-agent-a1b995daa94e4e1b7' | 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; | |
| 3 | ||
| 4 | import { | |
| Workspace is the workspace's settings, in one place. Its sidebar is grouped, General, Access, Money, Compute, Code, Agents, Chat, Artifacts, Security and Integrations, every page one click away with no settings inside settings, the groups folding and the owner-only pages hidden from members; what is not here yet is marked Soon with a hint. Members is a list with search and filters for role, two-factor and team, and Invite opens a dialog: who, by username, name or email, their role and a note that goes into the invitation; nothing is filled in inline any more. Invitations is its own page with All, Pending, Accepted, Declined, Expired and Revoked filters that say how many, and each row's menu can copy the link, send it again or revoke it; identity learned to send an invitation again and to carry the note. Permissions gathers every rule about who may do what, by part: Code's base permission and member privileges, who creates teams, who creates channels, and, marked Soon with what applies today, forking private repositories, adding agents to conversations, messaging agents directly, creating spaces and default sharing, creating workspace agents and raising budgets, deploying to production and publishing packages. General lost what moved. The permissions guide is new, and the workspaces, people and teams, access, chat, teams, authentication and billing guides say where things are now. | 5 | filterInvitations, |
| 6 | filterMembers, | |
| 7 | invitationCounts, | |
| 8 | invitationFilter, | |
| 9 | invitationFilterOf, | |
| 10 | invitationTone, | |
| Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page) | 11 | inviteKind, |
| Workspace is the workspace's settings, in one place. Its sidebar is grouped, General, Access, Money, Compute, Code, Agents, Chat, Artifacts, Security and Integrations, every page one click away with no settings inside settings, the groups folding and the owner-only pages hidden from members; what is not here yet is marked Soon with a hint. Members is a list with search and filters for role, two-factor and team, and Invite opens a dialog: who, by username, name or email, their role and a note that goes into the invitation; nothing is filled in inline any more. Invitations is its own page with All, Pending, Accepted, Declined, Expired and Revoked filters that say how many, and each row's menu can copy the link, send it again or revoke it; identity learned to send an invitation again and to carry the note. Permissions gathers every rule about who may do what, by part: Code's base permission and member privileges, who creates teams, who creates channels, and, marked Soon with what applies today, forking private repositories, adding agents to conversations, messaging agents directly, creating spaces and default sharing, creating workspace agents and raising budgets, deploying to production and publishing packages. General lost what moved. The permissions guide is new, and the workspaces, people and teams, access, chat, teams, authentication and billing guides say where things are now. | 12 | memberFilters, |
| Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page) | 13 | invitePageCopy, |
| 14 | workspaceInviteCopy, | |
| Merge branch 'worktree-agent-a1b995daa94e4e1b7' | 15 | cleanCode, |
| Merge invite emails that confirm the address: the emailed link carries a proof only that email has, so signing up from it needs no code; shared links and typed codes still confirm | 16 | cleanProof, |
| 17 | invitePath, | |
| 18 | inviteSignUpCopy, | |
| Merge branch 'worktree-agent-a1b995daa94e4e1b7' | 19 | inviteFor, |
| 20 | inviteLink, | |
| 21 | inviteState, | |
| 22 | landingFor, | |
| 23 | looksAutomated, | |
| 24 | signUpCopy, | |
| 25 | suggestUsername, | |
| 26 | welcomeCookie, | |
| 27 | clearWelcome, | |
| 28 | welcomes, | |
| 29 | } from "./invites.ts"; | |
| 30 | ||
| 31 | const CODE = "g1t-k7m2-q9xd-4hpw-abcd-0123-4567-89ef-ghjk"; | |
| 32 | ||
| Workspace is the workspace's settings, in one place. Its sidebar is grouped, General, Access, Money, Compute, Code, Agents, Chat, Artifacts, Security and Integrations, every page one click away with no settings inside settings, the groups folding and the owner-only pages hidden from members; what is not here yet is marked Soon with a hint. Members is a list with search and filters for role, two-factor and team, and Invite opens a dialog: who, by username, name or email, their role and a note that goes into the invitation; nothing is filled in inline any more. Invitations is its own page with All, Pending, Accepted, Declined, Expired and Revoked filters that say how many, and each row's menu can copy the link, send it again or revoke it; identity learned to send an invitation again and to carry the note. Permissions gathers every rule about who may do what, by part: Code's base permission and member privileges, who creates teams, who creates channels, and, marked Soon with what applies today, forking private repositories, adding agents to conversations, messaging agents directly, creating spaces and default sharing, creating workspace agents and raising budgets, deploying to production and publishing packages. General lost what moved. The permissions guide is new, and the workspaces, people and teams, access, chat, teams, authentication and billing guides say where things are now. | 33 | test("the Invitations page sorts every state under one filter, and counts them", () => { |
| 34 | const statuses = ["pending", "awaiting_confirmation", "awaiting_answer", "redeemed", "declined", "expired", "revoked"] as const; | |
| 35 | const invites = statuses.map((status) => ({ status })); | |
| 36 | assert.equal(invitationFilterOf("pending"), "pending"); | |
| 37 | assert.equal(invitationFilterOf("awaiting_confirmation"), "pending"); | |
| 38 | assert.equal(invitationFilterOf("awaiting_answer"), "pending"); | |
| 39 | assert.equal(invitationFilterOf("redeemed"), "accepted"); | |
| 40 | assert.equal(invitationFilterOf("declined"), "declined"); | |
| 41 | assert.equal(invitationFilterOf("expired"), "expired"); | |
| 42 | assert.equal(invitationFilterOf("revoked"), "revoked"); | |
| 43 | assert.deepEqual(invitationCounts(invites), { all: 7, pending: 3, accepted: 1, declined: 1, expired: 1, revoked: 1 }); | |
| 44 | assert.equal(filterInvitations(invites, "all").length, 7); | |
| 45 | assert.deepEqual(filterInvitations(invites, "pending").map((invite) => invite.status), ["pending", "awaiting_confirmation", "awaiting_answer"]); | |
| 46 | assert.deepEqual(filterInvitations(invites, "accepted").map((invite) => invite.status), ["redeemed"]); | |
| 47 | // The query names a filter, or means All. | |
| 48 | assert.equal(invitationFilter("pending"), "pending"); | |
| 49 | assert.equal(invitationFilter("failed"), "all"); | |
| 50 | assert.equal(invitationFilter(null), "all"); | |
| 51 | // Waiting is amber, joined green, a no red, and over is quiet. | |
| 52 | assert.equal(invitationTone("awaiting_answer"), "warn"); | |
| 53 | assert.equal(invitationTone("redeemed"), "success"); | |
| 54 | assert.equal(invitationTone("revoked"), "danger"); | |
| 55 | assert.equal(invitationTone("expired"), "neutral"); | |
| 56 | }); | |
| 57 | ||
| 58 | test("the Members page filters by role, two-factor, team and words", () => { | |
| 59 | const members = [ | |
| 60 | { username: "ada", name: "Ada Lovelace", role: "owner" as const, two_factor: true }, | |
| 61 | { username: "grace", name: "Grace Hopper", role: "member" as const, two_factor: false }, | |
| 62 | { username: "linus", name: null, role: "member" as const, two_factor: true }, | |
| 63 | ]; | |
| 64 | const teams = { ada: [{ slug: "core" }], grace: [{ slug: "web" }, { slug: "core" }] }; | |
| 65 | assert.deepEqual(memberFilters(""), { role: "all", twoFactor: "all", team: null, query: "" }); | |
| 66 | assert.deepEqual(memberFilters("?role=owner&2fa=off&team=Core&q=%20ada%20"), { role: "owner", twoFactor: "off", team: "core", query: "ada" }); | |
| 67 | assert.deepEqual(memberFilters("?role=admin&2fa=maybe"), { role: "all", twoFactor: "all", team: null, query: "" }); | |
| 68 | const names = (filters: string) => filterMembers(members, memberFilters(filters), teams).map((member) => member.username); | |
| 69 | assert.deepEqual(names(""), ["ada", "grace", "linus"]); | |
| 70 | assert.deepEqual(names("?role=member"), ["grace", "linus"]); | |
| 71 | assert.deepEqual(names("?2fa=off"), ["grace"]); | |
| 72 | assert.deepEqual(names("?team=core"), ["ada", "grace"]); | |
| 73 | assert.deepEqual(names("?q=hopper"), ["grace"]); | |
| 74 | assert.deepEqual(names("?q=@LIN"), ["linus"]); | |
| 75 | assert.deepEqual(names("?role=owner&team=web"), []); | |
| 76 | }); | |
| 77 | ||
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 78 | test("everyone is offered a plain sign-up", () => { |
| Sign-up buttons say Sign up everywhere; only the sign-up page says registration takes an invite | 79 | assert.deepEqual(signUpCopy(), { primary: "Sign up", secondary: null }); |
| Merge branch 'worktree-agent-a1b995daa94e4e1b7' | 80 | }); |
| 81 | ||
| 82 | test("an invite link is on g1t.sh unless told otherwise", () => { | |
| 83 | assert.equal(inviteLink(CODE), `https://g1t.sh/invite/${CODE}`); | |
| 84 | assert.equal(inviteLink(CODE, "http://localhost:8787/"), `http://localhost:8787/invite/${CODE}`); | |
| 85 | }); | |
| 86 | ||
| Merge invite emails that confirm the address: the emailed link carries a proof only that email has, so signing up from it needs no code; shared links and typed codes still confirm | 87 | const PROOF = "4f9c2a7e0b13d5c84f9c2a7e0b13d5c84f9c2a7e0b13d5c84f9c2a7e0b13d5c8"; |
| 88 | ||
| 89 | test("an invite email's proof is kept only when it looks like one, and goes along to the invite's page", () => { | |
| 90 | assert.equal(cleanProof(PROOF), PROOF); | |
| 91 | assert.equal(cleanProof(` ${PROOF.toUpperCase()} `), PROOF); | |
| 92 | assert.equal(cleanProof("not-a-proof"), null); | |
| 93 | assert.equal(cleanProof("abc"), null); | |
| 94 | assert.equal(cleanProof("a".repeat(500)), null); | |
| 95 | assert.equal(cleanProof(null), null); | |
| 96 | assert.equal(invitePath(CODE, PROOF), `/invite/${CODE}?proof=${PROOF}`); | |
| 97 | assert.equal(invitePath(CODE, null), `/invite/${CODE}`); | |
| 98 | assert.equal(invitePath(CODE), `/invite/${CODE}`); | |
| 99 | }); | |
| 100 | ||
| 101 | test("signing up from the invite email says the address is confirmed already; otherwise the code step applies", () => { | |
| 102 | const base = { address: "ada@example.com", emailProven: false, workspace: { name: "Flagon, Inc." }, repository: null }; | |
| 103 | const proven = inviteSignUpCopy({ ...base, emailProven: true }); | |
| Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040) | 104 | assert.equal(proven.intro, "You can join Flagon, Inc. as soon as you create it: accept the invitation then."); |
| Merge invite emails that confirm the address: the emailed link carries a proof only that email has, so signing up from it needs no code; shared links and typed codes still confirm | 105 | assert.match(proven.confirmed ?? "", /^ada@example\.com is confirmed: you came here from the invite we emailed to it/); |
| 106 | assert.match(proven.hint, /confirmed already/); | |
| 107 | assert.doesNotMatch(proven.hint, /code/); | |
| 108 | ||
| 109 | // No proof (a code typed in, or a link passed on): nothing new is said. | |
| 110 | const plain = inviteSignUpCopy(base); | |
| Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040) | 111 | assert.equal(plain.intro, "You can join Flagon, Inc. as soon as you confirm your email: accept the invitation then."); |
| Merge invite emails that confirm the address: the emailed link carries a proof only that email has, so signing up from it needs no code; shared links and typed codes still confirm | 112 | assert.equal(plain.confirmed, null); |
| 113 | assert.equal(plain.hint, "Your invite was sent here. We email it a code to confirm it before you start."); | |
| 114 | ||
| 115 | // An invite for anyone with the code has no address to prove. | |
| 116 | const open = inviteSignUpCopy({ ...base, address: null, emailProven: true, workspace: null }); | |
| 117 | assert.equal(open.confirmed, null); | |
| 118 | assert.equal(open.intro, "It takes a minute."); | |
| 119 | assert.equal(open.hint, "We email it a code to confirm it before you start."); | |
| 120 | ||
| 121 | const repo = inviteSignUpCopy({ ...base, workspace: null, repository: { name: "flagon-io/g1t" }, emailProven: true }); | |
| 122 | assert.equal(repo.intro, "You get flagon-io/g1t as soon as you create it."); | |
| 123 | }); | |
| 124 | ||
| Merge branch 'worktree-agent-a1b995daa94e4e1b7' | 125 | test("a pasted link or code is tidied to the code", () => { |
| 126 | assert.equal(cleanCode(CODE), CODE); | |
| 127 | assert.equal(cleanCode(` ${CODE} `), CODE); | |
| 128 | assert.equal(cleanCode(`https://g1t.sh/invite/${CODE}`), CODE); | |
| 129 | assert.equal(cleanCode(`https://g1t.sh/register?invite=${CODE}&next=%2F`), CODE); | |
| 130 | assert.equal(cleanCode("g1t-k7m2 q9xd"), "g1t-k7m2q9xd"); | |
| 131 | assert.equal(cleanCode(null), ""); | |
| 132 | assert.equal(cleanCode("x".repeat(500)).length, 80); | |
| 133 | }); | |
| 134 | ||
| 135 | test("each invite says where it stands and whom it is for", () => { | |
| 136 | const base = { redeemedBy: null, email: null, workspace: null }; | |
| 137 | assert.deepEqual(inviteState({ ...base, status: "pending" }), { label: "Pending", tone: "pending" }); | |
| 138 | assert.deepEqual(inviteState({ ...base, status: "redeemed", redeemedBy: "ada" }), { label: "Joined as @ada", tone: "done" }); | |
| Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036) | 139 | assert.deepEqual(inviteState({ ...base, status: "awaiting_confirmation", redeemedBy: "ada" }), { |
| 140 | label: "@ada is confirming their email", | |
| 141 | tone: "pending", | |
| 142 | }); | |
| Merge branch 'worktree-agent-a1b995daa94e4e1b7' | 143 | assert.deepEqual(inviteState({ ...base, status: "expired" }), { label: "Expired", tone: "dead" }); |
| 144 | assert.deepEqual(inviteState({ ...base, status: "revoked" }), { label: "Revoked", tone: "dead" }); | |
| 145 | assert.equal(inviteFor({ ...base, status: "pending" }), "Anyone with the link"); | |
| Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page) | 146 | assert.equal(inviteFor({ ...base, status: "pending", email: "ada@example.com", workspace: "acme" }), "ada@example.com"); |
| 147 | assert.equal(inviteFor({ ...base, status: "pending", invitee: "daweazl", workspace: "flagon-io" }), "@daweazl"); | |
| 148 | // Which kind each is, beside whom it is for. | |
| 149 | assert.deepEqual(inviteKind({ workspace: null }), { kind: "g1t", label: "Invite to g1t" }); | |
| 150 | assert.deepEqual(inviteKind({ workspace: "flagon-io" }), { kind: "workspace", label: "Invite to join flagon-io" }); | |
| Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040) | 151 | assert.deepEqual(inviteState({ ...base, status: "awaiting_answer", redeemedBy: "daweazl" }), { label: "Waiting for @daweazl to accept", tone: "pending" }); |
| 152 | assert.deepEqual(inviteState({ ...base, status: "declined", invitee: "daweazl" }), { label: "@daweazl declined", tone: "dead" }); | |
| Merge branch 'worktree-agent-a1b995daa94e4e1b7' | 153 | }); |
| 154 | ||
| 155 | test("bots that fill the hidden field or answer instantly are turned away", () => { | |
| 156 | const form = (fields: Record<string, string>) => ({ get: (name: string) => fields[name] ?? null }); | |
| 157 | const now = 1_000_000; | |
| 158 | assert.equal(looksAutomated(form({ website: "http://spam.example" }), now), true); | |
| 159 | assert.equal(looksAutomated(form({ started: String(now - 200) }), now), true); | |
| 160 | assert.equal(looksAutomated(form({ started: String(now - 10_000) }), now), false); | |
| 161 | assert.equal(looksAutomated(form({}), now), false); | |
| 162 | assert.equal(looksAutomated(form({ website: " " }), now), false); | |
| 163 | }); | |
| 164 | ||
| 165 | test("a username is suggested from the invited address", () => { | |
| 166 | assert.equal(suggestUsername("ada.lovelace@example.com"), "ada-lovelace"); | |
| 167 | assert.equal(suggestUsername("Margaret_Hamilton+g1t@example.com"), "margaret-hamilton"); | |
| 168 | assert.equal(suggestUsername("--x--@example.com"), "x"); | |
| 169 | assert.equal(suggestUsername(`${"a".repeat(38)}.b@example.com`), "a".repeat(38)); | |
| 170 | assert.equal(suggestUsername("...@example.com"), ""); | |
| 171 | assert.equal(suggestUsername(null), ""); | |
| 172 | }); | |
| 173 | ||
| 174 | test("an invite lands in its workspace, else its repository", () => { | |
| 175 | assert.equal(landingFor({ workspace: { slug: "Flagon-IO" }, repository: null }), "flagon-io"); | |
| 176 | assert.equal(landingFor({ workspace: null, repository: { name: "flagon-io/g1t" } }), "flagon-io/g1t"); | |
| 177 | assert.equal(landingFor({ workspace: null, repository: null }), null); | |
| 178 | }); | |
| 179 | ||
| 180 | test("the welcome is for one place, and ends", () => { | |
| 181 | const set = welcomeCookie("flagon-io/g1t", true); | |
| 182 | assert.match(set, /^g1t_welcome=flagon-io%2Fg1t; Path=\/; Max-Age=300; HttpOnly; SameSite=Lax; Secure$/); | |
| 183 | const header = `a=1; ${set.split(";")[0]}; b=2`; | |
| 184 | assert.equal(welcomes(header, "flagon-io/g1t"), true); | |
| 185 | assert.equal(welcomes(header, "flagon-io"), false); | |
| 186 | assert.equal(welcomes("g1t_welcome=flagon-io", "Flagon-IO"), true); | |
| 187 | assert.equal(welcomes("g1t_welcome=%E0%A4%A", "flagon-io"), false); | |
| 188 | assert.equal(welcomes("g1t_welcome=..%2F..%2Fx", "../../x"), false); | |
| 189 | assert.equal(welcomes(null, "flagon-io"), false); | |
| 190 | assert.match(clearWelcome(false), /^g1t_welcome=; Path=\/; Max-Age=0; HttpOnly; SameSite=Lax$/); | |
| Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page) | 191 | }); |
| 192 | ||
| 193 | test("the two invites say which they are", () => { | |
| 194 | // A workspace's People page: an invitation to accept or decline, which signs up whoever has no account. | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 195 | const copy = workspaceInviteCopy("Flagon, Inc."); |
| 196 | assert.equal(copy.heading, "Invite to Flagon, Inc."); | |
| 197 | assert.match(copy.hint, /invitation to join Flagon, Inc\..*join only if they accept/); | |
| 198 | assert.match(copy.hint, /If they do not have a g1t account yet, they sign up from the invitation first/); | |
| 199 | assert.doesNotMatch(copy.hint, /one of yours|shared invites/); | |
| Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page) | 200 | }); |
| 201 | ||
| 202 | test("an invite's page names the invite it is", () => { | |
| 203 | const base = { kind: "account" as const, invitedBy: { username: "syntaqx" }, workspace: null, repository: null, hasAccount: false }; | |
| 204 | const g1t = invitePageCopy(base, false); | |
| 205 | assert.equal(`${g1t.before}${g1t.place ?? ""}${g1t.after}`, "@syntaqx invited you to g1t"); | |
| 206 | assert.match(g1t.about, /lets you make an account\. It does not add you to anyone's workspace/); | |
| 207 | const join = invitePageCopy({ ...base, workspace: { name: "Flagon, Inc." } }, false); | |
| 208 | assert.equal(`${join.before}${join.place}${join.after}`, "@syntaqx invited you to join Flagon, Inc. on g1t"); | |
| 209 | assert.equal(join.place, "Flagon, Inc."); | |
| 210 | assert.match(join.about, /invitation to join Flagon, Inc\., which you accept or decline/); | |
| 211 | assert.match(join.about, /You do not have a g1t account yet, so it also lets you make one/); | |
| 212 | // Someone with an account, or signed in, just accepts. | |
| 213 | assert.match(invitePageCopy({ ...base, kind: "workspace", workspace: { name: "Flagon, Inc." }, hasAccount: true }, false).about, /Accepting joins you to Flagon, Inc\./); | |
| 214 | assert.equal(invitePageCopy({ ...base, invitedBy: null }, false).before, "The g1t team invited you to g1t"); | |
| Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040) | 215 | }); |
This file's history is long; its oldest lines are credited to the oldest commit read.