pmacs/tests/worker_identity_acceptance.rs

1198 lines
45 KiB
Rust

// tests/worker_identity_acceptance.rs --- worker identity Stage 1.
//! Worker identity Stage 1 (`docs/worker-identity-framing.md` §6,
//! `COHERENCE.md` §9).
//!
//! §9 grades the worker model **mechanism without identity**: a job
//! carries a `JobKind` naming the builtin dispatcher it funnelled
//! through, so every third-party job renders under a builtin's label,
//! and no progress indicator exists anywhere. This suite pins what
//! Stage 1 does about that — a required `purpose` on the job and the
//! process, the dispatch-name ambient that stops
//! `pmacs.workers.dispatch` discarding its handler name, and the first
//! indicator a user sees without running a command.
//!
//! # What is NOT here, deliberately
//!
//! **Presence is enforced by the COMPILER, not by anything below.**
//! `JobSpec::purpose` is non-optional and `JobSpec` has no `Default`, so
//! a dispatcher that supplies none does not build. A funnel test would
//! prove only that the funnel stores what it was handed, and would say
//! nothing about whether fourteen callers handed it anything meaningful.
//! Everything below is about *semantics*.
//!
//! **That a raw `coroutine.yield` inside either dynamic scope is
//! prevented — it is not.** R46 forbids package code from yielding
//! raw, but it is a convention, and the scheduler diagnoses a non-Handle
//! yield only *after* the coroutine has suspended (`async.lua` resumes,
//! then inspects what came back), so no refusal sited in a yield helper
//! is ever consulted. Rule 1 claims **the two supported yield APIs** and
//! nothing more. A test that "proved" coverage this design does not have
//! would be worse than the recorded gap, so the gap is recorded instead
//! (framing §2, §6, §7).
//!
//! **That background work is attributable from one place** (Stage 2's
//! unified view), **that a terminal PTY is visible anywhere** (Q#W-4),
//! or **that any job is attributed to the PACKAGE responsible for it**.
//! `purpose` records what work is being done and, under
//! `pmacs.workers.dispatch`, which registered handler it ran under.
//! Neither is package ownership, which waits for P3 — and there is no
//! `owner` field, in any spelling, for it to squat on (framing §3, §7).
use std::collections::HashMap;
use std::time::{Duration, Instant};
use pmacs::async_runtime::JobKind;
use pmacs::cell::{Cell, CellGrid, CellSize, Glyph};
use pmacs::editor::EditorState;
use pmacs::protocol::FrontendId;
use pmacs::statusline::{
StatuslineEvaluationOutcome, StatuslineEvaluationTarget, StatuslineProviderId,
evaluate_statusline,
};
#[path = "common/iso.rs"]
mod iso;
// ---------------------------------------------------------------------------
// Harness
// ---------------------------------------------------------------------------
fn exec(state: &EditorState, source: &str) {
state.lua_host.lua().load(source.to_owned()).exec().unwrap();
}
fn eval<T: mlua::FromLuaMulti>(state: &EditorState, source: &str) -> T {
state.lua_host.lua().load(source.to_owned()).eval().unwrap()
}
fn editor() -> EditorState {
let state = EditorState::new_with_roots(&iso::roots());
exec(&state, "pmacs.lsp.config = {}");
state
}
/// Drive the async runtime until nothing is in flight and no coroutine
/// is parked. How many frames that takes is not knowable in advance, so
/// this never counts them.
///
/// Quiescence is measured as **no `Running` job**, not as an empty
/// pending table. Most jobs here are dispatched and never awaited —
/// that is the shape the indicator exists to describe — and a settled
/// entry stays in the pending table until someone takes its result, so
/// `pending_count() == 0` would never come true.
fn pump(state: &mut EditorState) {
let deadline = Instant::now() + Duration::from_secs(10);
loop {
let idle: bool = eval(
state,
"return pmacs._async.parked_count() == 0
and #pmacs.workers.snapshot().active == 0",
);
if idle {
return;
}
assert!(Instant::now() < deadline, "async pump deadline exceeded");
state.tick_async();
}
}
/// The purposes of every job the runtime currently has in flight.
///
/// Read through the **Lua** snapshot surface, which is what `*workers*`
/// and any package consume, rather than through the Rust struct.
fn active_purposes(state: &EditorState) -> Vec<String> {
eval(
state,
"local out = {}
for _, job in ipairs(pmacs.workers.snapshot().active) do
out[#out + 1] = job.purpose
end
return out",
)
}
fn paint(state: &EditorState, rows: u32, cols: u32) -> Vec<Cell> {
let mut cells = vec![Cell::default(); (rows * cols) as usize];
let mut grid = CellGrid {
cells: &mut cells,
stride: cols,
size: CellSize::new(rows, cols),
};
let _ = pmacs::editor::paint_frame(
state,
FrontendId::LOCAL,
&HashMap::new(),
&mut grid,
CellSize::new(rows, cols),
);
cells
}
fn row_text(cells: &[Cell], cols: u32, row: u32) -> String {
(0..cols)
.map(
|column| match &cells[(row * cols + column) as usize].glyph {
Glyph::Char(ch) => *ch,
Glyph::Cluster(bytes) => std::str::from_utf8(bytes)
.ok()
.and_then(|text| text.chars().next())
.unwrap_or(' '),
Glyph::Continuation => ' ',
},
)
.collect()
}
/// The registration handle of the builtin activity provider.
fn activity_provider(state: &EditorState) -> StatuslineProviderId {
state
.statusline_registry
.borrow()
.providers()
.into_iter()
.find(|provider| provider.name == "activity")
.expect("builtin activity provider")
.id
}
/// The activity provider's segment for `LOCAL`'s only window, or `None`
/// when it produced **no segment at all**.
///
/// `Option<String>`, never `String`, is the whole point of this helper:
/// "absent" and "empty" must be distinguishable, because a zero-width
/// segment still consumes a separator in the composed modeline.
fn activity_segment(state: &EditorState) -> Option<String> {
let id = activity_provider(state);
let evaluation = evaluate_statusline(
state.lua_host.lua(),
&state.core,
&state.statusline_registry,
StatuslineEvaluationTarget::Grid {
frontend_id: FrontendId::LOCAL,
},
);
let StatuslineEvaluationOutcome::Ready(windows) = evaluation.outcome else {
panic!("statusline evaluation must be ready in a single-window editor");
};
windows
.iter()
.flat_map(|window| window.left.iter().chain(window.right.iter()))
.find(|segment| segment.provider_id == id)
.map(|segment| segment.text.clone())
}
/// Dispatch one job that will still be **in flight** when the caller
/// looks, without sleeping.
///
/// A pending entry leaves `Running` only inside `AsyncRuntime::tick`, so
/// a dispatch with no intervening tick is in flight by construction —
/// no wall-clock race, and no worker left sleeping past the test.
fn dispatch_one_in_flight(state: &EditorState) {
exec(state, "IN_FLIGHT = pmacs.workers.sleep(50)");
}
// ---------------------------------------------------------------------------
// 1 — purpose reaches the three structurally distinct entry paths
// ---------------------------------------------------------------------------
/// One per distinct **shape**, not one per dispatcher: a pool
/// dispatcher, an `register_external` job, and a spawned process.
///
/// `register_external` is here because MCP and LSP bypass the worker
/// pool entirely — they are the likeliest paths for a later field to be
/// added to `PendingJob` and quietly missed — and because its `JobKind`
/// is the undifferentiated `LspRequest`/`McpRequest` for every method,
/// so `purpose` is the only thing that tells two of its rows apart.
#[test]
fn every_entry_shape_records_what_its_work_is() {
let mut state = editor();
// (a) A pool dispatcher.
dispatch_one_in_flight(&state);
let purposes = active_purposes(&state);
assert_eq!(purposes.len(), 1, "one job in flight: {purposes:?}");
assert_eq!(
purposes[0], "sleep 50ms",
"a pool job records the work, not just its handler's name"
);
// (b) An externally-settled job. The purpose is a PARAMETER here
// because `register_external` has nothing to derive one from: its
// kind is a category, not a description.
let (job_id, _token) = state.async_runtime.register_external(
JobKind::LspRequest,
None,
"lsp textDocument/definition file:///tmp/x.rs",
);
let purposes = active_purposes(&state);
assert!(
purposes
.iter()
.any(|p| p == "lsp textDocument/definition file:///tmp/x.rs"),
"an externally-registered job carries its caller's description: {purposes:?}"
);
state.async_runtime.complete_external_cancelled(job_id);
// (c) A spawned process. `label` keeps its existing meaning and its
// existing callers; `purpose` is the new, separate answer to "what
// is this doing".
exec(
&state,
r#"P = pmacs.process.spawn {
label = "sh-1",
purpose = "probing the repository for a build system",
command = "/bin/sh",
args = { "-c", "sleep 5" },
}"#,
);
let rows: Vec<String> = eval(
&state,
"local out = {}
for _, row in ipairs(pmacs.process.list()) do
out[#out + 1] = row.label .. ' | ' .. row.purpose
end
return out",
);
assert!(
rows.iter()
.any(|row| row == "sh-1 | probing the repository for a build system"),
"a spawned process carries a purpose ALONGSIDE its label: {rows:?}"
);
exec(&state, "pmacs.process.terminate(P)");
pump(&mut state);
}
/// **`pmacs.process.spawn` REFUSES a spec with no purpose, and spawns
/// nothing.**
///
/// An earlier revision of this lane defaulted the field to `label` so
/// that existing callers kept working. That preserved compatibility and
/// delivered nothing: §9's complaint about `ProcessSpec` is precisely
/// that `label` is "caller-supplied, unvalidated convention", so a
/// purpose defaulting to the label hands every caller back the
/// convention this lane exists to replace.
///
/// Six refusals, each asserted the same way — the call raises, **the
/// message names the field and the rule**, and the process list is
/// unchanged, because a validation that rejects after spawning has
/// already done the thing it was rejecting:
///
/// * absent;
/// * empty, and whitespace-only — these satisfy the type and defeat the
/// point exactly as copying the label would (R42 rejects
/// whitespace-only config descriptions for the same reason);
/// * wrong type;
/// * **not valid UTF-8**. Lua strings are BYTE strings, so
/// `purpose = string.char(255)` is a value a caller can write, and
/// converting it with `?` would surface mlua's generic conversion
/// error before this lane's own diagnostic was ever constructed. The
/// refusal is not the interesting part — it refuses either way, and
/// nothing spawns either way — the MESSAGE is, which is why the
/// assertion is on content. Retyping this read as a bare `?` breaks
/// the row rather than silently degrading the error;
/// * **metatable-provided**, which is the `stdin`/`group` raw-read
/// posture: a spec table is plain data, so a purpose cannot be
/// smuggled in through `__index`.
#[test]
fn spawning_without_a_real_purpose_is_refused_and_starts_nothing() {
let mut state = editor();
let baseline: usize = eval(&state, "return #pmacs.process.list()");
for (label, spec, expected) in [
(
"absent",
r#"{ label = "x", command = "/bin/sh", args = { "-c", "sleep 5" } }"#,
"purpose is required",
),
(
"empty",
r#"{ label = "x", purpose = "", command = "/bin/sh", args = { "-c", "sleep 5" } }"#,
"must not be empty",
),
(
"whitespace-only",
r#"{ label = "x", purpose = " ", command = "/bin/sh", args = { "-c", "sleep 5" } }"#,
"must not be empty",
),
(
"wrong type",
r#"{ label = "x", purpose = 7, command = "/bin/sh", args = { "-c", "sleep 5" } }"#,
"purpose must be a string",
),
(
"invalid UTF-8",
r#"{ label = "x", purpose = "run " .. string.char(255),
command = "/bin/sh", args = { "-c", "sleep 5" } }"#,
"purpose must be valid UTF-8",
),
(
"metatable-provided",
r#"setmetatable(
{ label = "x", command = "/bin/sh", args = { "-c", "sleep 5" } },
{ __index = function(_, k)
if k == "purpose" then return "smuggled" end
return nil
end })"#,
"purpose is required",
),
] {
let (ok, err): (bool, String) = eval(
&state,
&format!(
"local ok, err = pcall(pmacs.process.spawn, {spec})
return ok, tostring(err)"
),
);
assert!(!ok, "{label}: spawn must refuse");
assert!(
err.contains(expected),
"{label}: the refusal must name the field and the rule; got {err:?}"
);
assert_eq!(
eval::<usize>(&state, "return #pmacs.process.list()"),
baseline,
"{label}: a refused spawn must start no process"
);
}
pump(&mut state);
}
/// Q#W-4's preservation half, pinned here as well as by the three
/// leak-detector suites: `purpose` is a new KEY on each existing row and
/// changes nothing about **which** processes `list()` enumerates.
///
/// `m6_8_multi_repl_acceptance`, `compile_mode_acceptance` and
/// `lean4_stage1_acceptance` all assert on `#pmacs.process.list()` as a
/// leak baseline. If any of them needs editing, the design is wrong.
#[test]
fn process_list_still_hides_terminal_ptys() {
let mut state = editor();
let before: usize = eval(&state, "return #pmacs.process.list()");
exec(
&state,
"T = pmacs.terminal.open { command = '/bin/sh', args = { '-c', 'sleep 5' } }",
);
let after: usize = eval(&state, "return #pmacs.process.list()");
assert_eq!(
before, after,
"a terminal PTY must stay invisible to pmacs.process.list (Q#W-4)"
);
assert!(
eval::<bool>(&state, "return pmacs.terminal.is_terminal(T)"),
"precondition: the PTY really was opened"
);
// No explicit close: terminals have no Lua teardown surface, and
// `EditorState::drop` shuts the supervisor down with SIGTERM then
// SIGKILL, so the child cannot outlive the test.
pump(&mut state);
}
// ---------------------------------------------------------------------------
// 2 — the dispatch-name ambient (Q#W-2)
// ---------------------------------------------------------------------------
/// **A registered handler name is DISPLAY TEXT, and gets `purpose`'s
/// meaningful-value standard.**
///
/// Before this lane the name died inside `dispatch` and a type check was
/// the whole of what it needed. It no longer dies there: the ambient
/// carries it into every job the handler allocates and composes it into
/// `purpose`, which `*workers*` and the modeline both render. So empty
/// and whitespace-only are refused here for the reason they are refused
/// in `required_purpose` — they satisfy the type and say nothing.
///
/// **Refused at `register`, and asserted twice**: the call raises with a
/// message naming the rule, *and* nothing is installed under the name —
/// a validation that stored the handler first would have registered the
/// thing it was rejecting.
#[test]
fn a_handler_name_that_says_nothing_is_refused_at_registration() {
let mut state = editor();
for (label, name_expr) in [("empty", r#""""#), ("whitespace-only", r#"" \t ""#)] {
let (ok, err): (bool, String) = eval(
&state,
&format!(
"local ok, err = pcall(pmacs.workers.register, {name_expr}, function() end)
return ok, tostring(err)"
),
);
assert!(!ok, "{label}: register must refuse");
assert!(
err.contains("must not be empty or whitespace-only"),
"{label}: the refusal must name the rule; got {err:?}"
);
let (dispatched, dispatch_err): (bool, String) = eval(
&state,
&format!(
"local ok, err = pcall(pmacs.workers.dispatch, {name_expr})
return ok, tostring(err)"
),
);
assert!(!dispatched, "{label}: a refused name must install nothing");
assert!(
dispatch_err.contains("unknown handler"),
"{label}: and the name must be genuinely absent from the table; \
got {dispatch_err:?}"
);
}
pump(&mut state);
}
/// **Control characters in a handler name are refused at the source —
/// and this is the one rule `purpose` deliberately does NOT get.**
///
/// The asymmetry is the design (`#228`'s shape). A purpose may
/// legitimately contain a newline: a filesystem path can, and
/// `pmacs-magit`'s spawn purpose is a whole argv — so its one-line
/// constraint is enforced by escaping at the surfaces that have one row.
/// A handler name is an identifier a package chooses for itself and
/// hands back to `dispatch`; a control character in one is a mistake or
/// an attempt at one, and refusing costs nobody anything.
///
/// The rows are chosen to be **not** whitespace-only, so this test
/// cannot pass on the previous guard: a newline mid-word, an ESC (which
/// starts a terminal escape sequence), and a NUL.
#[test]
fn a_handler_name_with_control_characters_is_refused_at_registration() {
let mut state = editor();
for (label, name_expr) in [
("newline", r#""index\ner""#),
("escape", r#""index" .. string.char(27) .. "[31mer""#),
("nul", r#""index" .. string.char(0) .. "er""#),
] {
let (ok, err): (bool, String) = eval(
&state,
&format!(
"local ok, err = pcall(pmacs.workers.register, {name_expr}, function() end)
return ok, tostring(err)"
),
);
assert!(!ok, "{label}: register must refuse");
assert!(
err.contains("must not contain control characters"),
"{label}: the refusal must name the rule; got {err:?}"
);
let (dispatched, dispatch_err): (bool, String) = eval(
&state,
&format!(
"local ok, err = pcall(pmacs.workers.dispatch, {name_expr})
return ok, tostring(err)"
),
);
assert!(!dispatched, "{label}: a refused name must install nothing");
assert!(
dispatch_err.contains("unknown handler"),
"{label}: and the name must be genuinely absent from the table; \
got {dispatch_err:?}"
);
}
pump(&mut state);
}
/// **The third rule a display-text name needs, enforced at the one
/// place that can see it.**
///
/// Lua strings are BYTE strings and Lua 5.1 has no `utf8` library, so
/// `register` cannot tell a valid name from arbitrary bytes —
/// `string.char(255)` is neither whitespace nor a control character by
/// `%c`. The name crosses into Rust exactly once, at
/// `_push_dispatch_name`, and that is where the byte-level rule is
/// enforced. **This is the P2a class again**: an `mlua`-driven
/// conversion there would refuse with a generic message naming neither
/// the argument nor the rule, so the conversion failure is mapped onto
/// an owned diagnostic instead.
///
/// The refusal lands at `dispatch` rather than at `register`, which is
/// later than ideal and is asserted as such — but it is still **before
/// the handler runs**, so a name that cannot be displayed never reaches
/// a job's purpose.
#[test]
fn a_handler_name_that_is_not_valid_utf8_is_refused_before_the_handler_runs() {
let mut state = editor();
let (ok, err): (bool, String) = eval(
&state,
"RAN = false
pmacs.workers.register('bad' .. string.char(255), function() RAN = true end)
local ok, err = pcall(pmacs.workers.dispatch, 'bad' .. string.char(255))
return ok, tostring(err)",
);
assert!(!ok, "dispatch must refuse a name it cannot display");
assert!(
err.contains("handler name must be valid UTF-8"),
"the refusal must name the argument and the rule; got {err:?}"
);
assert!(
!eval::<bool>(&state, "return RAN"),
"and it must refuse BEFORE running the handler"
);
assert!(
!eval::<bool>(&state, "return pmacs._async._in_dispatch_name_scope()"),
"a push that failed must leave no name on the stack"
);
pump(&mut state);
}
/// **Rule 7 + the defect itself.** A job dispatched through
/// `pmacs.workers.dispatch("name", …)` reports `"name"`.
///
/// The witness is a handler **registered from Lua that calls a real
/// dispatcher**, not a synthetic push of the ambient. A test that
/// pushed the name by hand would prove the stack works and leave the
/// actual defect — `name` dying inside an arbitrary handler, three
/// layers above anything that takes a name — completely unwitnessed.
#[test]
fn a_dispatched_job_reports_the_registered_handler_name() {
let mut state = editor();
exec(
&state,
"pmacs.workers.register('indexer', function()
return pmacs.workers.sleep(50)
end)
H = pmacs.workers.dispatch('indexer')",
);
let purposes = active_purposes(&state);
assert_eq!(purposes.len(), 1, "one job in flight: {purposes:?}");
assert!(
purposes[0].starts_with("indexer"),
"the third party's own name must survive the call chain: {purposes:?}"
);
// Rule 7: outside any extent, nothing changes.
exec(&state, "DIRECT = pmacs.workers.sleep(50)");
let purposes = active_purposes(&state);
assert!(
purposes.iter().any(|p| p == "sleep 50ms"),
"a builtin invoked directly records its own purpose: {purposes:?}"
);
pump(&mut state);
}
/// **Rule 6 — COMPOSE, do not replace.** Both halves asserted, because
/// a test on the prefix alone passes when the description is dropped,
/// and a test on the description alone passes when the third party is
/// lost again.
#[test]
fn a_dispatched_job_composes_the_handler_name_with_the_work() {
let mut state = editor();
exec(
&state,
"pmacs.workers.register('indexer', function()
return pmacs.workers.sleep(50)
end)
H = pmacs.workers.dispatch('indexer')",
);
let purposes = active_purposes(&state);
assert_eq!(
purposes,
vec!["indexer: sleep 50ms".to_owned()],
"letting the name win discards the work; letting the work win \
loses the third party"
);
pump(&mut state);
}
/// **Rules 3 and 4 — nesting is a stack (innermost wins) and fan-out
/// shares the name.**
#[test]
fn nesting_takes_the_innermost_name_and_fan_out_shares_it() {
let mut state = editor();
exec(
&state,
"pmacs.workers.register('inner', function()
-- Fan-out: two jobs under one handler.
A = pmacs.workers.sleep(50)
B = pmacs.workers.sleep(51)
return A
end)
pmacs.workers.register('outer', function()
pmacs.workers.dispatch('inner')
-- Back in `outer`'s extent: the stack restored on return.
C = pmacs.workers.sleep(52)
return C
end)
pmacs.workers.dispatch('outer')",
);
let mut purposes = active_purposes(&state);
purposes.sort();
assert_eq!(
purposes,
vec![
"inner: sleep 50ms".to_owned(),
"inner: sleep 51ms".to_owned(),
"outer: sleep 52ms".to_owned(),
],
"innermost wins inside, and the outer name is restored after"
);
pump(&mut state);
}
/// **Rule 5 — unwind-safe, and this is the one that makes a naive
/// version worse than none.**
///
/// A handler that raises must still pop. Otherwise one failure poisons
/// every subsequent dispatch in the session with a stale name, and the
/// feature stops failing loudly and starts lying silently — a
/// regression that would surface as intermittent misattribution long
/// after the lane landed.
#[test]
fn a_raising_handler_still_pops_its_name() {
let mut state = editor();
exec(
&state,
"pmacs.workers.register('boom', function() error('handler failed') end)
OK, ERR = pcall(pmacs.workers.dispatch, 'boom')",
);
assert!(
!eval::<bool>(&state, "return OK"),
"the handler's error must still reach the caller"
);
assert!(
eval::<String>(&state, "return tostring(ERR)").contains("handler failed"),
"and must reach it unchanged"
);
exec(&state, "LATER = pmacs.workers.sleep(50)");
let purposes = active_purposes(&state);
assert_eq!(
purposes,
vec!["sleep 50ms".to_owned()],
"an unrelated later dispatch must not inherit the failed \
handler's name: {purposes:?}"
);
pump(&mut state);
}
/// **Preservation.** `pmacs.workers.dispatch` was `return
/// handler(args, opts)` — a tail call that propagates **every** return
/// value. Bracketing it must not quietly truncate that.
///
/// A `local ok, result = pcall(...)` bracketing would pass every other
/// test in this file and lose a two-value handler's second value with no
/// error anywhere, which is the shape of regression that surfaces months
/// later in somebody else's package.
#[test]
fn dispatch_still_propagates_every_value_the_handler_returns() {
let mut state = editor();
let values: Vec<String> = eval(
&state,
"pmacs.workers.register('multi', function()
return pmacs.workers.sleep(50), 'second', 'third'
end)
local a, b, c = pmacs.workers.dispatch('multi')
return { type(a), tostring(b), tostring(c) }",
);
assert_eq!(
values,
vec!["table".to_owned(), "second".to_owned(), "third".to_owned()],
"a multi-value handler must survive the bracketing"
);
pump(&mut state);
}
/// **Rule 2 — work dispatched LATER is not covered, deliberately.**
///
/// A job dispatched from an `on_complete` callback runs ticks later,
/// outside the extent, and carries only its own purpose. Asserted so
/// that the boundary reads as designed rather than as broken; covering
/// it would need the asynchronous lifetime mechanism Stage 3 owns
/// (Q#W-5).
#[test]
fn work_dispatched_from_a_completion_callback_carries_no_handler_name() {
let mut state = editor();
exec(
&state,
"LATE = nil
pmacs.workers.register('deferred', function()
local h = pmacs.workers.sleep(1)
h:on_complete(function()
LATE = pmacs.workers.sleep(50)
end)
return h
end)
pmacs.workers.dispatch('deferred')",
);
// One tick settles the first job and fires the callback; the job the
// callback dispatches is what this test is about, so do not pump to
// quiescence before reading it.
let deadline = Instant::now() + Duration::from_secs(10);
while !eval::<bool>(&state, "return LATE ~= nil") {
assert!(Instant::now() < deadline, "callback never fired");
state.tick_async();
}
let purposes = active_purposes(&state);
assert_eq!(
purposes,
vec!["sleep 50ms".to_owned()],
"the extent is the handler CALL, not the job's lifetime: {purposes:?}"
);
pump(&mut state);
}
// ---------------------------------------------------------------------------
// 3 — rule 1: the extent is non-yieldable, and that is ENFORCED
// ---------------------------------------------------------------------------
/// **Rule 1, first supported yield API.** Two assertions, and the
/// second is the load-bearing one.
///
/// A guard that raises but leaves the name pushed has converted a silent
/// misattribution into a silent misattribution *plus* an error. So the
/// witness dispatches again after the rejection and asserts the new job
/// carries no stale name.
#[test]
fn awaiting_inside_a_handler_is_refused_and_the_scope_restores() {
let mut state = editor();
// The awaited handle is created OUTSIDE the extent on purpose: the
// second assertion below is about what a job allocated *after* the
// refusal carries, and a job the handler allocated for itself would
// legitimately wear the handler's name and blur that.
exec(
&state,
"OUTSIDE = pmacs.workers.sleep(1)
REFUSAL = nil
pmacs.workers.register('awaits', function()
local ok, err = pcall(function() return OUTSIDE:await() end)
REFUSAL = (not ok) and tostring(err) or '<no raise>'
return OUTSIDE
end)
pmacs.async(function() pmacs.workers.dispatch('awaits') end)",
);
let refusal: String = eval(&state, "return REFUSAL");
assert!(
refusal.contains("cannot await inside") && refusal.contains("pmacs.workers.dispatch"),
"the refusal must name the rule it enforces; got {refusal:?}"
);
assert!(
!eval::<bool>(&state, "return pmacs._async._in_dispatch_name_scope()"),
"a refused await must still leave the scope popped"
);
exec(&state, "AFTER = pmacs.workers.sleep(50)");
let purposes = active_purposes(&state);
assert!(
purposes.iter().any(|p| p == "sleep 50ms"),
"and a later dispatch must carry no stale name: {purposes:?}"
);
assert!(
!purposes.iter().any(|p| p.starts_with("awaits:")),
"no job allocated after the refusal may inherit the handler's \
name: {purposes:?}"
);
pump(&mut state);
}
/// **Rule 1, unconditionally.** The refusal fires even when the awaited
/// handle has already settled.
///
/// This is the case that separates an unconditional guard from one whose
/// behaviour depends on a race: a guard placed after the `_is_complete`
/// check would fire only when a yield would really occur, passing under
/// test and failing intermittently in production depending on whether
/// the job happened to finish first.
#[test]
fn the_await_refusal_fires_even_for_an_already_complete_handle() {
let mut state = editor();
exec(&state, "SETTLED = pmacs.workers.sleep(0)");
let deadline = Instant::now() + Duration::from_secs(10);
while !eval::<bool>(&state, "return SETTLED:is_complete()") {
assert!(Instant::now() < deadline, "the canary never settled");
state.tick_async();
}
exec(
&state,
"REFUSAL = nil
pmacs.workers.register('awaits-settled', function()
local ok, err = pcall(function() return SETTLED:await() end)
REFUSAL = (not ok) and tostring(err) or '<no raise>'
return pmacs.workers.sleep(50)
end)
pmacs.workers.dispatch('awaits-settled')",
);
let refusal: String = eval(&state, "return REFUSAL");
assert!(
refusal.contains("cannot await inside") && refusal.contains("pmacs.workers.dispatch"),
"a settled handle must be refused too, or the guard's behaviour \
depends on a race; got {refusal:?}"
);
assert!(
!eval::<bool>(&state, "return pmacs._async._in_dispatch_name_scope()"),
"and the scope must still be popped"
);
pump(&mut state);
}
/// **Rule 1, second supported yield API.** Guarding `:await()` and not
/// `yield_to_next_tick` would leave the extent open through a second
/// door — and Q#W-7 below is the proof that exactly that happens when
/// only one door is guarded.
#[test]
fn yield_to_next_tick_inside_a_handler_is_refused_and_the_scope_restores() {
let mut state = editor();
exec(
&state,
"REFUSAL = nil
pmacs.workers.register('yields', function()
local ok, err = pcall(pmacs.async.yield_to_next_tick)
REFUSAL = (not ok) and tostring(err) or '<no raise>'
return pmacs.workers.sleep(50)
end)
pmacs.async(function() pmacs.workers.dispatch('yields') end)",
);
let refusal: String = eval(&state, "return REFUSAL");
assert!(
refusal.contains("cannot yield inside") && refusal.contains("pmacs.workers.dispatch"),
"the second yield API must refuse too; got {refusal:?}"
);
assert!(
!eval::<bool>(&state, "return pmacs._async._in_dispatch_name_scope()"),
"and must leave the scope popped"
);
exec(&state, "AFTER = pmacs.workers.sleep(51)");
let purposes = active_purposes(&state);
assert!(
purposes.iter().any(|p| p == "sleep 51ms"),
"a later dispatch must carry no stale name: {purposes:?}"
);
pump(&mut state);
}
// ---------------------------------------------------------------------------
// 4 — Q#W-7: the same hole in `commit_to`, closed here
// ---------------------------------------------------------------------------
/// **Q#W-7 — a pre-existing defect, found by reading and repaired in
/// this lane.**
///
/// `Handle:await()` refuses inside `pmacs.window.commit_to` precisely so
/// a coroutine cannot park with the frontend scope pushed (Journey Stage
/// 1a, Q#JR14b). But `pmacs.async.yield_to_next_tick()` also yields, is
/// public, and carried **no** such refusal — so that invariant had a
/// second entrance.
///
/// **Reachability by a real caller is UNPROVEN.** No production caller
/// is known to yield through this door inside a commit; this pins the
/// guard rather than reproducing a user-visible bug.
///
/// Both halves asserted, for the same reason as rule 1's: a refusal that
/// leaves the scope pushed swaps a silent misrouting for a loud one and
/// fixes neither.
#[test]
fn yield_to_next_tick_inside_commit_to_is_refused_and_the_commit_scope_restores() {
let mut state = editor();
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join("alpha.txt"), b"alpha\n").expect("write");
// A GENUINE destination, produced by the production capture: the
// listener claims (returns false), so nothing commits and what lands
// in `dest` is exactly the userdata dired would have received.
// Nothing in a test can construct one.
exec(
&state,
"dest = nil
pmacs.hook.add('path.open-directory', function(_, d) dest = d return false end)",
);
state.open_directory_target(dir.path());
pump(&mut state);
assert!(
eval::<bool>(&state, "return dest ~= nil"),
"the chain must hand listeners a destination"
);
exec(
&state,
"REFUSAL = nil
pmacs.async(function()
local ok, err = pcall(pmacs.window.commit_to, dest, function()
pmacs.async.yield_to_next_tick()
end)
REFUSAL = (not ok) and tostring(err) or '<no raise>'
end)",
);
let refusal: String = eval(&state, "return REFUSAL");
assert!(
refusal.contains("cannot yield inside") && refusal.contains("commit_to"),
"the second door into the commit scope must be shut; got {refusal:?}"
);
assert!(
!eval::<bool>(&state, "return pmacs._async._in_commit_scope()"),
"and the commit scope must still be restored afterwards"
);
pump(&mut state);
}
// ---------------------------------------------------------------------------
// 5 — the statusline activity indicator (Q#W-3, Q#W-6)
// ---------------------------------------------------------------------------
/// **Absent at zero, asserted as an absent SEGMENT rather than as an
/// empty string.** A zero-width segment still consumes a separator in
/// the composed modeline, so "returns nothing" and "returns nothing
/// visible" are different claims and only one of them is the design.
#[test]
fn the_indicator_produces_no_segment_at_all_when_nothing_is_running() {
let state = editor();
assert_eq!(
activity_segment(&state),
None,
"an idle editor must produce NO activity segment"
);
}
/// **A count plus the oldest in-flight job's purpose, witnessed through
/// the real per-frame evaluation path.**
///
/// Driven through `paint_frame`, not by calling the provider function
/// directly: a provider that works in isolation and never gets evaluated
/// is exactly the failure this must exclude.
#[test]
fn the_indicator_shows_a_count_and_the_oldest_purpose_in_a_painted_frame() {
let mut state = editor();
exec(
&state,
"FIRST = pmacs.workers.sleep(50)
SECOND = pmacs.workers.grep({ root = '/tmp', pattern = 'zzz-no-match' })",
);
let cells = paint(&state, 24, 160);
let modeline = row_text(&cells, 160, 22);
assert!(
modeline.contains("⋯2 sleep 50ms"),
"the painted modeline must carry the count and the OLDEST job's \
purpose (not the newest); got {modeline:?}"
);
// And the same value reaches the evaluator's segment vector, which is
// what the semantic frontend ships.
assert_eq!(
activity_segment(&state).as_deref(),
Some("⋯2 sleep 50ms"),
"the segment and the painted row must agree"
);
exec(&state, "SECOND:cancel()");
pump(&mut state);
}
/// **Q#W-6 — the setting, witnessed with work genuinely in flight.**
///
/// The discriminating case: an assertion taken on an idle editor cannot
/// tell "disabled" from "nothing is happening", which is the only thing
/// this setting changes.
#[test]
fn the_indicator_honours_its_setting_while_work_is_in_flight() {
let mut state = editor();
dispatch_one_in_flight(&state);
assert!(
activity_segment(&state).is_some(),
"precondition: work is in flight and the indicator is on"
);
exec(&state, "pmacs.config.set('ui.activity-indicator', false)");
assert_eq!(
activity_segment(&state),
None,
"disabled means NO segment, with work still running"
);
exec(&state, "pmacs.config.set('ui.activity-indicator', true)");
assert!(
activity_segment(&state).is_some(),
"and re-enabling brings it back without a restart"
);
pump(&mut state);
}
/// The setting is a real registry entry, not an ad-hoc global: it is
/// discoverable through `pmacs.config.describe` like every other
/// setting, which is what `COHERENCE.md` §11 grades.
#[test]
fn the_setting_is_registered_with_a_true_default() {
let state = editor();
let (kind, default): (String, bool) = eval(
&state,
"local d = pmacs.config.describe('ui.activity-indicator')
return d.type, d.default",
);
assert_eq!(kind, "boolean");
assert!(default, "visible by default — no configuration, no command");
}
// ---------------------------------------------------------------------------
// 6 — `*workers*` renders the purpose
// ---------------------------------------------------------------------------
/// The view §9 already has, now answering §9's question.
///
/// `Kind` names the builtin dispatcher a job funnelled through, which
/// for a third-party job is a builtin's label rather than the caller's;
/// the `Purpose` column is what carries the caller's own account.
#[test]
fn the_workers_buffer_renders_the_purpose_column() {
let mut state = editor();
exec(
&state,
"pmacs.workers.register('indexer', function()
return pmacs.workers.sleep(50)
end)
pmacs.workers.dispatch('indexer')
BUF = pmacs.workers.show()",
);
let text: String = eval(&state, "return BUF:slice(0, BUF:len())");
assert!(
text.contains("Purpose"),
"the active table must have a Purpose column:\n{text}"
);
assert!(
text.contains("indexer: sleep 50ms"),
"and the row must render it:\n{text}"
);
exec(&state, "pmacs.workers.hide()");
pump(&mut state);
}
// ---------------------------------------------------------------------------
// 7 — the display-text boundary: a row must not forge another row
// ---------------------------------------------------------------------------
//
// A purpose is free-form caller text and is legitimately multi-line — a
// filesystem path may contain a newline and `pmacs-magit`'s spawn
// purpose is a whole argv — so the one-line constraint belongs to the
// surfaces that have one line, not to the purpose. That is `#228`'s
// decision on `Command.description`, applied here as escaping rather
// than clipping, because a purpose's later words are load-bearing.
//
// Every test below drives the REAL rendering path. Calling
// `purpose_for_one_row` directly would prove the escaper escapes and say
// nothing about whether either surface calls it.
//
// `register_external` is the witness in all three because it is the one
// entry shape whose purpose is verbatim caller text: the pool
// dispatchers all `format!` their own, and `{:?}` in those formats
// already escapes, so a hostile purpose cannot reach a row through them.
/// **The spoofing property, and the whole reason the escaping exists.**
///
/// A purpose crafted to look like a row boundary followed by a plausible
/// job row does not produce a second row. Asserted by COUNTING the rows,
/// not by looking for the escape sequence: a renderer that dropped the
/// purpose entirely would satisfy "no forged row" while destroying the
/// feature, so the escaped text is asserted present in the surviving row
/// as well.
#[test]
fn a_purpose_shaped_like_a_row_boundary_does_not_produce_a_second_row() {
let mut state = editor();
let (job_id, _token) = state.async_runtime.register_external(
JobKind::LspRequest,
None,
"lsp definition\n#99 grep 0ms forged \
cancelled by nobody",
);
exec(&state, "BUF = pmacs.workers.show()");
let text: String = eval(&state, "return BUF:slice(0, BUF:len())");
let job_rows: Vec<&str> = text.lines().filter(|line| line.starts_with('#')).collect();
assert_eq!(
job_rows.len(),
1,
"one job must render as exactly ONE row:\n{text}"
);
assert!(
job_rows[0].contains("lsp definition\\n#99"),
"and the break must be rendered, escaped, INSIDE that row:\n{text}"
);
assert!(
!text.contains("\n#99"),
"no line may begin with the forged id:\n{text}"
);
// `#228`'s other half, and what makes this a rendering decision
// rather than data loss: the free-form surface still hands Lua every
// byte, unescaped.
let raw = active_purposes(&state);
assert!(
raw.iter().any(|purpose| purpose.contains('\n')),
"pmacs.workers.snapshot() is the raw path and must stay raw: {raw:?}"
);
exec(&state, "pmacs.workers.hide()");
state.async_runtime.complete_external_cancelled(job_id);
pump(&mut state);
}
/// **The modeline is one line, and that is enforced where the modeline
/// reads.**
///
/// Two assertions, because they exclude different failures: the segment
/// carries no break at all (a composed modeline splicing one would
/// misplace every segment after it), and the escaped text survives the
/// real per-frame paint rather than only the evaluator.
#[test]
fn a_purpose_that_spans_lines_reaches_the_modeline_as_one_line() {
let mut state = editor();
let (job_id, _token) = state.async_runtime.register_external(
JobKind::LspRequest,
None,
"lsp didOpen\nfile:///tmp/x.rs",
);
let segment = activity_segment(&state).expect("work is in flight, so a segment exists");
assert!(
!segment.contains(['\n', '\r']),
"a modeline segment is ONE line: {segment:?}"
);
assert_eq!(
segment, "⋯1 lsp didOpen\\nfile:///tmp/x.rs",
"and the break is escaped in place, not clipped away"
);
let cells = paint(&state, 24, 160);
let modeline = row_text(&cells, 160, 22);
assert!(
modeline.contains("⋯1 lsp didOpen\\nfile:///tmp/x.rs"),
"the painted modeline must carry it too; got {modeline:?}"
);
state.async_runtime.complete_external_cancelled(job_id);
pump(&mut state);
}
/// **A purpose with no control characters is byte-identical after
/// escaping** — on both surfaces.
///
/// The fixture is chosen to break a careless escaper: a literal
/// backslash (which a JSON-style escaper would double, and which is
/// deliberately NOT escaped here — no number of backslashes produces a
/// second row), a `\v` that is text rather than a vertical tab, quotes,
/// and a non-ASCII character.
#[test]
fn a_purpose_with_no_control_characters_is_unchanged_by_the_boundary() {
const PURPOSE: &str = r#"grep "fn \d+" in /tmp/pro—ject\v2"#;
let mut state = editor();
let (job_id, _token) =
state
.async_runtime
.register_external(JobKind::LspRequest, None, PURPOSE);
exec(&state, "BUF = pmacs.workers.show()");
let text: String = eval(&state, "return BUF:slice(0, BUF:len())");
assert!(
text.contains(PURPOSE),
"the *workers* row must reproduce an ordinary purpose byte for byte:\n{text}"
);
assert_eq!(
activity_segment(&state).as_deref(),
Some(format!("⋯1 {PURPOSE}").as_str()),
"and so must the modeline segment"
);
exec(&state, "pmacs.workers.hide()");
state.async_runtime.complete_external_cancelled(job_id);
pump(&mut state);
}