fix(delivery): four failures from the #55 trace — auth, prompts, truncation, retry
All four were surfaced while tracing #55 and left open. Each one on its own is
small; together they are why a two-line git rejection took hours to read.
**1. `with_ambient_auth` failed open.** It matched one literal prefix,
`https://git.redclaw.dev/`, and returned the URL unchanged for everything else
with no log line. An `http://` remote, an explicit port, a different case in the
host, an ssh remote, a URL that already carried userinfo — all came back
unauthenticated and looked identical to success. It now returns `Authed`, which
carries the URL AND why no credential reached it, and recognises the forge in
every shape a remote can be written (host parsed with userinfo stripped BEFORE
the port, or `oauth2:token@host` reports its username as the host — the first
version of this function did exactly that and failed its own test).
**2. Nothing set `GIT_TERMINAL_PROMPT=0`.** So a credential-less URL did not
fail — git opened `/dev/tty`, and in a server container that surfaces as
`No such device or address`, several layers from the missing token. Now set on
every git invocation that can reach the network. And `push_url_for` refuses
outright when the URL is on OUR forge and unauthenticated: that push cannot
succeed, and letting it proceed only buys a symptom that looks like something
else.
**3. The truncation fix went to the wrong path.** e31688b clamped the caller,
but a rejected push comes back as `Ok(Publish { error })` — the string was
already cut to 300 head chars inside `git()`, so the reject reason had been
dropped before the both-ends clamp ever saw it. Clamped where the output is
produced, and redacted there too.
**4. #55: a mission that re-clones can never push.** The branch name is
deterministic per (mission, phase, iteration), so a checkout rebuilt after a
retry, a container teardown or disk loss produces divergent history against its
own branch, and git rejects it — leaving the work on a local branch in a
directory the sweeper deletes. Reachable in normal operation, not just by
deleting a checkout by hand.
The escape is a NEW ref, not `--force`: forcing would overwrite whatever the
earlier attempt pushed, which may be the only copy of that work, to make this
attempt look tidy. The retry lands on `<branch>-<sha8>` — deterministic,
self-describing in a branch list, and collision-free since divergent history is
by definition a different sha. "Never force" stays a rule.
NEGATIVE CONTROL, run rather than assumed: with the rescue arm disabled,
`diverged_history_lands_on_a_new_branch_instead_of_being_lost` FAILS with git's
real `! [rejected] ... (fetch first)` — which also demonstrates fix 3, since that
whole message now survives to the assertion. The test asserts the earlier
attempt's ref is byte-identical afterwards.
Also measured, not read off docs: which hooks fire under `claude -p` (2.1.222,
via `--settings`). SessionStart, UserPromptSubmit, PreToolUse, PostToolUse,
SubagentStop and Stop fire; TaskCreated, TaskCompleted, TeammateIdle, SessionEnd,
Notification and PreCompact do not. So the agent-teams hooks Slice 3 deferred are
inert on our path by construction, and `Stop` is the seam that could move
`done_when` into the agent's own loop.
507 tests pass, clippy clean.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
12147a1e01
commit
1d554396f4
@@ -564,6 +564,109 @@ async fn a_failed_gate_publishes_to_a_wip_branch() {
|
||||
);
|
||||
}
|
||||
|
||||
/// #55: a mission whose checkout was re-cloned builds divergent history against
|
||||
/// its OWN deterministic branch, and git rejects every push it will ever make.
|
||||
/// That was terminal — the work stayed on a local branch in a directory that
|
||||
/// gets reaped — and it is reachable from a retry, a container teardown, or disk
|
||||
/// loss, not just from someone deleting a checkout by hand.
|
||||
#[tokio::test]
|
||||
async fn diverged_history_lands_on_a_new_branch_instead_of_being_lost() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let remote = tmp.path().join("remote.git");
|
||||
std::fs::create_dir_all(&remote).unwrap();
|
||||
Command::new("git")
|
||||
.args(["init", "--bare", "--quiet"])
|
||||
.arg(&remote)
|
||||
.output()
|
||||
.unwrap();
|
||||
let branch = "clawmates/mission-test-dddddddd";
|
||||
let url = remote.to_str().unwrap();
|
||||
|
||||
// The first attempt: a checkout that pushed its work and then vanished.
|
||||
let first = seed_repo(tmp.path(), Uuid::now_v7());
|
||||
std::fs::write(first.join("first.rs"), "fn first() {}\n").unwrap();
|
||||
git(&first, &["add", "."]);
|
||||
git(&first, &["commit", "--quiet", "-m", "first pass"]);
|
||||
git(&first, &["checkout", "-B", branch]);
|
||||
let one = mission_delivery::publish_phase_branch(
|
||||
&first,
|
||||
url,
|
||||
branch,
|
||||
mission_delivery::Gate::Always,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(one.pushed, "setup push failed: {:?}", one.error);
|
||||
let claimed = Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(&remote)
|
||||
.args(["rev-parse", branch])
|
||||
.output()
|
||||
.unwrap();
|
||||
let claimed = String::from_utf8_lossy(&claimed.stdout).trim().to_string();
|
||||
|
||||
// The retry: a fresh clone of the same mission, so unrelated history under
|
||||
// the same deterministic branch name.
|
||||
let second = seed_repo(tmp.path(), Uuid::now_v7());
|
||||
std::fs::write(second.join("second.rs"), "fn second() {}\n").unwrap();
|
||||
git(&second, &["add", "."]);
|
||||
git(&second, &["commit", "--quiet", "-m", "retry"]);
|
||||
git(&second, &["checkout", "-B", branch]);
|
||||
|
||||
let out = mission_delivery::publish_phase_branch(
|
||||
&second,
|
||||
url,
|
||||
branch,
|
||||
mission_delivery::Gate::Always,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
out.pushed,
|
||||
"the retry's work never reached the forge: {:?}",
|
||||
out.error
|
||||
);
|
||||
assert!(
|
||||
out.branch.starts_with(branch) && out.branch != branch,
|
||||
"it must land on a NEW ref, not the contested one: {}",
|
||||
out.branch
|
||||
);
|
||||
|
||||
let refs = Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(&remote)
|
||||
.args(["for-each-ref", "--format=%(refname:short)"])
|
||||
.output()
|
||||
.unwrap();
|
||||
let refs = String::from_utf8_lossy(&refs.stdout);
|
||||
assert!(refs.contains(&out.branch), "refs: {refs}");
|
||||
|
||||
// NEVER force: the first attempt's ref still points where it did. Losing it
|
||||
// to make this push look tidy would trade one lost copy of the work for
|
||||
// another.
|
||||
let still = Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(&remote)
|
||||
.args(["rev-parse", branch])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&still.stdout).trim(),
|
||||
claimed,
|
||||
"the earlier attempt's branch was overwritten"
|
||||
);
|
||||
let show = Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(&remote)
|
||||
.args(["show", &format!("{}:second.rs", out.branch)])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(String::from_utf8_lossy(&show.stdout).contains("fn second()"));
|
||||
}
|
||||
|
||||
/// An unreachable remote is a degraded success, not a failure: the patch and
|
||||
/// the local branch both still exist.
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user