fix(daemon): contain failed target sessions
Shut down bootstrap sockets on every dispatcher-side failure and reject frontend events whose session state was never installed. This prevents a lingering failed client from reaching absent render/size state. Track target-side CRDT upgrades independently from load/create status so a deduplicated hidden buffer is published to every existing grid replica. Add real-daemon regressions for both failure containment and replica publication.
This commit is contained in:
parent
65e500eba1
commit
be8c67c30c
|
|
@ -63,19 +63,19 @@ If it does not, stop and repair the remote/fetch configuration.
|
|||
- Original implementation base: canonical `githubsucks/main` @ `c49a8c7`
|
||||
(folding Stage 1 #142); current canonical `main` @ `47581f4` is integrated
|
||||
conflict-free by merge `d6d4be6`. Protocol was v19 before this work.
|
||||
- State: implementation checkpoint `2dd30ec`; review-fix checkpoint `bef1c08`.
|
||||
Integrated, smoke-tested, fully gated, and published on 2026-07-23 at
|
||||
protocol v20. PR #148 remains open for user review:
|
||||
`https://github.com/levineuwirth/pmacs/pull/148`.
|
||||
- State: implementation checkpoint `2dd30ec`; first review-fix checkpoint
|
||||
`bef1c08`. Second-review fixes are complete locally, smoke-tested, and fully
|
||||
gated at protocol v20; publication is next. PR #148 remains open for user
|
||||
review: `https://github.com/levineuwirth/pmacs/pull/148`.
|
||||
- Scope delivered: one session-scoped `pmacs --gpu [--socket …] FILE` target,
|
||||
protocol-v20 semantic bootstrap, launcher-owned tilde/cwd resolution, exact
|
||||
Unix path transport, pre-window target readiness, replica coherence, and the
|
||||
approved behavioral acceptance matrix.
|
||||
- Post-review verification: formatting and strict Clippy; 1,800 default + 1,977
|
||||
CRDT library tests; target gate 1 default + 13 CRDT; M4 121; required GPU 152;
|
||||
Vterm Stage 3 5 default + 7 CRDT; isolated-config workspace sweep 3,269 across
|
||||
87 suites. Two concurrent real Wayland/Vulkan GPU windows remained on distinct
|
||||
target buffers after the second attach.
|
||||
- Second-review verification: formatting and strict Clippy; 1,801 default +
|
||||
1,978 CRDT library tests; target gate 1 default + 14 CRDT; M4 121; required
|
||||
GPU 152; Vterm Stage 3 5 default + 7 CRDT; isolated-config workspace sweep
|
||||
3,272 across 87 suites. The prior two-window Wayland/Vulkan isolation smoke
|
||||
remains valid; this round changes only daemon failure/publication behavior.
|
||||
- Deferred unchanged: automatic GUI selection, multiple files, general
|
||||
live-open commands, packaging, and remote GPU paths.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
# Agent handoff — cross-machine continuity
|
||||
|
||||
**Last updated: 2026-07-23, after GPU initial-target PR #148 review fixes and
|
||||
verification completed on branch `gpu-initial-target` (protocol v20), following
|
||||
one-command GPU invocation (#141), the documentation refresh (#140), Vterm
|
||||
Stage 3 (#135), tab-width rendering parity (#137), locals-query processing
|
||||
(#134), modeline detection (#132), mode system wiring (#129), config registry
|
||||
(#127), Vterm Stages 1–2 (#126/#130), and completed Themes Arc 4
|
||||
(#120/#124/#125).**
|
||||
**Last updated: 2026-07-24, after GPU initial-target PR #148 second-review fixes
|
||||
and verification completed on branch `gpu-initial-target` (protocol v20),
|
||||
following one-command GPU invocation (#141), the documentation refresh (#140),
|
||||
Vterm Stage 3 (#135), tab-width rendering parity (#137), locals-query
|
||||
processing (#134), modeline detection (#132), mode system wiring (#129),
|
||||
config registry (#127), Vterm Stages 1–2 (#126/#130), and completed Themes
|
||||
Arc 4 (#120/#124/#125).**
|
||||
This file is the
|
||||
bridge between development machines. If you are an agent reading
|
||||
this on a fresh clone: this document plus the `docs/*-framing.md`
|
||||
|
|
@ -18,7 +18,7 @@ reads it the way you just did.
|
|||
For volatile branches, checkpoints, verification, and recovery
|
||||
commands, read `docs/active-work.md` immediately after this file.
|
||||
|
||||
## 1. Where the project stands (2026-07-23)
|
||||
## 1. Where the project stands (2026-07-24)
|
||||
|
||||
- `main` @ `47581f4` (web grammars #146 atop folding Stage 1 #142,
|
||||
inline-math framing #145, and one-command GPU invocation #141), protocol
|
||||
|
|
@ -31,14 +31,16 @@ commands, read `docs/active-work.md` immediately after this file.
|
|||
appended `InitialTargetResult` readiness barrier; v6–v19 wire encodings stay
|
||||
pinned. The daemon resolves the path lexically, deduplicates or loads/creates
|
||||
it in the authenticated frontend's view, runs the established load/switch
|
||||
hooks, upgrades the buffer for CRDT, publishes fresh buffers to existing grid
|
||||
replicas, and sends the target snapshot before readiness. Semantic replicas
|
||||
receive a publication only when displaying that buffer, so a second target
|
||||
launch cannot switch an existing GPU window; one dead peer cannot fail the
|
||||
new session. Failed bootstrap removes the provisional session and restores
|
||||
the ambient active frontend without poisoning the daemon. Existing no-target
|
||||
managed launch, direct attach, TUI, and legacy protocol behavior remain
|
||||
intact. See `docs/active-work.md` for the portable checkpoint and verification.
|
||||
hooks, upgrades the buffer for CRDT, and publishes every target-side CRDT
|
||||
upgrade to existing grid replicas before readiness. Semantic replicas receive
|
||||
a publication only when displaying that buffer, so a second target launch
|
||||
cannot switch an existing GPU window; one dead peer cannot fail the new
|
||||
session. Failed bootstrap writes a bounded result, shuts down the socket,
|
||||
removes provisional state, and restores the ambient active frontend. Any
|
||||
stale event from an uninstalled session is dropped before state access.
|
||||
Existing no-target managed launch, direct attach, TUI, and legacy protocol
|
||||
behavior remain intact. See `docs/active-work.md` for the portable checkpoint
|
||||
and verification.
|
||||
- **One-command GPU invocation LANDED — #141**
|
||||
(`docs/gpu-invocation-framing.md` rev 6; merge `63fbc66`; two implementation
|
||||
reviews). The additive public path is `pmacs --gpu [--socket NAME|PATH]`;
|
||||
|
|
|
|||
|
|
@ -230,7 +230,8 @@ pub struct InitialTarget {
|
|||
are the authority.
|
||||
- `cwd` and `path` are Unix path bytes, not text. This stage is the local Unix-
|
||||
socket GPU path; it does not claim a cross-platform/remote path protocol.
|
||||
- Each field is bounded to 32 KiB before allocation/use. `path` must be
|
||||
- Postcard decodes each field under the transport's 16 MiB frame cap; daemon
|
||||
validation then bounds each to 32 KiB before filesystem use. `path` must be
|
||||
nonempty, `cwd` must be nonempty and absolute, and embedded NUL is rejected
|
||||
with a bootstrap failure.
|
||||
|
||||
|
|
@ -550,9 +551,11 @@ process behavior.
|
|||
status/path identity, accepts an edit/save through the real session, and
|
||||
creates the requested file under the launcher cwd—not the daemon cwd.
|
||||
10. **Open error:** a directory/permission-denied target returns a specific
|
||||
failure before ready/window creation and makes root fail. An existing daemon
|
||||
remains connectable; a pre-existing frontend's active buffer and contents
|
||||
remain unchanged.
|
||||
failure before ready/window creation and makes root fail. The daemon shuts
|
||||
down that failed session's socket; a client that lingers or sends another
|
||||
event cannot reach uninstalled session state. An existing daemon remains
|
||||
connectable; a pre-existing frontend's active buffer and contents remain
|
||||
unchanged.
|
||||
11. **Dedup preserves unsaved edits:** frontend A opens and modifies a file
|
||||
without saving; target-launch frontend B opens the same normalized path and
|
||||
receives A's authoritative unsaved text with the same `BufferId`, not disk
|
||||
|
|
@ -561,10 +564,11 @@ process behavior.
|
|||
target launches B and C open different files. Each result/snapshot pair
|
||||
names its own view; a subsequent input/resize proof shows A, B, and C remain
|
||||
independently usable on their original buffers.
|
||||
13. **Fresh-buffer publication:** keep replica A attached, then target-launch B
|
||||
onto a previously unknown file. A receives the new buffer snapshot before
|
||||
any CRDT op for it; both replicas accept later operations without unknown-
|
||||
buffer fallback or disconnect.
|
||||
13. **Replica publication after target upgrade:** keep grid replica A attached,
|
||||
then target-launch B onto a previously unknown file. Repeat for a file that
|
||||
Lua loaded into a hidden, not-yet-CRDT-backed buffer before the target dedup.
|
||||
A receives each buffer snapshot before any CRDT op for it; both replicas
|
||||
accept later operations without unknown-buffer fallback or disconnect.
|
||||
14. **Hook context and count:** fresh disk load fires `buffer.after-load` once;
|
||||
dedup fires `buffer.after-switch` once even when the fresh view already
|
||||
shares that exact buffer and the select itself is a no-op; missing-file
|
||||
|
|
@ -617,20 +621,20 @@ Vterm Stage 3 acceptance in default and CRDT configurations where the suite
|
|||
supports both. The final full workspace sweep remains required before PR.
|
||||
|
||||
The named gate intentionally reuses the managed-lifecycle acceptance module,
|
||||
so a workspace sweep executes those 13 CRDT cases under both test-binary
|
||||
so a workspace sweep executes those 14 CRDT cases under both test-binary
|
||||
names. The duplicate runtime is retained to keep the approved named command
|
||||
and the complete #141 lifecycle fixture coverage together.
|
||||
|
||||
Post-review verification on 2026-07-23:
|
||||
Post-second-review verification on 2026-07-24:
|
||||
|
||||
- `cargo fmt --check` and strict workspace Clippy passed.
|
||||
- Library gates passed 1,800 default and 1,977 CRDT tests.
|
||||
- The named initial-target gate passed 1 default and 13 CRDT tests; the
|
||||
underlying GPU invocation suite passed 13 CRDT tests.
|
||||
- Library gates passed 1,801 default and 1,978 CRDT tests.
|
||||
- The named initial-target gate passed 1 default and 14 CRDT tests; the
|
||||
underlying GPU invocation suite passed 14 CRDT tests.
|
||||
- M4 passed 121 tests with the documented basedpyright skip; required real-GPU
|
||||
tests passed 152.
|
||||
- Vterm Stage 3 passed 5 default and 7 CRDT tests.
|
||||
- The isolated-config workspace CRDT sweep passed 3,269 tests across 87 suites,
|
||||
- The isolated-config workspace CRDT sweep passed 3,272 tests across 87 suites,
|
||||
with 29 ignored and the documented basedpyright case filtered.
|
||||
- A coherent release build launched two concurrent real Wayland/Vulkan GPU
|
||||
windows on one daemon, targeting distinct `alpha` and `beta` files. Both
|
||||
|
|
|
|||
103
src/daemon.rs
103
src/daemon.rs
|
|
@ -51,6 +51,7 @@
|
|||
use std::collections::HashMap;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::io::ErrorKind;
|
||||
use std::net::Shutdown;
|
||||
use std::os::unix::ffi::{OsStrExt, OsStringExt};
|
||||
use std::os::unix::net::{UnixListener, UnixStream};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
|
@ -655,6 +656,7 @@ fn send_initial_target_failure(stream: &mut UnixStream, message: impl Into<Strin
|
|||
message: bounded_initial_target_error(message.into()),
|
||||
};
|
||||
let _ = write_message(stream, &InstanceMessage::InitialTargetResult(result));
|
||||
let _ = stream.shutdown(Shutdown::Both);
|
||||
}
|
||||
|
||||
fn validate_initial_target(target: &InitialTarget) -> Result<(), String> {
|
||||
|
|
@ -1604,6 +1606,11 @@ struct OpenedInitialTarget {
|
|||
publish_to_replicas: bool,
|
||||
}
|
||||
|
||||
struct InitialTargetSnapshot {
|
||||
crdt_snapshot: Vec<u8>,
|
||||
upgraded_to_crdt: bool,
|
||||
}
|
||||
|
||||
fn resolve_initial_target(target: InitialTarget) -> PathBuf {
|
||||
let cwd = PathBuf::from(OsString::from_vec(target.cwd));
|
||||
let path = PathBuf::from(OsString::from_vec(target.path));
|
||||
|
|
@ -1674,30 +1681,35 @@ fn open_initial_target(
|
|||
fn initial_target_snapshot(
|
||||
editor: &EditorState,
|
||||
buffer_id: crate::buffer::BufferId,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
) -> Result<InitialTargetSnapshot, String> {
|
||||
let core = editor.core.borrow();
|
||||
let mut registry = core.registry.borrow_mut();
|
||||
let buffer = registry
|
||||
.get_mut(buffer_id)
|
||||
.map_err(|error| format!("initial target buffer disappeared: {error}"))?;
|
||||
if !buffer.is_crdt_backed() {
|
||||
let upgraded_to_crdt = !buffer.is_crdt_backed();
|
||||
if upgraded_to_crdt {
|
||||
let peer_id = crate::crdt::peer_id_from_frontend(FrontendId::LOCAL);
|
||||
buffer
|
||||
.upgrade_to_crdt(peer_id)
|
||||
.map_err(|error| format!("initial target CRDT upgrade failed: {error:?}"))?;
|
||||
}
|
||||
buffer
|
||||
let crdt_snapshot = buffer
|
||||
.crdt_state()
|
||||
.ok_or_else(|| "initial target CRDT state is unavailable".to_owned())?
|
||||
.export_snapshot()
|
||||
.map_err(|error| format!("initial target snapshot export failed: {error:?}"))
|
||||
.map_err(|error| format!("initial target snapshot export failed: {error:?}"))?;
|
||||
Ok(InitialTargetSnapshot {
|
||||
crdt_snapshot,
|
||||
upgraded_to_crdt,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "crdt"))]
|
||||
fn initial_target_snapshot(
|
||||
_editor: &EditorState,
|
||||
_buffer_id: crate::buffer::BufferId,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
) -> Result<InitialTargetSnapshot, String> {
|
||||
Err("initial target requires a CRDT-enabled daemon".to_owned())
|
||||
}
|
||||
|
||||
|
|
@ -1714,7 +1726,9 @@ fn cleanup_provisional_session(
|
|||
) {
|
||||
render_states.remove(&frontend_id);
|
||||
semantic_states.remove(&frontend_id);
|
||||
streams.remove(&frontend_id);
|
||||
if let Some(stream) = streams.remove(&frontend_id) {
|
||||
let _ = stream.shutdown(Shutdown::Both);
|
||||
}
|
||||
term_sizes.remove(&frontend_id);
|
||||
last_active_buffer_sent.remove(&frontend_id);
|
||||
session_registry.unregister_session(frontend_id);
|
||||
|
|
@ -1770,7 +1784,7 @@ fn handle_session_established(
|
|||
let negotiated_protocol_version = session_state.negotiated_protocol_version;
|
||||
|
||||
if let Some(opened) = opened_target.as_ref() {
|
||||
let snapshot = match initial_target_snapshot(editor, opened.buffer_id) {
|
||||
let target_snapshot = match initial_target_snapshot(editor, opened.buffer_id) {
|
||||
Ok(snapshot) => snapshot,
|
||||
Err(message) => {
|
||||
send_initial_target_failure(&mut write_stream, message);
|
||||
|
|
@ -1783,9 +1797,9 @@ fn handle_session_established(
|
|||
};
|
||||
let snapshot_message = InstanceMessage::BufferSnapshot {
|
||||
buffer_id: opened.buffer_id,
|
||||
crdt_snapshot: snapshot,
|
||||
crdt_snapshot: target_snapshot.crdt_snapshot,
|
||||
};
|
||||
if opened.publish_to_replicas {
|
||||
if opened.publish_to_replicas || target_snapshot.upgraded_to_crdt {
|
||||
publish_buffer_snapshot_to_replicas(
|
||||
editor,
|
||||
opened.buffer_id,
|
||||
|
|
@ -1796,6 +1810,7 @@ fn handle_session_established(
|
|||
);
|
||||
}
|
||||
if write_message(&mut write_stream, &snapshot_message).is_err() {
|
||||
let _ = write_stream.shutdown(Shutdown::Both);
|
||||
editor
|
||||
.core
|
||||
.borrow_mut()
|
||||
|
|
@ -1888,6 +1903,10 @@ fn handle_dispatcher_event(
|
|||
);
|
||||
}
|
||||
DispatcherEvent::FrontendEvent { source, event } => {
|
||||
if session_registry.session_state(source).is_none() {
|
||||
eprintln!("pmacs: dropping frontend event from uninstalled session {source:?}");
|
||||
return;
|
||||
}
|
||||
match event {
|
||||
FrontendEvent::Detach(_) => {
|
||||
// The per-attach thread will follow up with a
|
||||
|
|
@ -2102,9 +2121,12 @@ fn handle_dispatcher_event(
|
|||
}
|
||||
}
|
||||
_ => {
|
||||
let term_size = *term_sizes
|
||||
.get(&source)
|
||||
.expect("term_size present for source");
|
||||
let Some(&term_size) = term_sizes.get(&source) else {
|
||||
eprintln!(
|
||||
"pmacs: dropping frontend event without size state for {source:?}"
|
||||
);
|
||||
return;
|
||||
};
|
||||
let mut term_size = term_size;
|
||||
if let Some(render_state) = render_states.get_mut(&source) {
|
||||
apply_event(editor, source, event, &mut term_size, render_state);
|
||||
|
|
@ -2123,10 +2145,8 @@ fn handle_dispatcher_event(
|
|||
// nothing before B1.)
|
||||
apply_semantic_input_event(editor, source, event, term_size);
|
||||
} else {
|
||||
debug_assert!(
|
||||
false,
|
||||
"fid with neither a render_state nor a semantic_state \
|
||||
sent a frontend event"
|
||||
eprintln!(
|
||||
"pmacs: dropping frontend event without render state for {source:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -3168,7 +3188,6 @@ mod tests {
|
|||
let semantic = crate::protocol::NegotiatedCapabilities {
|
||||
multi_frontend: true,
|
||||
crdt_replica: true,
|
||||
|
||||
semantic_render: true,
|
||||
};
|
||||
let old_peer = FrontendId(2);
|
||||
|
|
@ -3263,6 +3282,44 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frontend_events_from_uninstalled_sessions_are_dropped_without_state_access() {
|
||||
let source = FrontendId(77);
|
||||
let mut editor = EditorState::new();
|
||||
let mut render_states = HashMap::new();
|
||||
let mut semantic_states = HashMap::new();
|
||||
let mut streams = HashMap::new();
|
||||
let mut term_sizes = HashMap::new();
|
||||
let mut last_dispatch_idle_sent = HashMap::new();
|
||||
let mut last_active_buffer_sent = HashMap::new();
|
||||
let mut terminal_bell_baselines = HashMap::new();
|
||||
let mut session_registry = SessionRegistry::new();
|
||||
|
||||
handle_dispatcher_event(
|
||||
DispatcherEvent::FrontendEvent {
|
||||
source,
|
||||
event: FrontendEvent::Key(crate::protocol::KeyEvent {
|
||||
frontend_id: source,
|
||||
key: crate::protocol::Key::Char('x'),
|
||||
mods: crate::protocol::Modifiers::NONE,
|
||||
timestamp_ns: 0,
|
||||
}),
|
||||
},
|
||||
&mut editor,
|
||||
&mut render_states,
|
||||
&mut semantic_states,
|
||||
&mut streams,
|
||||
&mut term_sizes,
|
||||
&mut last_dispatch_idle_sent,
|
||||
&mut last_active_buffer_sent,
|
||||
&mut terminal_bell_baselines,
|
||||
&mut session_registry,
|
||||
);
|
||||
|
||||
assert_eq!(editor.core.borrow().active_frontend, FrontendId::LOCAL);
|
||||
assert!(term_sizes.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_identity_includes_version_and_uptime() {
|
||||
let s = DaemonState::new(Some("research".into()));
|
||||
|
|
@ -3654,6 +3711,18 @@ mod tests {
|
|||
let mut last_active_buffer_sent = HashMap::new();
|
||||
let mut terminal_bell_baselines = HashMap::new();
|
||||
let mut session_registry = SessionRegistry::new();
|
||||
session_registry.register_session(
|
||||
source,
|
||||
crate::presence::SessionState::new(
|
||||
PROTOCOL_VERSION,
|
||||
crate::protocol::NegotiatedCapabilities {
|
||||
multi_frontend: true,
|
||||
crdt_replica: false,
|
||||
semantic_render: false,
|
||||
},
|
||||
0,
|
||||
),
|
||||
);
|
||||
|
||||
handle_dispatcher_event(
|
||||
DispatcherEvent::FrontendEvent {
|
||||
|
|
|
|||
|
|
@ -281,7 +281,11 @@ mod crdt {
|
|||
}
|
||||
}
|
||||
|
||||
fn request_raw_target(socket: &Path, cwd: Vec<u8>, path: Vec<u8>) -> Vec<InstanceMessage> {
|
||||
fn open_raw_target(
|
||||
socket: &Path,
|
||||
cwd: Vec<u8>,
|
||||
path: Vec<u8>,
|
||||
) -> (FrontendId, UnixStream, Vec<InstanceMessage>) {
|
||||
let mut stream = UnixStream::connect(socket).expect("connect raw target frontend");
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_secs(5)))
|
||||
|
|
@ -310,14 +314,19 @@ mod crdt {
|
|||
.expect("send raw target");
|
||||
|
||||
let first = read_message::<InstanceMessage>(&mut stream).expect("raw target result");
|
||||
if matches!(first, InstanceMessage::BufferSnapshot { .. }) {
|
||||
let messages = if matches!(first, InstanceMessage::BufferSnapshot { .. }) {
|
||||
vec![
|
||||
first,
|
||||
read_message::<InstanceMessage>(&mut stream).expect("raw opened result"),
|
||||
]
|
||||
} else {
|
||||
vec![first]
|
||||
};
|
||||
(hello.assigned_frontend_id, stream, messages)
|
||||
}
|
||||
|
||||
fn request_raw_target(socket: &Path, cwd: Vec<u8>, path: Vec<u8>) -> Vec<InstanceMessage> {
|
||||
open_raw_target(socket, cwd, path).2
|
||||
}
|
||||
|
||||
fn spawn_daemon(socket: &Path, envs: &[(&str, &str)]) -> Child {
|
||||
|
|
@ -666,8 +675,8 @@ mod crdt {
|
|||
(cwd.clone(), vec![b'x'; 32 * 1024 + 1]),
|
||||
(cwd.clone(), b".".to_vec()),
|
||||
];
|
||||
for (bad_cwd, bad_path) in invalid {
|
||||
let messages = request_raw_target(&socket, bad_cwd, bad_path);
|
||||
for (index, (bad_cwd, bad_path)) in invalid.into_iter().enumerate() {
|
||||
let (frontend_id, mut stream, messages) = open_raw_target(&socket, bad_cwd, bad_path);
|
||||
assert_eq!(messages.len(), 1, "failure must send no snapshot");
|
||||
match &messages[0] {
|
||||
InstanceMessage::InitialTargetResult(InitialTargetResult::Failed { message }) => {
|
||||
|
|
@ -676,8 +685,20 @@ mod crdt {
|
|||
}
|
||||
other => panic!("expected bounded target failure, got {other:?}"),
|
||||
}
|
||||
assert!(
|
||||
read_message::<InstanceMessage>(&mut stream).is_err(),
|
||||
"failed bootstrap {index} must close its socket"
|
||||
);
|
||||
let _ = write_message(
|
||||
&mut stream,
|
||||
&FrontendEvent::Key(pmacs::protocol::KeyEvent {
|
||||
frontend_id,
|
||||
key: pmacs::protocol::Key::Char('x'),
|
||||
mods: pmacs::protocol::Modifiers::NONE,
|
||||
timestamp_ns: 0,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
fs::write(temp.path().join("still-alive.txt"), "alive\n").expect("write survivor");
|
||||
let survivor = attach_target(&socket, temp.path(), Path::new("still-alive.txt"));
|
||||
assert_eq!(survivor.replica.materialize_string(), "alive\n");
|
||||
|
|
@ -685,6 +706,63 @@ mod crdt {
|
|||
assert!(wait_for_exit(&mut daemon, Duration::from_secs(5)).success());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dedup_upgrade_publishes_the_snapshot_to_preexisting_grid_replicas() {
|
||||
let temp = secure_tempdir();
|
||||
let config_dir = temp.path().join("pmacs");
|
||||
fs::create_dir(&config_dir).expect("create config dir");
|
||||
let seed_path = temp.path().join("seed.txt");
|
||||
let hidden_path = temp.path().join("hidden.txt");
|
||||
fs::write(&seed_path, "seed\n").expect("write seed");
|
||||
fs::write(&hidden_path, "hidden\n").expect("write hidden");
|
||||
fs::write(
|
||||
config_dir.join("init.lua"),
|
||||
format!(
|
||||
"local created_hidden = false\n\
|
||||
pmacs.hook.add('buffer.after-load', function()\n\
|
||||
if created_hidden then return end\n\
|
||||
created_hidden = true\n\
|
||||
pmacs.buffer.find_or_open({hidden_path:?})\n\
|
||||
end)\n"
|
||||
),
|
||||
)
|
||||
.expect("write hidden-buffer hook");
|
||||
let socket = temp.path().join("dedup-upgrade.sock");
|
||||
let mut daemon = spawn_daemon(&socket, &[]);
|
||||
let (_, mut grid) = attach_surviving_frontend(&socket);
|
||||
|
||||
let seed = attach_target(&socket, temp.path(), Path::new("seed.txt"));
|
||||
loop {
|
||||
match read_message::<InstanceMessage>(&mut grid).expect("grid seed publication") {
|
||||
InstanceMessage::BufferSnapshot { buffer_id, .. }
|
||||
if buffer_id == seed.buffer_id =>
|
||||
{
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let hidden = attach_target(&socket, temp.path(), Path::new("hidden.txt"));
|
||||
let hidden_snapshot = loop {
|
||||
match read_message::<InstanceMessage>(&mut grid).expect("grid hidden publication") {
|
||||
InstanceMessage::BufferSnapshot {
|
||||
buffer_id,
|
||||
crdt_snapshot,
|
||||
} if buffer_id == hidden.buffer_id => break crdt_snapshot,
|
||||
_ => {}
|
||||
}
|
||||
};
|
||||
let replica = CrdtState::new(900).expect("grid hidden replica");
|
||||
replica
|
||||
.import_snapshot(&hidden_snapshot)
|
||||
.expect("import hidden publication");
|
||||
assert_eq!(replica.materialize_string(), "hidden\n");
|
||||
|
||||
signal_pid(daemon.id(), Signal::SIGTERM);
|
||||
assert!(wait_for_exit(&mut daemon, Duration::from_secs(5)).success());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_killed_by_hook_fails_closed_and_slow_hook_holds_the_ready_barrier() {
|
||||
let temp = secure_tempdir();
|
||||
|
|
|
|||
Loading…
Reference in New Issue