Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6f60bfbae | ||
|
|
a8a335eadb | ||
|
|
048e273ff9 | ||
|
|
beef2049ed | ||
|
|
c30592e6b6 | ||
|
|
abae81a0ce | ||
|
|
66a568818a | ||
|
|
f3d6146884 |
@@ -2916,17 +2916,16 @@ async fn mark_session_dead_with_worktree_settlement(
|
|||||||
// Release anyone attached; the scrollback survives for a later
|
// Release anyone attached; the scrollback survives for a later
|
||||||
// attach.
|
// attach.
|
||||||
record.bridge.seal();
|
record.bridge.seal();
|
||||||
let session = record.session.clone();
|
|
||||||
let worktree = record
|
let worktree = record
|
||||||
.worktree_base_repo
|
.worktree_base_repo
|
||||||
.clone()
|
.clone()
|
||||||
.zip(record.worktree_path.clone());
|
.zip(record.worktree_path.clone());
|
||||||
Some((session, worktree))
|
Some(worktree)
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let Some((mut session, worktree)) = flipped else {
|
let Some(worktree) = flipped else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let stream = inner
|
let stream = inner
|
||||||
@@ -2943,9 +2942,6 @@ async fn mark_session_dead_with_worktree_settlement(
|
|||||||
if settle_worktree {
|
if settle_worktree {
|
||||||
if let Some(wt) = &worktree {
|
if let Some(wt) = &worktree {
|
||||||
if settle_worktree_on_exit(inner, id, wt).await {
|
if settle_worktree_on_exit(inner, id, wt).await {
|
||||||
// The tree is settled (removed); a stale "in the way" would
|
|
||||||
// describe something the session no longer holds.
|
|
||||||
session.blocked_work = None;
|
|
||||||
let mut sessions = inner.sessions.lock().await;
|
let mut sessions = inner.sessions.lock().await;
|
||||||
if let Some(record) = sessions.get_mut(&id) {
|
if let Some(record) = sessions.get_mut(&id) {
|
||||||
record.session.worktree = None;
|
record.session.worktree = None;
|
||||||
@@ -2957,13 +2953,17 @@ async fn mark_session_dead_with_worktree_settlement(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Settle has now been applied or refused either way (#115): from here on
|
// Settle has now been applied or refused either way (#115): from here on
|
||||||
// its outcomes are safe to observe.
|
// its outcomes are safe to observe. Re-read the session from the record
|
||||||
{
|
// so the sidecar write includes any updates (e.g. a haiku rename) that
|
||||||
|
// landed between the initial clone and now.
|
||||||
|
let session = {
|
||||||
let mut sessions = inner.sessions.lock().await;
|
let mut sessions = inner.sessions.lock().await;
|
||||||
if let Some(record) = sessions.get_mut(&id) {
|
let Some(record) = sessions.get_mut(&id) else {
|
||||||
record.settled = true;
|
return false;
|
||||||
}
|
};
|
||||||
}
|
record.settled = true;
|
||||||
|
record.session.clone()
|
||||||
|
};
|
||||||
|
|
||||||
let _ = inner.sidecar.write(&session);
|
let _ = inner.sidecar.write(&session);
|
||||||
let _ = inner.events.send(CatalogEvent::SessionUpdated { session });
|
let _ = inner.events.send(CatalogEvent::SessionUpdated { session });
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ use common::TmuxFixture;
|
|||||||
use maestro_daemon::sidecar::SidecarStore;
|
use maestro_daemon::sidecar::SidecarStore;
|
||||||
use maestro_daemon::worktree::WorktreeManager;
|
use maestro_daemon::worktree::WorktreeManager;
|
||||||
use maestro_daemon::SessionManager;
|
use maestro_daemon::SessionManager;
|
||||||
use maestro_protocol::{display, CatalogEvent, CreateSessionRequest, NameSource};
|
use maestro_protocol::{display, CatalogEvent, CreateSessionRequest, NameSource, SessionStatus};
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
struct Fixture {
|
struct Fixture {
|
||||||
manager: SessionManager,
|
manager: SessionManager,
|
||||||
sidecar: SidecarStore,
|
sidecar: SidecarStore,
|
||||||
_tmux: TmuxFixture,
|
tmux: TmuxFixture,
|
||||||
_state_dir: TempDir,
|
_state_dir: TempDir,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ fn fixture() -> Fixture {
|
|||||||
Fixture {
|
Fixture {
|
||||||
manager,
|
manager,
|
||||||
sidecar,
|
sidecar,
|
||||||
_tmux: tmux,
|
tmux,
|
||||||
_state_dir: state_dir,
|
_state_dir: state_dir,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,3 +171,37 @@ async fn a_blank_name_changes_nothing() {
|
|||||||
let live = fx.sidecar.read(id).unwrap().unwrap();
|
let live = fx.sidecar.read(id).unwrap().unwrap();
|
||||||
assert_eq!(display::name_source(&live), NameSource::Generated);
|
assert_eq!(display::name_source(&live), NameSource::Generated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A session that dies naturally (liveness watcher marks it dead) must not
|
||||||
|
/// overwrite a haiku rename that landed before the death. Regression test for
|
||||||
|
/// the stale-clone sidecar write in `mark_session_dead`.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn session_death_preserves_the_haiku_name() {
|
||||||
|
let fx = fixture();
|
||||||
|
let id = spawn(&fx.manager, None).await;
|
||||||
|
|
||||||
|
fx.manager.claim_autoname(id).await.unwrap();
|
||||||
|
fx.manager.apply_autoname(id, "haiku-name").await.unwrap();
|
||||||
|
|
||||||
|
// Verify the sidecar has Haiku before the session dies.
|
||||||
|
let before = fx.sidecar.read(id).unwrap().unwrap();
|
||||||
|
assert_eq!(display::name_source(&before), NameSource::Haiku);
|
||||||
|
|
||||||
|
// Kill the tmux session directly so the liveness watcher picks it up.
|
||||||
|
fx.tmux.tmux.kill_session(id).expect("kill tmux directly");
|
||||||
|
fx.manager
|
||||||
|
.start_liveness_watcher(std::time::Duration::from_millis(25));
|
||||||
|
|
||||||
|
common::wait_for_async(std::time::Duration::from_secs(3), || async {
|
||||||
|
fx.manager.status(id).await == Some(SessionStatus::Dead)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let after = fx.sidecar.read(id).unwrap().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
display::name_source(&after),
|
||||||
|
NameSource::Haiku,
|
||||||
|
"mark_session_dead must not overwrite a haiku rename in the sidecar"
|
||||||
|
);
|
||||||
|
assert_eq!(display::custom_name(&after), Some("haiku-name"));
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ async fn embedded_index_is_real_spa_not_placeholder() {
|
|||||||
let html = String::from_utf8(body.to_vec()).unwrap();
|
let html = String::from_utf8(body.to_vec()).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
html.contains(r#"id="app""#),
|
html.contains(r#"id="root""#),
|
||||||
"served index is missing the SPA mount point; got:\n{html}"
|
"served index is missing the SPA mount point; got:\n{html}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!html.contains("maestro is running"),
|
!html.contains("maestro is running"),
|
||||||
"served index is the placeholder, not the built bundle"
|
"served index is the placeholder, not the built bundle"
|
||||||
);
|
);
|
||||||
// Stronger signal than `id="app"` (which appears in both source and built
|
// Stronger signal than `id="root"` (which appears in both source and built
|
||||||
// HTML): the vite build rewrites the entry to a hashed `/assets/index-<hash>`
|
// HTML): the vite build rewrites the entry to a hashed `/assets/index-<hash>`
|
||||||
// bundle, whereas the un-built source references `/src/main.ts`. This catches
|
// bundle, whereas the un-built source references `/src/main.ts`. This catches
|
||||||
// both an empty/placeholder embed and the staleness case.
|
// both an empty/placeholder embed and the staleness case.
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ async fn root_serves_the_spa_shell() {
|
|||||||
.map(|v| v.starts_with("text/html")),
|
.map(|v| v.starts_with("text/html")),
|
||||||
Some(true)
|
Some(true)
|
||||||
);
|
);
|
||||||
assert!(resp.text().await.unwrap().contains(r#"id="app""#));
|
assert!(resp.text().await.unwrap().contains(r#"id="root""#));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -69,7 +69,7 @@ async fn client_side_route_falls_back_to_the_shell() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(resp.status(), 200);
|
assert_eq!(resp.status(), 200);
|
||||||
assert!(resp.text().await.unwrap().contains(r#"id="app""#));
|
assert!(resp.text().await.unwrap().contains(r#"id="root""#));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An unmatched API path must 404 rather than fall through to the SPA shell:
|
/// An unmatched API path must 404 rather than fall through to the SPA shell:
|
||||||
|
|||||||
@@ -1093,7 +1093,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-color: color-mix(in oklab, var(--danger) 35%, var(--border));
|
border-color: transparent;
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1287,6 +1287,7 @@
|
|||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
align-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.structured-attach:hover {
|
.structured-attach:hover {
|
||||||
|
|||||||
Reference in New Issue
Block a user