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.
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 1 | //! Every response the REST routes give, run through the converter the API |
| 2 | //! sends them with, checked for `camelCase` that would leak out. | |
| 3 | //! | |
| 4 | //! The samples are the reference's example responses, put back into the | |
| 5 | //! `camelCase` the services send (as serde's `rename_all` writes it) and, | |
| 6 | //! where an operation returns a contract type, decoded into that type and | |
| 7 | //! encoded again, so that every field the type has is sent, not only the | |
| 8 | //! ones an example shows. | |
| 9 | ||
| Merge checks: statuses and check runs on every commit | 10 | use g1t_contracts::{access, actions, checks, codeowners, integrations, repos, rules, search, teams, webhooks, work}; |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 11 | use g1t_kit::wire::{self, USER_KEYED}; |
| 12 | use serde::Serialize; | |
| 13 | use serde::de::DeserializeOwned; | |
| 14 | use serde_json::{Map, Value, json}; | |
| 15 | ||
| 16 | use crate::openapi::document; | |
| 17 | use crate::operations::Op; | |
| Merge checks: statuses and check runs on every commit | 18 | use crate::checks::ChecksOp; |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 19 | use crate::rules::RulesOp; |
| Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca | 20 | use crate::deploy_keys::DeployKeysOp; |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 21 | |
| 22 | /// A key as `#[serde(rename_all = "camelCase")]` writes it. | |
| 23 | fn camel_key(key: &str) -> String { | |
| 24 | let mut out = String::with_capacity(key.len()); | |
| 25 | let mut upper = false; | |
| 26 | for c in key.chars() { | |
| 27 | if c == '_' { | |
| 28 | upper = true; | |
| 29 | } else if upper { | |
| 30 | out.extend(c.to_uppercase()); | |
| 31 | upper = false; | |
| 32 | } else { | |
| 33 | out.push(c); | |
| 34 | } | |
| 35 | } | |
| 36 | out | |
| 37 | } | |
| 38 | ||
| 39 | /// A response as the services send it: `camelCase`, but for the maps the | |
| 40 | /// converter passes through, which are data. | |
| 41 | fn as_services_send(value: &Value) -> Value { | |
| 42 | match value { | |
| 43 | Value::Object(fields) => { | |
| 44 | let mut out = Map::new(); | |
| 45 | for (key, value) in fields { | |
| 46 | let user_keyed = value.is_object() | |
| 47 | && (USER_KEYED.contains(&key.as_str()) || key.starts_with("by_")); | |
| 48 | let value = if user_keyed { value.clone() } else { as_services_send(value) }; | |
| 49 | // A `by_…` map keeps its name in the converter's spelling. | |
| 50 | let key = if key.starts_with("by_") { key.clone() } else { camel_key(key) }; | |
| 51 | out.insert(key, value); | |
| 52 | } | |
| 53 | Value::Object(out) | |
| 54 | } | |
| 55 | Value::Array(items) => Value::Array(items.iter().map(as_services_send).collect()), | |
| 56 | other => other.clone(), | |
| 57 | } | |
| 58 | } | |
| 59 | ||
| 60 | /// `value` decoded as `T` and encoded again, as the service would send it. | |
| 61 | fn through<T: DeserializeOwned + Serialize>(op: Op, value: Value) -> Value { | |
| 62 | let decoded: T = serde_json::from_value(value) | |
| 63 | .unwrap_or_else(|error| panic!("{}: the example is not a {}: {error}", op.name(), std::any::type_name::<T>())); | |
| 64 | serde_json::to_value(decoded).unwrap() | |
| 65 | } | |
| 66 | ||
| 67 | /// What the service behind an operation sends, from its example. | |
| 68 | fn sample(op: Op, example: &Value) -> Value { | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 69 | // Types serde already writes in `snake_case`: a person, and who has |
| 70 | // access. Sent as they are. | |
| 71 | let as_is = example.clone(); | |
| 72 | match op { | |
| 73 | Op::Whoami => return through::<g1t_contracts::User>(op, as_is), | |
| 74 | Op::ListCollaborators => return through::<access::RepoAccess>(op, as_is), | |
| 75 | Op::AddCollaborator => return through::<access::Added>(op, as_is), | |
| 76 | Op::UpdateCollaborator => return through::<access::Collaborator>(op, as_is), | |
| 77 | Op::GetCollaboratorPermission => return through::<access::PermissionInfo>(op, as_is), | |
| 78 | Op::ListRepoInvitations | Op::ListMyRepoInvitations => { | |
| 79 | return through::<Vec<access::RepoInvitation>>(op, as_is); | |
| 80 | } | |
| 81 | Op::RevokeRepoInvitation | Op::AcceptRepoInvitation | Op::DeclineRepoInvitation => { | |
| 82 | return through::<access::RepoInvitation>(op, as_is); | |
| 83 | } | |
| 84 | Op::ListOutsideCollaborators => return through::<Vec<access::OutsideCollaborator>>(op, as_is), | |
| Merge main (membership, two-factor, GitHub repo roles) into tokens | 85 | // Members, also `snake_case`. |
| 86 | Op::ListMembers => return through::<Vec<g1t_contracts::identity::Member>>(op, as_is), | |
| 87 | Op::UpdateMember => return through::<g1t_contracts::identity::Member>(op, as_is), | |
| 88 | Op::RemoveMember | Op::TransferOwnership | Op::LeaveWorkspace => return through::<bool>(op, as_is), | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 89 | // Teams and code owners, also `snake_case`. |
| 90 | Op::ListTeams | Op::ListChildTeams | Op::ListUserTeams => return through::<Vec<teams::Team>>(op, as_is), | |
| 91 | Op::GetTeam | Op::CreateTeam | Op::UpdateTeam | Op::SetTeamReviewAssignment => { | |
| 92 | return through::<teams::Team>(op, as_is); | |
| 93 | } | |
| 94 | Op::ListTeamMembers => return through::<Vec<teams::TeamMember>>(op, as_is), | |
| 95 | Op::SetTeamMember => return through::<teams::TeamMember>(op, as_is), | |
| 96 | Op::ListTeamRepos => return through::<Vec<teams::TeamRepo>>(op, as_is), | |
| 97 | Op::SetTeamRepo => return through::<teams::TeamRepo>(op, as_is), | |
| 98 | Op::DeleteTeam | Op::RemoveTeamMember | Op::RemoveTeamRepo => return through::<bool>(op, as_is), | |
| 99 | Op::GetCodeownersErrors => return through::<codeowners::CodeOwnersReport>(op, as_is), | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 100 | // Rulesets travel in `snake_case` between services too. |
| 101 | Op::Rules(RulesOp::ListRepoRulesets | RulesOp::ListWorkspaceRulesets) => { | |
| 102 | return through::<Vec<rules::Ruleset>>(op, as_is); | |
| 103 | } | |
| 104 | Op::Rules( | |
| 105 | RulesOp::GetRepoRuleset | |
| 106 | | RulesOp::CreateRepoRuleset | |
| 107 | | RulesOp::UpdateRepoRuleset | |
| 108 | | RulesOp::GetWorkspaceRuleset | |
| 109 | | RulesOp::CreateWorkspaceRuleset | |
| 110 | | RulesOp::UpdateWorkspaceRuleset, | |
| 111 | ) => return through::<rules::Ruleset>(op, as_is), | |
| 112 | Op::Rules(RulesOp::GetBranchRules) => return through::<rules::EffectiveRules>(op, as_is), | |
| 113 | Op::Rules(RulesOp::ListRuleEvaluations | RulesOp::ListWorkspaceRuleEvaluations) => { | |
| 114 | return through::<rules::EvaluationPage>(op, as_is); | |
| 115 | } | |
| 116 | // Built by the API itself. | |
| 117 | Op::Rules(RulesOp::DeleteRepoRuleset | RulesOp::DeleteWorkspaceRuleset) => return as_is, | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 118 | // Deployments travel in `snake_case` between services too. |
| 119 | Op::Deployments(_) => return as_is, | |
| Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2 | 120 | // Artifacts are shaped by the API itself, in `snake_case`. |
| 121 | Op::Artifacts(_) => return as_is, | |
| Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca | 122 | // Deploy keys travel in `snake_case` from identity. |
| 123 | Op::DeployKeys(DeployKeysOp::ListDeployKeys) => return through::<Vec<g1t_contracts::deploy_keys::DeployKey>>(op, as_is), | |
| 124 | Op::DeployKeys(DeployKeysOp::GetDeployKey | DeployKeysOp::CreateDeployKey) => { | |
| 125 | return through::<g1t_contracts::deploy_keys::DeployKey>(op, as_is); | |
| 126 | } | |
| 127 | Op::DeployKeys(DeployKeysOp::DeleteDeployKey) => return through::<bool>(op, as_is), | |
| Merge packages: roles, Actions access, source label, soft delete, API | 128 | // Packages are shaped by the API itself, in `snake_case`. |
| 129 | Op::Packages(_) => return as_is, | |
| Merge main into Artifacts Phase 2 | 130 | // Artifacts are shaped by the API itself, in `snake_case`. |
| 131 | Op::Folios(_) => return as_is, | |
| Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next. | 132 | // Agents' computers travel in `snake_case` from the agents service. |
| 133 | Op::GetAgentComputer | Op::WakeAgentComputer | Op::SleepAgentComputer | Op::ResetAgentComputer | Op::ListAgentComputerCommands => return as_is, | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 134 | // Built by the API itself, in `snake_case`. |
| 135 | Op::ListSecurityAlerts => return through::<Vec<crate::alerts::SecurityAlert>>(op, as_is), | |
| 136 | Op::DismissSecurityAlert | Op::ReopenSecurityAlert => { | |
| 137 | return through::<crate::alerts::SecurityAlert>(op, as_is); | |
| 138 | } | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 139 | _ => {} |
| 140 | } | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 141 | let mut sent = as_services_send(example); |
| 142 | // A pull request's code owners are `snake_case` inside it. | |
| 143 | if op == Op::GetPullRequest | |
| 144 | && let Some(code_owners) = example.get("code_owners") | |
| 145 | { | |
| 146 | sent["codeOwners"] = code_owners.clone(); | |
| 147 | } | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 148 | match op { |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 149 | Op::GetWorkspace | Op::CreateWorkspace | Op::UpdateWorkspace => through::<g1t_contracts::identity::Workspace>(op, sent), |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 150 | Op::ListRepos => through::<Vec<repos::Repo>>(op, sent), |
| Search across all of g1t, Explore, and a command palette | 151 | Op::Search => through::<search::SearchResults>(op, sent), |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 152 | Op::GetRepo |
| 153 | | Op::CreateRepo | |
| 154 | | Op::UpdateRepo | |
| 155 | | Op::TransferRepo | |
| 156 | | Op::RenameRepo | |
| 157 | | Op::RenameBranch | |
| 158 | | Op::ArchiveRepo | |
| 159 | | Op::UnarchiveRepo | |
| 160 | | Op::SetRepoVisibility | |
| 161 | | Op::RestoreRepo => through::<repos::Repo>(op, sent), | |
| 162 | Op::DeleteRepo => through::<repos::DeletedRepo>(op, sent), | |
| 163 | Op::ListDeletedRepos => through::<Vec<repos::DeletedRepo>>(op, sent), | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 164 | Op::GetRepoSettings | Op::UpdateRepoSettings => through::<work::RepoSettings>(op, sent), |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 165 | Op::ListCheckNames => through::<Vec<work::SeenCheck>>(op, sent), |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 166 | Op::GetMergeQueue => through::<work::QueueView>(op, sent), |
| 167 | Op::ListIssues => through::<Vec<work::Issue>>(op, sent), | |
| 168 | Op::CreateIssue | Op::UpdateIssue | Op::CloseIssue | Op::ReopenIssue => { | |
| 169 | through::<work::Issue>(op, sent) | |
| 170 | } | |
| 171 | Op::GetIssue => through::<work::IssueDetail>(op, sent), | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 172 | Op::Delegate => through::<work::Delegated>(op, sent), |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 173 | Op::ListPullRequests => through::<Vec<work::Pull>>(op, sent), |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 174 | Op::UpdatePullRequest => through::<work::Pull>(op, sent), |
| 175 | Op::ListLabels | Op::AddDefaultLabels | Op::ListIssueLabels => through::<Vec<work::Label>>(op, sent), | |
| 176 | Op::CreateLabel | Op::UpdateLabel => through::<work::Label>(op, sent), | |
| 177 | Op::ListMilestones => through::<Vec<work::Milestone>>(op, sent), | |
| 178 | Op::CreateMilestone | Op::UpdateMilestone => through::<work::Milestone>(op, sent), | |
| 179 | Op::GetMilestone => through::<work::MilestoneDetail>(op, sent), | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 180 | Op::GetPullRequest => through::<work::PullDetail>(op, sent), |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 181 | Op::MarkPullRequestReady |
| 182 | | Op::ClosePullRequest | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 183 | | Op::ReopenPullRequest |
| 184 | | Op::ConvertPullRequestToDraft | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 185 | | Op::MergePullRequest |
| 186 | | Op::AssignIssue | |
| 187 | | Op::RequestReviewers | |
| 188 | | Op::RemoveRequestedReviewers => { | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 189 | through::<work::Pull>(op, sent) |
| 190 | } | |
| 191 | Op::ListWorkflows => through::<Vec<actions::Workflow>>(op, sent), | |
| 192 | Op::ListWorkflowRuns => through::<Vec<actions::WorkflowRun>>(op, sent), | |
| 193 | Op::GetWorkflowRun => through::<actions::RunDetail>(op, sent), | |
| 194 | Op::GetJobLogs => through::<actions::JobLog>(op, sent), | |
| 195 | Op::DispatchWorkflow | Op::CancelWorkflowRun | Op::RerunWorkflowRun => { | |
| 196 | through::<actions::WorkflowRun>(op, sent) | |
| 197 | } | |
| 198 | Op::ListActionsSecrets | Op::ListActionsVariables => through::<Vec<actions::Setting>>(op, sent), | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 199 | Op::ListRunners => through::<Vec<g1t_contracts::runners::Runner>>(op, sent), |
| 200 | Op::ListRunnerGroups => through::<Vec<g1t_contracts::runners::RunnerGroup>>(op, sent), | |
| 201 | Op::CreateRunnerGroup | Op::UpdateRunnerGroup => through::<g1t_contracts::runners::RunnerGroup>(op, sent), | |
| 202 | Op::GetRunnerSettings | Op::UpdateRunnerSettings => through::<g1t_contracts::runners::RunnerSettings>(op, sent), | |
| 203 | Op::CreateRunnerRegistrationToken => through::<g1t_contracts::runners::RegistrationToken>(op, sent), | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 204 | Op::ListWebhooks => through::<Vec<webhooks::Hook>>(op, sent), |
| 205 | Op::ListIntegrations => through::<Vec<integrations::Connection>>(op, sent), | |
| 206 | Op::GetModelRoutes | Op::SetModelRoutes => through::<Vec<integrations::ModelRoute>>(op, sent), | |
| 207 | Op::ListEvents => through::<Vec<g1t_contracts::events::Event>>(op, sent), | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 208 | Op::ListEmails | Op::AddEmail | Op::RemoveEmail | Op::UpdateEmailSettings => { |
| 209 | through::<g1t_contracts::accounts::AccountEmails>(op, sent) | |
| 210 | } | |
| Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036) | 211 | Op::ConfirmEmail => through::<g1t_contracts::accounts::EmailConfirmed>(op, sent), |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 212 | Op::ListInvites => through::<g1t_contracts::identity::InvitesOverview>(op, sent), |
| 213 | Op::CreateInvite | Op::RevokeInvite | Op::InviteMember | Op::RevokeWorkspaceInvite => { | |
| 214 | through::<g1t_contracts::identity::Invite>(op, sent) | |
| 215 | } | |
| 216 | Op::ListWorkspaceInvites => through::<Vec<g1t_contracts::identity::Invite>>(op, sent), | |
| 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) | 217 | Op::ListInvitations => through::<Vec<g1t_contracts::identity::WorkspaceInvitation>>(op, sent), |
| API: notifications over REST and MCP, with notifications scopes | 218 | Op::ListNotifications => through::<g1t_contracts::inbox::InboxPage>(op, sent), |
| 219 | Op::GetNotificationThread | Op::MarkThreadRead | Op::MarkThreadDone | Op::SaveThread | Op::SnoozeThread => { | |
| 220 | through::<g1t_contracts::inbox::InboxThread>(op, sent) | |
| 221 | } | |
| 222 | Op::GetThreadSubscription | Op::SetThreadSubscription | Op::DeleteThreadSubscription => { | |
| 223 | through::<g1t_contracts::inbox::ThreadSubscription>(op, sent) | |
| 224 | } | |
| Merge checks: statuses and check runs on every commit | 225 | Op::Checks(ChecksOp::CreateCommitStatus) => through::<work::CommitStatus>(op, sent), |
| 226 | Op::Checks(ChecksOp::ListCommitStatuses) => through::<Vec<work::CommitStatus>>(op, sent), | |
| 227 | Op::Checks(ChecksOp::GetCombinedStatus) => through::<checks::CombinedStatus>(op, sent), | |
| 228 | Op::Checks(ChecksOp::CreateCheckRun | ChecksOp::UpdateCheckRun | ChecksOp::GetCheckRun) => { | |
| 229 | through::<checks::CommitCheckRun>(op, sent) | |
| 230 | } | |
| 231 | Op::Checks(ChecksOp::ListCheckRunAnnotations) => through::<Vec<checks::CheckAnnotation>>(op, sent), | |
| 232 | Op::Checks(ChecksOp::ListCheckRunsForRef) => through::<checks::CheckRunList>(op, sent), | |
| 233 | Op::Checks(ChecksOp::ListCheckSuitesForRef) => through::<checks::CheckSuiteList>(op, sent), | |
| 234 | Op::Checks(ChecksOp::GetCheckSuite) => through::<checks::CommitCheckSuite>(op, sent), | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 235 | _ => sent, |
| 236 | } | |
| 237 | } | |
| 238 | ||
| 239 | /// Every key of `example`, as paths, outside the maps passed through. | |
| 240 | fn paths(value: &Value, path: &str, out: &mut Vec<String>) { | |
| 241 | match value { | |
| 242 | Value::Object(fields) => { | |
| 243 | for (key, value) in fields { | |
| 244 | let here = format!("{path}.{key}"); | |
| 245 | out.push(here.clone()); | |
| 246 | let user_keyed = value.is_object() | |
| 247 | && (USER_KEYED.contains(&key.as_str()) || key.starts_with("by_")); | |
| 248 | if !user_keyed { | |
| 249 | paths(value, &here, out); | |
| 250 | } | |
| 251 | } | |
| 252 | } | |
| 253 | Value::Array(items) => { | |
| 254 | for item in items { | |
| 255 | paths(item, &format!("{path}[]"), out); | |
| 256 | } | |
| 257 | } | |
| 258 | _ => {} | |
| 259 | } | |
| 260 | } | |
| 261 | ||
| 262 | #[test] | |
| 263 | fn no_route_answers_with_camel_case() { | |
| 264 | let document = document(); | |
| 265 | let (mut checked, mut converted) = (0, 0); | |
| 266 | for (path, methods) in document["paths"].as_object().unwrap() { | |
| 267 | for (method, operation) in methods.as_object().unwrap() { | |
| 268 | let example = &operation["responses"]["200"]["content"]["application/json"]["example"]; | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 269 | let tool = operation["x-operation"].as_str().unwrap_or_default(); |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 270 | let Some(op) = Op::by_name(tool) else { |
| 271 | // Device sign-in, which is written in `snake_case` by hand. | |
| 272 | assert!(wire::camel_case_keys(example).is_empty(), "{method} {path}"); | |
| 273 | continue; | |
| 274 | }; | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 275 | let mut sample = sample(op, example); |
| 276 | // Each person gets their chosen case on the way out (people.rs). | |
| 277 | crate::people::fill(&mut sample, &std::collections::HashMap::new()); | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 278 | converted += wire::camel_case_keys(&sample).len(); |
| 279 | let sent = wire::snake_case(sample); | |
| 280 | let leaked = wire::camel_case_keys(&sent); | |
| 281 | assert!(leaked.is_empty(), "{method} {path} sends {leaked:?}"); | |
| 282 | // The reference shows what is sent: each of its names is one. | |
| 283 | let (mut shown, mut real) = (Vec::new(), Vec::new()); | |
| 284 | paths(example, "", &mut shown); | |
| 285 | paths(&sent, "", &mut real); | |
| 286 | for name in shown { | |
| 287 | assert!(real.contains(&name), "{method} {path}: the reference shows {name}, which is not sent"); | |
| 288 | } | |
| 289 | checked += 1; | |
| 290 | } | |
| 291 | } | |
| 292 | assert!(checked >= Op::ALL.len()); | |
| 293 | // The samples are in the services' spelling, so there was something to | |
| 294 | // convert. | |
| 295 | assert!(converted > 100, "{converted}"); | |
| 296 | } | |
| 297 | ||
| 298 | #[test] | |
| 299 | fn every_route_has_a_sample() { | |
| 300 | let document = document(); | |
| 301 | for route in crate::rest::ROUTES { | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 302 | if route.no_content() { |
| 303 | continue; | |
| 304 | } | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 305 | let path = route |
| 306 | .path | |
| 307 | .split('/') | |
| 308 | .map(|segment| match segment.strip_prefix(':') { | |
| 309 | Some(name) => format!("{{{name}}}"), | |
| 310 | None => segment.to_owned(), | |
| 311 | }) | |
| 312 | .collect::<Vec<_>>() | |
| 313 | .join("/"); | |
| 314 | let example = &document["paths"][&path][route.method.to_lowercase()]["responses"]["200"] | |
| 315 | ["content"]["application/json"]["example"]; | |
| 316 | assert!(!example.is_null(), "{} {path}", route.method); | |
| 317 | } | |
| 318 | } | |
| 319 | ||
| 320 | #[test] | |
| 321 | fn errors_and_reports_are_snake_case() { | |
| 322 | let failure = g1t_contracts::Failure { | |
| 323 | code: g1t_contracts::FailureCode::NotFound, | |
| 324 | message: "No such endpoint.".to_owned(), | |
| 325 | }; | |
| 326 | assert!(wire::camel_case_keys(&wire::snake_case(json!({ "error": failure }))).is_empty()); | |
| 327 | } | |
| 328 | ||
| 329 | #[test] | |
| 330 | fn a_job_spec_keeps_github_s_spelling() { | |
| 331 | let spec = json!({ | |
| 332 | "job": "job_1", | |
| 333 | "spec": { "runs-on": "ubuntu-latest", "timeoutMinutes": 5 }, | |
| 334 | "workflow": { "env": { "nodeEnv": "x" } }, | |
| 335 | "github": { "eventName": "push", "headRef": "" }, | |
| 336 | "event": { "pull_request": { "headSha": "x" } }, | |
| 337 | "contexts": { "inputs": { "dryRun": true }, "matrix": { "nodeVersion": 20 } }, | |
| 338 | "checkout": { "ref": "main" }, | |
| 339 | "timeoutMinutes": 30, | |
| 340 | "masks": [], | |
| 341 | }); | |
| 342 | let sent = wire::snake_case_keeping(spec.clone(), crate::JOB_SPEC_AS_GIVEN); | |
| 343 | assert_eq!(sent["timeout_minutes"], 30); | |
| 344 | assert!(sent.get("timeoutMinutes").is_none()); | |
| 345 | for kept in ["spec", "workflow", "github", "event", "contexts", "checkout"] { | |
| 346 | assert_eq!(sent[kept], spec[kept], "{kept}"); | |
| 347 | } | |
| 348 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.