Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

Commit

Ask builds and ships something new from one card (#53): asked to build an app from nothing, Ask had no way to get there. It reads only, propose_session needed a repository that already existed, deployments are turned on only in Settings, and the session it handed off was told never to push to the default branch, so an empty workspace couldn't end with anything live. propose_session now takes new_repository (with private, and deploy, on unless asked otherwise) for a repo in the workspace that doesn't exist yet. Its card says what starting it does: it creates the repository as the person (its project described with the issue's title), turns on its deployments, opens the issue and starts the session, and then shows Created <repo> and the production address on g1t.page, or why deployments couldn't be turned on (no g1t plan, no Admin role), in which case the session still builds it. Sent again (a retry, or Copy after Start), the repository already made is used. The session is told the repository is new and empty: push the first working version straight to the default branch, since there's nothing to open a pull request against, close the issue with Closes #, watch `g1t deploy list` until it's live or fix what failed, and end its report with the address. The agent's system prompt allows that first push and says a push to a deploying project's default branch deploys production. Ask's prompt says when to propose a new repository, that the card, not Ask, changes g1t, and how to answer: what will be built, that it goes live by itself once pushed, and to ask how it's going. For an existing repository it points to Settings → Deployments. Ask still changes nothing itself; the person's click does.

Checked:
cargo test -p g1t-integrations --lib (76 pass, with a new test for new-repository proposals: names, modes, private/deploy defaults, label) and -p g1t-contracts ai (5); apps/web npm run typecheck and node --test app/lib/ask.test.ts (4). Read services/deployments updateSettings (turning on with no commit yet builds nothing, so a new repository isn't a failed deploy) and its git.push handling (a push to the default branch deploys production), projects.describe going through byRepo, which makes the project at once, and repos create (protected false). Not yet run end to end against production.
G1t-Session:
ses_01m4ype025ebxa88d367gx24xv
syntaqxcommitted Parent0519b26Browse files
9 files+212−170/9 viewed
+45−6
1−import { Ban, Check, ChevronRight, CircleAlert, Copy, ExternalLink, LoaderCircle, Play, SquareTerminal } from "lucide-react";
1+import { Ban, Check, ChevronRight, CircleAlert, Copy, ExternalLink, LoaderCircle, Play, Rocket, SquareTerminal } from "lucide-react";
22 import { useEffect, useState } from "react";
33
44 import type { AskSource } from "@g1t/contracts";
55
6−import { type Proposal, type Step, foldAnswer, proposalTitle } from "../lib/ask";
6+import { type Proposal, type Step, foldAnswer, proposalSteps, proposalTitle } from "../lib/ask";
77 import { Badge } from "./ui/badge";
88 import { Button } from "./ui/button";
99 import { Card } from "./ui/card";
116116 );
117117 }
118118
119−type Started = { session_url: string | null; provider_session_url: string | null; handoff: boolean; command: string | null; prompt: string };
119+type Started = {
120+ session_url: string | null;
121+ provider_session_url: string | null;
122+ handoff: boolean;
123+ command: string | null;
124+ prompt: string;
125+ /** A new repository's proposal: what the card made first. */
126+ repo_url?: string;
127+ production_url?: string | null;
128+ deploy_error?: string | null;
129+};
120130
121131 /**
122132 * Work g1t proposed to hand to a session in the person's own harness. Ask
130140 const [started, setStarted] = useState<Started | null>(null);
131141 const [copied, setCopied] = useState(false);
132142 const [error, setError] = useState<string | null>(null);
143+ const [made, setMade] = useState<Pick<Started, "repo_url" | "production_url" | "deploy_error"> | null>(null);
133144
134145 useEffect(() => {
135146 let live = true;
152163 body: JSON.stringify({ ...proposal, handoff }),
153164 });
154165 const body = (await response.json().catch(() => null)) as (Started & { error?: string }) | null;
166+ // The repository may be made even when the session is not: keep where it is.
167+ if (body?.repo_url) setMade({ repo_url: body.repo_url, production_url: body.production_url ?? null, deploy_error: body.deploy_error ?? null });
155168 if (!response.ok || !body) throw new Error(body?.error ?? "The session could not be started.");
156169 setStarted(body);
157170 if (handoff && body.command) {
176189 <span className="truncate text-sm font-medium text-fg">{proposalTitle(proposal)}</span>
177190 <Badge className="px-1.5 py-0 font-normal">Session</Badge>
178191 </div>
179− <p className="mt-0.5 text-xs text-muted">
180− {proposal.issue ? "Runs in your own harness, with your skills, tied to the issue." : "Opens the issue, then runs in your own harness, tied to it."}
181− </p>
192+ <p className="mt-0.5 text-xs text-muted">{proposalSteps(proposal)}</p>
182193 {proposal.instruction && <p className="mt-1 line-clamp-2 text-xs text-faint">{proposal.instruction}</p>}
183194 </div>
184195 </div>
196+ {made && (
197+ <div className="flex flex-col gap-1 text-xs">
198+ <span className="flex flex-wrap items-center gap-1.5">
199+ <Check size={13} aria-hidden="true" className="text-success" />
200+ <span className="text-muted">Created</span>
201+ <Button asChild variant="link" size="inline" className="text-xs">
202+ <a href={made.repo_url}>{proposal.repo}</a>
203+ </Button>
204+ </span>
205+ {made.production_url && (
206+ <span className="flex flex-wrap items-center gap-1.5">
207+ <Rocket size={13} aria-hidden="true" className="text-accent" />
208+ <span className="text-muted">Live once the first version is pushed at</span>
209+ <Button asChild variant="link" size="inline" className="text-xs">
210+ <a href={made.production_url} target="_blank" rel="noreferrer">
211+ {made.production_url.replace(/^https?:\/\//, "")} <ExternalLink size={11} aria-hidden="true" />
212+ </a>
213+ </Button>
214+ </span>
215+ )}
216+ {made.deploy_error && (
217+ <span className="flex items-start gap-1.5 text-warn">
218+ <CircleAlert size={12} aria-hidden="true" className="mt-0.5 shrink-0" />
219+ Not deployed: {made.deploy_error}
220+ </span>
221+ )}
222+ </div>
223+ )}
185224 {running ? (
186225 <div className="flex flex-wrap items-center gap-2 text-xs">
187226 <Check size={13} aria-hidden="true" className="text-success" />
+12−0
7070 /** A session g1t proposed in an answer: the person starts it, or copies its command. */
7171 export type Proposal = Extract<AskEvent, { type: "proposal" }>;
7272
73+/** What starting a proposal does, in a line, under its title. */
74+export function proposalSteps(proposal: Proposal): string {
75+ if (proposal.new_repository) {
76+ const made = `Creates ${proposal.private ? "the private" : "the public"} repository`;
77+ return proposal.deploy
78+ ? `${made}, turns on its deployments, then runs in your own harness and pushes the first version, which goes live on g1t.page.`
79+ : `${made}, then runs in your own harness and pushes the first version.`;
80+ }
81+ return proposal.issue ? "Runs in your own harness, with your skills, tied to the issue." : "Opens the issue, then runs in your own harness, tied to it.";
82+}
83+
7384 /** What a proposal asks the session to do, in a line: "Implement #12 in acme/web". */
7485 export function proposalTitle(proposal: Proposal): string {
86+ if (proposal.new_repository) return `Build “${proposal.title}” in ${proposal.repo}`;
7587 const verb = { plan: "Plan", implement: "Implement", review: "Review" }[proposal.mode] ?? "Work on";
7688 return proposal.issue ? `${verb} #${proposal.issue} in ${proposal.repo}` : `${verb} “${proposal.title}” in ${proposal.repo}`;
7789 }
+66−6
1010 * `POST -/ask` starts a session g1t proposed in an answer (the proposal
1111 * card in components/ask-steps): the person's click is the confirmation.
1212 * `handoff` asks for the Claude Code command instead of starting one on
13− * the workspace's Anthropic account.
13+ * the workspace's Anthropic account. A proposal with `new_repository`
14+ * first creates the repository as the person (its project described with
15+ * the issue's title), turns on its deployments when `deploy`, and tells
16+ * the session to push the first version to the default branch, which
17+ * deploys it. Sent again (a retry, or Copy after Start), a repository
18+ * already made is used as it is.
1419 */
1520 import { data } from "react-router";
1621
17−import { AGENT_MODES } from "@g1t/contracts";
22+import { AGENT_MODES, type User } from "@g1t/contracts";
1823
1924 import type { Route } from "./+types/ask";
20−import { agentSessions, ai } from "../../lib/services.server";
25+import { agentSessions, ai, deployments, projects, repos } from "../../lib/services.server";
2126 import { assertSameOrigin, getViewer, requireUser, roleIn } from "../../lib/session.server";
2227
2328 export async function loader({ params, context, request }: Route.LoaderArgs) {
4954 const title = issue ? undefined : text("title");
5055 if (!issue && !title) return Response.json({ error: "The proposal names no issue." }, { status: 400 });
5156 const mode = AGENT_MODES.find((one) => one.mode === input?.mode)?.mode ?? "implement";
57+ let instruction = text("instruction");
58+ let built: Built | null = null;
59+ if (input?.new_repository === true) {
60+ const made = await newRepository(user, namespace, name, { title: title ?? name, isPrivate: input.private === true, deploy: input.deploy !== false });
61+ if ("error" in made) return Response.json({ error: made.error }, { status: 400 });
62+ built = made;
63+ instruction = [firstPush(made), instruction].filter(Boolean).join("\n\n");
64+ }
5265 const result = await agentSessions
53− .start(user, { namespace, name }, { issue, title, body: issue ? undefined : text("body"), instruction: text("instruction"), mode, handoff: input?.handoff === true })
66+ .start(user, { namespace, name }, { issue, title, body: issue ? undefined : text("body"), instruction, mode, handoff: input?.handoff === true })
5467 .catch(() => null);
55− if (!result) return Response.json({ error: "The session could not be started. Try again in a moment." }, { status: 502 });
56− if (!result.ok) return Response.json({ error: result.error.message }, { status: 400 });
68+ if (!result) return Response.json({ error: "The session could not be started. Try again in a moment.", ...built }, { status: 502 });
69+ if (!result.ok) return Response.json({ error: result.error.message, ...built }, { status: 400 });
5770 const started = result.value;
5871 return Response.json({
5972 issue: started.issue,
6275 handoff: started.handoff,
6376 command: started.command,
6477 prompt: started.prompt,
78+ ...built,
6579 });
6680 }
81+
82+/** What the card made before the session: the repository, and where it deploys or why it can't. */
83+type Built = { repo_url: string; default_branch: string; production_url: string | null; deploy_error: string | null };
84+
85+/**
86+ * The repository a new-repository proposal names, made as the person, or
87+ * the one already made by an earlier click; its project described; its
88+ * deployments on when asked. A deploy that can't be turned on (no plan, no
89+ * Admin role) is said, not fatal: the session still builds it.
90+ */
91+async function newRepository(
92+ user: User,
93+ namespace: string,
94+ name: string,
95+ wanted: { title: string; isPrivate: boolean; deploy: boolean },
96+): Promise<Built | { error: string }> {
97+ const created = await repos.create(user, { namespace, name, isPrivate: wanted.isPrivate }).catch(() => null);
98+ let repo = created?.ok ? created.value : null;
99+ if (!repo) {
100+ // Already made: a second click, or Copy after Start. Anything else is the reason it wasn't.
101+ const found = await repos.get({ namespace, name }, user).catch(() => null);
102+ if (!found?.ok) return { error: created && !created.ok ? created.error.message : "The repository could not be created. Try again in a moment." };
103+ repo = found.value;
104+ } else {
105+ const described = await projects.describe(user, repo.id, wanted.title).catch((error: unknown) => String(error));
106+ if (typeof described === "string" || !described.ok) console.error("ask: new repository without its description", repo.id, described);
107+ }
108+ const built: Built = { repo_url: `/${repo.namespace}/${repo.name}`, default_branch: repo.defaultBranch, production_url: null, deploy_error: null };
109+ if (!wanted.deploy) return built;
110+ const saved = await deployments.updateSettings(user, { workspace: repo.namespace, slug: repo.name }, { enabled: true }).catch(() => null);
111+ if (saved?.ok) built.production_url = saved.value.productionUrl;
112+ else built.deploy_error = saved ? saved.error.message : "Deployments could not be turned on. Try again from the project's Settings → Deployments.";
113+ return built;
114+}
115+
116+/** What the session is told about a repository the card just made. */
117+function firstPush(built: Built): string {
118+ const live = built.production_url
119+ ? `Its deployments are on: that push deploys production to ${built.production_url}. Watch it with \`g1t deploy list\` until it is live; if it fails, read why (\`g1t deploy view ID\`), fix it and push again. End your report with the address.`
120+ : "Its deployments are off, so nothing deploys yet; say so in your report.";
121+ return (
122+ `The repository was just created for this and is empty. Build the first working version and push it straight to the default branch ` +
123+ `(\`git push origin HEAD:${built.default_branch}\`): there is nothing to open a pull request against yet, so open none. ` +
124+ `Keep it small and make it build and serve from the repository's root. Put \`Closes #\` and the issue's number in the commit message, so the push closes it. ${live}`
125+ );
126+}
+11−0
434434 /// on an issue, or on a new one with `title` and `body`. Nothing has
435435 /// started; the person starts it, or copies the command, from the card
436436 /// the answer shows. `mode` is `plan`, `implement` or `review`.
437+ ///
438+ /// With `new_repository`, `repo` does not exist yet: the card creates
439+ /// it (`private` or public), turns on its deployments when `deploy`,
440+ /// then opens the issue and starts the session, which pushes the first
441+ /// version to the default branch, and so deploys it.
437442 Proposal {
438443 id: String,
439444 repo: String,
442447 body: Option<String>,
443448 mode: String,
444449 instruction: Option<String>,
450+ #[serde(default, skip_serializing_if = "std::ops::Not::not")]
451+ new_repository: bool,
452+ #[serde(default, skip_serializing_if = "std::ops::Not::not")]
453+ private: bool,
454+ #[serde(default, skip_serializing_if = "std::ops::Not::not")]
455+ deploy: bool,
445456 },
446457 /// The answer is complete. `stop_reason` as the provider said it, such
447458 /// as `end_turn`, `stop` or `max_tokens`.
+9−0
246246 body: string | null;
247247 mode: "plan" | "implement" | "review";
248248 instruction: string | null;
249+ /**
250+ * `repo` does not exist yet: the card creates it (`private` or
251+ * public), turns on its deployments when `deploy`, then opens the
252+ * issue and starts the session, which pushes the first version to
253+ * the default branch, and so deploys it.
254+ */
255+ new_repository?: boolean;
256+ private?: boolean;
257+ deploy?: boolean;
249258 }
250259 | { type: "done"; stop_reason: string | null }
251260 | { type: "error"; message: string };
+5−1
115115 - Follow the work-on-issue skill. Check the work context and the sessions on the issue before you start, and claim only what you take. \
116116 When work splits into independent parts, you may give a part to a copy of yourself; each is recorded on g1t as a subagent and sees the others.\n\
117117 - Put the trailer `G1t-Session: <the session id in your first message>` on every commit, so g1t ties the commits to this session.\n\
118− - Work on a branch whose name starts with the issue number, and open a draft pull request early. Never push to the default branch.\n\
118+ - Work on a branch whose name starts with the issue number, and open a draft pull request early. Never push to the default branch, \
119+ except a repository's first commit when it has none yet: there is nothing to open a pull request against, so the first version goes \
120+ straight to it.\n\
121+ - A push to the default branch of a project with deployments on deploys production. When you push there, watch it with \
122+ `g1t deploy list` until it is live or failed; if it failed, read why, fix it and push again. Say where it is live in your report.\n\
119123 - Never write `@g1t` in what you post on g1t: that asks g1t for another session.\n\
120124 - When you finish, say in a few sentences what you did, what is left, and the pull request, if any. That is your report on g1t."
121125 .to_owned()
+15−0
377377 your answer to start it on their workspace's Anthropic account or copy a Claude Code command; nothing \
378378 starts until they choose. If a session already holds the issue, say so and link it instead of proposing \
379379 another. Split large work into a few issues and propose a session for each. \
380+ The card the person clicks is what changes g1t, never you.\n\
381+ To build something new from nothing (an app, a site, an API: \"build me…\", \"make a…\", \"let's ship a…\"), \
382+ propose one session with `new_repository` true and a `repo` of \"{slug}/<name>\", a short lowercase name \
383+ that does not exist yet (check with the repository tool's `get`; if it exists, pick another or work there), \
384+ a title and a body saying what to build and what done means (keep it small enough to ship in one go, and \
385+ say it must build and serve from the repository's root), and mode implement. Leave `deploy` on unless they \
386+ don't want it live. Starting the card creates the repository, turns on its deployments, opens the issue and \
387+ starts the session, which pushes the first version straight to the default branch; that push deploys \
388+ production on g1t.page, and the card shows the address. Say so plainly: what will be built, that it goes live \
389+ by itself once the session pushes, and that they can ask you how it is going (read the session, then the \
390+ workflow tool's `list_deployments`). If the workspace isn't on the g1t plan, deployments can't be turned on; \
391+ the card says so and the session still builds it.\n\
392+ For an existing repository they want live: if its deployments are off, an admin turns them on at \
393+ {site}/owner/repo/settings/deployments (Turn on deployments); after that every push to the default branch \
394+ deploys production and every pull request gets a preview.\n\
380395 When they ask about work already under way, read the issue's `work` and the session tool's `get` for the \
381396 session doing it (its subagents, what it touched, where it stands), and link the session.\n\n\
382397 ## Answering\n\
+8−1
3333 /// What the model is told once a session is proposed.
3434 const PROPOSED: &str = "Proposed. The person now sees a card under your answer to start this session on their workspace's Anthropic account or copy a Claude Code command for it; nothing has started. Don't ask them to confirm in words: say in a sentence what the session will do.";
3535
36+/// What the model is told once it proposes building a new repository.
37+const PROPOSED_NEW: &str = "Proposed. The person now sees a card under your answer; nothing has happened yet. Starting it creates the repository, turns on its deployments if you asked, opens the issue and starts the session, which pushes the first version to the default branch; that push deploys it, and the card then shows the address where it will be live on g1t.page. Don't ask them to confirm in words: say in a sentence or two what will be built, that it goes live on its own once the session pushes it, and that they can ask you how it is going.";
38+
3639 /// The most sessions one answer proposes.
3740 const MAX_PROPOSALS: usize = 5;
3841
287290 match ask_tools::proposal(&id, &call.input) {
288291 Ok(proposal) => {
289292 self.proposals += 1;
293+ let told = match &proposal {
294+ AskEvent::Proposal { new_repository: true, .. } => PROPOSED_NEW,
295+ _ => PROPOSED,
296+ };
290297 self.out.push_back(ask::line(&proposal));
291− Plan::Skip(PROPOSED.to_owned(), AskToolStatus::Done)
298+ Plan::Skip(told.to_owned(), AskToolStatus::Done)
292299 }
293300 Err(why) => Plan::Skip(why, AskToolStatus::Failed),
294301 }
+41−3
188188 vec![Native {
189189 spec: ToolSpec {
190190 name: PROPOSE_SESSION.to_owned(),
191− description: "Propose handing work to a coding session in the person's own harness (their Claude, with their skills and subagents), tied to an issue. Use it when they ask for work to be done: planning, implementing, fixing or reviewing. It starts nothing: the person sees a card to start the session on their workspace's Anthropic account, or to copy a Claude Code command to run it themselves. Give `issue` when the work has one (read it first), else `title` and `body` for a new issue. Propose one session per piece of work."
191+ description: "Propose handing work to a coding session in the person's own harness (their Claude, with their skills and subagents), tied to an issue. Use it when they ask for work to be done: planning, implementing, fixing or reviewing. It starts nothing: the person sees a card to start the session on their workspace's Anthropic account, or to copy a Claude Code command to run it themselves. Give `issue` when the work has one (read it first), else `title` and `body` for a new issue. Propose one session per piece of work. To build something new from nothing (an app, a site, a service), set `new_repository` with a `repo` in the workspace that does not exist yet: the card creates it, turns on its deployments (unless `deploy` is false), opens the issue and starts the session, which pushes the first version to the default branch, and that deploys it."
192192 .to_owned(),
193193 input_schema: json!({
194194 "type": "object",
195195 "properties": {
196− "repo": { "type": "string", "description": "The repository, owner/name." },
196+ "repo": { "type": "string", "description": "The repository, owner/name. With new_repository, a new name in the workspace: short, lowercase, words joined by -." },
197+ "new_repository": { "type": "boolean", "description": "The repository does not exist yet: the card creates it first. Needs title, body and mode implement." },
198+ "private": { "type": "boolean", "description": "With new_repository: make it private. Public unless they ask." },
199+ "deploy": { "type": "boolean", "description": "With new_repository: turn on its deployments, so the first push goes live on g1t.page. Default true." },
197200 "issue": { "type": "integer", "description": "The issue the work belongs to." },
198201 "title": { "type": "string", "description": "A new issue's title, when there is no issue yet." },
199202 "body": { "type": "string", "description": "A new issue's description: what to do and what done means." },
220223 Some(mode @ ("plan" | "implement" | "review")) => mode.to_owned(),
221224 _ => return Err("Give a mode: plan, implement or review.".to_owned()),
222225 };
226+ let new_repository = arguments["new_repository"].as_bool() == Some(true);
227+ if new_repository {
228+ let name = repo.split('/').nth(1).unwrap_or_default();
229+ if name.is_empty() || !name.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.')) {
230+ return Err("A new repository's name is letters, digits, - _ and . only, such as hello-world.".to_owned());
231+ }
232+ if issue.is_some() || mode != "implement" {
233+ return Err("A new repository has no issues yet: give a title and body for its first, and mode implement.".to_owned());
234+ }
235+ }
223236 let clip = |value: Option<String>, max: usize| value.map(|value| value.chars().take(max).collect::<String>());
224237 Ok(AskEvent::Proposal {
225238 id: id.to_owned(),
229242 body: if issue.is_some() { None } else { clip(text("body"), 8_000) },
230243 mode,
231244 instruction: clip(text("instruction"), 4_000),
245+ new_repository,
246+ private: new_repository && arguments["private"].as_bool() == Some(true),
247+ deploy: new_repository && arguments["deploy"].as_bool() != Some(false),
232248 })
233249 }
234250
340356 return format!("Searched g1t for \u{201c}{query}\u{201d}");
341357 }
342358 if tool == PROPOSE_SESSION {
359+ if arguments["new_repository"].as_bool() == Some(true) {
360+ let repo = arguments["repo"].as_str().unwrap_or("a new repository");
361+ return format!("Proposed building {repo}, a new repository");
362+ }
343363 let what = arguments["issue"].as_u64().map(|number| format!("issue #{number}")).unwrap_or_else(|| "a new issue".to_owned());
344364 let repo = arguments["repo"].as_str().unwrap_or("the repository");
345365 return format!("Proposed a session on {what} in {repo}");
725745 assert!(toolbox.is_native(PROPOSE_SESSION));
726746 let on_issue = json!({ "repo": "acme/web", "issue": 12, "mode": "implement", "title": "ignored", "instruction": " use the new name " });
727747 assert_eq!(toolbox.decide(PROPOSE_SESSION, &on_issue), Decision::Confirm);
728− let AskEvent::Proposal { id, repo, issue, title, body, mode, instruction } = proposal("proposal_0_0", &on_issue).unwrap() else {
748+ let AskEvent::Proposal { id, repo, issue, title, body, mode, instruction, new_repository, deploy, .. } =
749+ proposal("proposal_0_0", &on_issue).unwrap()
750+ else {
729751 panic!()
730752 };
731753 assert_eq!((id.as_str(), repo.as_str(), issue, mode.as_str()), ("proposal_0_0", "acme/web", Some(12), "implement"));
732754 assert_eq!((title, body, instruction.as_deref()), (None, None, Some("use the new name")));
755+ assert!(!new_repository && !deploy, "an existing repository is left as it is");
733756 let new_issue = json!({ "repo": "acme/web", "title": "Add a collaborators route", "body": "List them.", "mode": "plan" });
734757 let AskEvent::Proposal { issue, title, .. } = proposal("p", &new_issue).unwrap() else { panic!() };
735758 assert_eq!((issue, title.as_deref()), (None, Some("Add a collaborators route")));
740763 }
741764
742765 #[test]
766+ fn a_new_repository_is_created_deployed_and_built_from_one_card() {
767+ let build = json!({ "repo": "acme/hello-world", "new_repository": true, "title": "Hello world", "body": "A page that says hello.", "mode": "implement" });
768+ let AskEvent::Proposal { repo, issue, title, new_repository, private, deploy, .. } = proposal("p", &build).unwrap() else { panic!() };
769+ assert_eq!((repo.as_str(), issue, title.as_deref()), ("acme/hello-world", None, Some("Hello world")));
770+ assert!(new_repository && deploy && !private, "public and deployed unless asked otherwise");
771+ let quiet = json!({ "repo": "acme/notes", "new_repository": true, "private": true, "deploy": false, "title": "Notes", "mode": "implement" });
772+ let AskEvent::Proposal { private, deploy, .. } = proposal("p", &quiet).unwrap() else { panic!() };
773+ assert!(private && !deploy);
774+ assert!(proposal("p", &json!({ "repo": "acme/hello world", "new_repository": true, "title": "x", "mode": "implement" })).is_err());
775+ assert!(proposal("p", &json!({ "repo": "acme/hello", "new_repository": true, "title": "x", "mode": "plan" })).is_err());
776+ assert!(proposal("p", &json!({ "repo": "acme/hello", "new_repository": true, "issue": 1, "mode": "implement" })).is_err());
777+ assert_eq!(label(PROPOSE_SESSION, &build), "Proposed building acme/hello-world, a new repository");
778+ }
779+
780+ #[test]
743781 fn steps_say_what_they_do_in_a_line() {
744782 assert_eq!(label("pull_request", &json!({ "action": "list", "repo": "acme/web" })), "Listed pull requests in acme/web");
745783 assert_eq!(label("pull_request", &json!({ "action": "get", "repo": "acme/web", "number": 7 })), "Read pull request #7 in acme/web");

Comments

Sign in to comment.