test(font): wire acceptance suite + protocol/design docs (items 2-8, 15)

tests/gpu_font_acceptance.rs (items 2-7; the header manifest routes
item 1 to src/protocol.rs pins, item 8 to the src/frontend.rs TUI
drop unit, items 9-14/16-19 to pmacs-gpu's headless suite, and item
15 to the docs):

- 2: a fresh attachment's first frame carries the REAL (None, None)
     default; unchanged ticks are silent; a late joiner receives the
     current preference without post-attach mutation
- 3: a mid-session set_font emits exactly one FontFacts on the next
     frame; an identical re-set advances the epoch without emitting
- 4 (producer half): on_buffer_snapshot_sent re-ships buffer facts
     but never the bufferless FontFacts
- 5: real-daemon probe -- a v17 semantic session receives FontFacts,
     a v16 peer never does (crdt feature)
- 6: the strict Lua contract -- 5.999/72.01/0/-16/NaN/inf/non-number
     sizes error naming `size`; empty/non-string family errors
     naming `family`; unknown keys rejected by name; hostile
     __index/__pairs metatables never invoked and never inject
     values; quantization pins 15.994->1599 / 15.996->1600 and both
     boundaries; set_font {} resets both axes; the getter returns a
     fresh quantized plain table; every rejected shape leaves the
     preference and the wire untouched
- 7: a load_user_config_at fixture's init.lua set_font lands in the
     handle installed before user config, and a pre-attach
     preference ships on the first frame

Bites (mutate, observe the acceptance test fail, restore): the
Option-seeded first-frame send, the payload compare, the snapshot
survival of the font baselines, the v17 gate (BOTH halves widened --
producer for_peer alone leaks nothing because the daemon skip arm
still filters; the wire test only fails when belt AND braces are
cut), the unknown-key rejection, the range-check-original ordering,
and the raw_get metatable isolation.

Docs (item 15): docs/semantic-frontend-protocol.md gains the
FontFacts variant entry (v17 gate, authoritative default, no-pixels
preference relay, fail-closed receiver) and folds FontFacts into the
BufferSnapshot reset-contract paragraph (bufferless facts survive;
the frontend's caret-follow scroll residual is the buffer-scoped
part). docs/pmacs-gpu-design.md's "future customization needs no
wire-protocol changes" claim is corrected in place and points at the
framing as design of record; the Lua-override bullet now names the
landed set_font shape. The framing status bumps to implemented.

Also re-homes the ProcessSupervisor doc comment that the make_font_pref
insertion had orphaned onto the wrong function.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
This commit is contained in:
Levi Neuwirth 2026-07-15 17:49:11 +01:00
parent fe65fddae5
commit 6cd78f0870
5 changed files with 534 additions and 9 deletions

View File

@ -1,6 +1,7 @@
# GPU font preference — framing (Arc 4 stage 2, `pmacs.gpu.set_font`) # GPU font preference — framing (Arc 4 stage 2, `pmacs.gpu.set_font`)
**Revision 4 — 2026-07-15. Status: awaiting review.** **Revision 4 — 2026-07-15. Status: implemented on branch
`gpu-set-font` (protocol v17); awaiting PR review.**
Revision 4 (framing round 3, findings 15): caret preservation is now Revision 4 (framing round 3, findings 15): caret preservation is now
VISUAL-RUN aware rather than source-line-only. The code buffer keeps a VISUAL-RUN aware rather than source-line-only. The code buffer keeps a

View File

@ -289,14 +289,21 @@ override.**
classified as a small finding under rule (iii) and absorbed; the classified as a small finding under rule (iii) and absorbed; the
bundled `fonts/OFL.txt` is shipped alongside the TTF as required bundled `fonts/OFL.txt` is shipped alongside the TTF as required
by the OFL. by the OFL.
- Lua override: `pmacs.gpu.set_font(path)` or similar (precise binding - Lua override: **landed** as `pmacs.gpu.set_font { family?, size? }`
shape decided session 2). (Arc 4 stage 2, `docs/gpu-set-font-framing.md`) — a family NAME
resolved frontend-locally against the sanitized font database, not
a path.
- Missing-glyph fallback: tofu (replacement character `U+FFFD`). - Missing-glyph fallback: tofu (replacement character `U+FFFD`).
Explicit non-goal to ship a sophisticated fallback chain in v0.1. Explicit non-goal to ship a sophisticated fallback chain in v0.1.
If real users hit this, it's v0.2+ scope. If real users hit this, it's v0.2+ scope.
The bundled-default-plus-override shape means v0.1 works without The bundled-default-plus-override shape means v0.1 works without
configuration; future customization needs no wire-protocol changes. configuration. **Correction (Arc 4 stage 2):** this section originally
claimed future customization would need no wire-protocol change, but
the preference lives daemon-side (init.lua runs in the daemon, and
every attaching GPU must render consistently), so customization
shipped as the `FontFacts` fact at protocol v17 —
`docs/gpu-set-font-framing.md` is the design of record.
## Rhythm ## Rhythm

View File

@ -126,8 +126,11 @@ invalidating its per-buffer emission baselines whenever it writes a
snapshot, so the frontend's post-snapshot viewport declaration snapshot, so the frontend's post-snapshot viewport declaration
receives authoritative re-sends even when nothing changed receives authoritative re-sends even when nothing changed
daemon-side (the unchanged-generation A → B → A revisit). Bufferless daemon-side (the unchanged-generation A → B → A revisit). Bufferless
facts (`ThemeFacts`, the minibuffer prompt) and per-frontend state facts (`ThemeFacts`, `FontFacts`, the minibuffer prompt) and
(the gutter mode) survive snapshots on both sides, and the per-frontend state (the gutter mode) survive snapshots on both sides
(frontend-locally the normalized code scroll — a caret-follow view
residual — is buffer-scoped and resets, while the resolved font and
derived metrics survive), and the
instance's stale-store diagnostic-count freeze is store knowledge, instance's stale-store diagnostic-count freeze is store knowledge,
not session state — the re-sent `StatusFacts` after a snapshot not session state — the re-sent `StatusFacts` after a snapshot
carries the frozen counts, never zeros, including for a session carries the frozen counts, never zeros, including for a session
@ -245,6 +248,28 @@ ResourceOffer {
ThemeFacts { ThemeFacts {
faces: Vec<ThemeFace>, // { name: String, style: Style }, sorted by name faces: Vec<ThemeFace>, // { name: String, style: Style }, sorted by name
}, },
/// The GLOBAL font preference (protocol v17, Arc 4 stage 2,
/// docs/gpu-set-font-framing.md), written by `pmacs.gpu.set_font`.
/// Bufferless and authoritative per attachment: every session's
/// first frame after viewport declaration carries the current
/// preference — the all-default `(None, None)` included, never
/// inferred from silence — and it is epoch-gated/cached-compare
/// suppressed thereafter, so an unchanged preference costs one
/// small message per attachment. `BufferSnapshot` resets never
/// touch it on either side. The daemon relays a PREFERENCE only
/// (no pixels): the frontend resolves the family locally
/// (monospace-gated, total fallback to its sanitized default) and
/// owns every metric consequence; sizes travel as integer
/// hundredths of a logical pixel (1600 = 16.0, validated to
/// 600..=7200 on BOTH sides — the receiver fails closed on
/// out-of-range wire values). Daemon-gated `>= 17`; appended as
/// the FINAL variant — postcard discriminants are ordinal, and the
/// ThemeFacts byte pin above guards this placement.
FontFacts {
family: Option<String>, // None = the frontend's default family
size_centi_px: Option<u32>, // None = the frontend's default size
},
``` ```
Each family member diffs against the previous frame the same way Each family member diffs against the previous frame the same way

View File

@ -7559,9 +7559,6 @@ pub fn install_process(lua: &Lua, supervisor: &SharedProcessSupervisor) -> mlua:
Ok(()) Ok(())
} }
/// Build a fresh [`ProcessSupervisor`] and install
/// `pmacs.process.*` over it. Mirrors [`make_async_runtime`] /
/// [`make_syntax_registry`] in shape.
/// `pmacs.gpu.*` — GPU frontend preferences (Arc 4 stage 2, framing /// `pmacs.gpu.*` — GPU frontend preferences (Arc 4 stage 2, framing
/// Q#F2). Installs the module and returns the shared preference /// Q#F2). Installs the module and returns the shared preference
/// handle the `semantic_render` producer reads. Called from /// handle the `semantic_render` producer reads. Called from
@ -7677,6 +7674,9 @@ fn validate_font_size(size: f64) -> mlua::Result<u32> {
Ok((size * 100.0).round() as u32) Ok((size * 100.0).round() as u32)
} }
/// Build a fresh [`ProcessSupervisor`] and install
/// `pmacs.process.*` over it. Mirrors [`make_async_runtime`] /
/// [`make_syntax_registry`] in shape.
pub fn make_process_supervisor(lua: &Lua) -> mlua::Result<SharedProcessSupervisor> { pub fn make_process_supervisor(lua: &Lua) -> mlua::Result<SharedProcessSupervisor> {
let supervisor = Rc::new(RefCell::new(ProcessSupervisor::new())); let supervisor = Rc::new(RefCell::new(ProcessSupervisor::new()));
install_process(lua, &supervisor)?; install_process(lua, &supervisor)?;

View File

@ -0,0 +1,492 @@
// gpu_font_acceptance.rs --- gpu-set-font Arc 4 stage 2 acceptance
// (docs/gpu-set-font-framing.md, acceptance items 27; item 1's pins
// live in src/protocol.rs, item 8's TUI drop arm is a unit in
// src/frontend.rs, items 914 and 1619 are GPU routes in pmacs-gpu's
// headless suite, and item 15 is the docs themselves).
//! The `pmacs.gpu.set_font` preference + the `FontFacts` wire channel
//! (protocol v17).
//!
//! Wire claims drive a `SemanticRenderState` frame by frame (the
//! `ThemeFacts` discipline: authoritative per attachment, epoch-gated,
//! silent when unchanged); the Lua contract is exercised against the
//! real `pmacs.gpu` module installed by `EditorState::new`; the
//! version gate exercises a real daemon; init.lua reachability goes
//! through the real `load_user_config_at`.
use pmacs::editor::EditorState;
use pmacs::protocol::{ByteRange, FrontendId, InstanceMessage};
use pmacs::semantic_render::SemanticRenderState;
#[cfg(feature = "crdt")]
mod common;
// ---------------------------------------------------------------------------
// Harness (theme_faces_acceptance conventions)
// ---------------------------------------------------------------------------
fn exec(s: &EditorState, src: &str) {
s.lua_host.lua().load(src.to_string()).exec().unwrap();
}
fn exec_err(s: &EditorState, src: &str) -> mlua::Error {
s.lua_host
.lua()
.load(src.to_string())
.exec()
.expect_err("chunk must error")
}
fn eval<T: mlua::FromLuaMulti>(s: &EditorState, src: &str) -> T {
s.lua_host.lua().load(src.to_string()).eval().unwrap()
}
/// Fresh editor with LSP spawning disabled.
fn editor() -> EditorState {
let s = EditorState::new();
exec(&s, "pmacs.lsp.config = {}");
s
}
fn active_buffer(state: &EditorState) -> pmacs::buffer::BufferId {
state.core.borrow().active_window().buffer_id
}
fn semantic(state: &EditorState) -> SemanticRenderState {
let buffer_id = active_buffer(state);
let mut s = SemanticRenderState::new(FrontendId::LOCAL);
s.set_viewport(
buffer_id,
ByteRange {
start: 0,
end: 1 << 20,
},
0,
);
s
}
fn font_facts_of(msgs: &[InstanceMessage]) -> Option<(Option<String>, Option<u32>)> {
msgs.iter().find_map(|m| match m {
InstanceMessage::FontFacts {
family,
size_centi_px,
} => Some((family.clone(), *size_centi_px)),
_ => None,
})
}
fn font_facts_count(msgs: &[InstanceMessage]) -> usize {
msgs.iter()
.filter(|m| matches!(m, InstanceMessage::FontFacts { .. }))
.count()
}
/// The daemon-side preference triple `(family, size_centi_px, epoch)`.
fn pref_of(state: &EditorState) -> (Option<String>, Option<u32>, u64) {
let pref = state.font_pref.lock().expect("font pref lock");
(pref.family.clone(), pref.size_centi_px, pref.epoch)
}
// ---------------------------------------------------------------------------
// 2 — authoritative default per attachment; unchanged ticks silent;
// late joiner receives the current preference
// ---------------------------------------------------------------------------
#[test]
fn first_frame_ships_the_authoritative_default_and_then_stays_silent() {
let state = editor();
let mut sem = semantic(&state);
let first = sem.render_frame(&state);
assert_eq!(
font_facts_of(&first),
Some((None, None)),
"a fresh attachment's first frame carries the REAL (None, None) default — \
never inferred from silence"
);
for tick in 0..3 {
let next = sem.render_frame(&state);
assert_eq!(
font_facts_count(&next),
0,
"unchanged tick {tick} must be FontFacts-silent"
);
}
// `state` untouched since the set below — a late-joining second
// session receives the current preference without any mutation
// post-attach.
exec(
&state,
r#"pmacs.gpu.set_font { family = "Iosevka", size = 18 }"#,
);
let mut late = semantic(&state);
let joined = late.render_frame(&state);
assert_eq!(
font_facts_of(&joined),
Some((Some("Iosevka".to_owned()), Some(1800))),
"a late joiner's first frame carries the current preference"
);
}
// ---------------------------------------------------------------------------
// 3 — live re-ship: one FontFacts on the next frame; an identical
// re-set advances the epoch without emitting
// ---------------------------------------------------------------------------
#[test]
fn set_font_reships_once_and_identical_resets_advance_the_epoch_silently() {
let state = editor();
let mut sem = semantic(&state);
let _ = sem.render_frame(&state);
exec(&state, "pmacs.gpu.set_font { size = 18 }");
let next = sem.render_frame(&state);
assert_eq!(
font_facts_count(&next),
1,
"a mid-session set emits exactly one FontFacts on the next frame"
);
assert_eq!(font_facts_of(&next), Some((None, Some(1800))));
let (_, _, epoch_before) = pref_of(&state);
exec(&state, "pmacs.gpu.set_font { size = 18 }");
let (_, _, epoch_after) = pref_of(&state);
assert_eq!(
epoch_after,
epoch_before + 1,
"an identical re-set still advances the epoch (caches advance on \
computation, Q#TH6)"
);
let silent = sem.render_frame(&state);
assert_eq!(
font_facts_count(&silent),
0,
"an identical payload does not re-emit"
);
}
// ---------------------------------------------------------------------------
// 4 — snapshot survival, producer side: the buffer-baseline reset
// re-ships buffer facts but never the bufferless FontFacts
// ---------------------------------------------------------------------------
#[test]
fn buffer_snapshot_reset_never_reships_font_facts() {
let state = editor();
exec(&state, "pmacs.gpu.set_font { size = 20 }");
let buffer_id = active_buffer(&state);
let mut sem = semantic(&state);
let first = sem.render_frame(&state);
assert_eq!(
font_facts_of(&first),
Some((None, Some(2000))),
"precondition: the preference shipped on the first frame"
);
let has_status = |msgs: &[InstanceMessage]| {
msgs.iter()
.any(|m| matches!(m, InstanceMessage::StatusFacts { .. }))
};
assert!(has_status(&first), "precondition: buffer facts shipped too");
// The daemon wrote a BufferSnapshot for this buffer (an A → B → A
// revisit): every BUFFER-scoped baseline resets…
sem.on_buffer_snapshot_sent(buffer_id);
let after = sem.render_frame(&state);
assert!(
has_status(&after),
"the reset re-ships the buffer's facts on the next frame"
);
// …but the global font baseline survives: the new buffer shapes
// under the same preference without a redundant global fact.
assert_eq!(
font_facts_count(&after),
0,
"FontFacts is bufferless — the snapshot reset must not touch it"
);
}
// ---------------------------------------------------------------------------
// 5 — the daemon version gate (v16 peer never receives FontFacts)
// ---------------------------------------------------------------------------
#[cfg(feature = "crdt")]
#[test]
fn v16_peer_never_receives_font_facts_and_v17_does() {
use common::daemon::{TestDaemon, build_default_caps};
use pmacs::cell::CellSize;
use pmacs::protocol::{AttachRequest, FrontendCapabilities, FrontendEvent, Hello};
use pmacs::transport::{read_message, write_message};
use std::time::{Duration, Instant};
fn semantic_caps() -> FrontendCapabilities {
FrontendCapabilities {
multi_frontend: true,
crdt_replica: true,
semantic_render: true,
..build_default_caps()
}
}
/// Attach a semantic session at `version`, declare a viewport,
/// and report `(saw_font_facts, saw_style_spans)` within the
/// deadline.
fn probe(daemon: &TestDaemon, version: u32) -> (bool, bool) {
let mut stream = daemon.connect();
stream
.set_read_timeout(Some(Duration::from_millis(250)))
.unwrap();
let hello: Hello = read_message(&mut stream).expect("read Hello");
let fid = hello.assigned_frontend_id;
write_message(
&mut stream,
&AttachRequest {
protocol_version: version,
frontend_capabilities: semantic_caps(),
initial_size: CellSize::new(24, 80),
},
)
.expect("write AttachRequest");
let mut buf = None;
let learn_by = Instant::now() + Duration::from_secs(2);
while Instant::now() < learn_by && buf.is_none() {
if let Ok(InstanceMessage::BufferSnapshot { buffer_id, .. }) =
read_message::<InstanceMessage>(&mut stream)
{
buf = Some(buffer_id);
}
}
let buffer_id = buf.expect("received a BufferSnapshot");
write_message(
&mut stream,
&FrontendEvent::Viewport {
frontend_id: fid,
buffer_id,
visible: ByteRange {
start: 0,
end: 4096,
},
generation: 0,
},
)
.expect("write Viewport");
let deadline = Instant::now() + Duration::from_secs(3);
let (mut saw_facts, mut saw_spans) = (false, false);
while Instant::now() < deadline && !(saw_facts && saw_spans) {
match read_message::<InstanceMessage>(&mut stream) {
Ok(InstanceMessage::FontFacts { .. }) => saw_facts = true,
Ok(InstanceMessage::StyleSpans { .. }) => saw_spans = true,
Ok(_) | Err(_) => {}
}
}
(saw_facts, saw_spans)
}
let daemon = TestDaemon::spawn();
let (v17_facts, v17_spans) = probe(&daemon, 17);
assert!(v17_spans, "a v17 semantic session receives StyleSpans");
assert!(
v17_facts,
"a v17 semantic session receives the authoritative FontFacts"
);
let (v16_facts, v16_spans) = probe(&daemon, 16);
assert!(v16_spans, "a v16 peer still receives StyleSpans");
assert!(
!v16_facts,
"the daemon skip arm must keep FontFacts off a v16 wire"
);
}
// ---------------------------------------------------------------------------
// 6 — the Lua contract: strict plain data, all-or-nothing
// ---------------------------------------------------------------------------
#[test]
fn set_font_rejects_bad_sizes_naming_the_field_and_nothing_lands() {
let state = editor();
let mut sem = semantic(&state);
let _ = sem.render_frame(&state);
let before = pref_of(&state);
for bad in [
"pmacs.gpu.set_font { size = 5.999 }", // must error, not round into range
"pmacs.gpu.set_font { size = 72.01 }",
"pmacs.gpu.set_font { size = 0 }",
"pmacs.gpu.set_font { size = -16 }",
"pmacs.gpu.set_font { size = 0/0 }", // NaN
"pmacs.gpu.set_font { size = 1/0 }", // +inf
"pmacs.gpu.set_font { size = '16' }", // non-number
] {
let err = exec_err(&state, bad);
assert!(
err.to_string().contains("`size`"),
"{bad}: the error names the offending field, got: {err}"
);
}
assert_eq!(pref_of(&state), before, "no failed set may land");
let silent = sem.render_frame(&state);
assert_eq!(
font_facts_count(&silent),
0,
"no failed set may emit on the wire"
);
}
#[test]
fn set_font_rejects_bad_families_and_unknown_keys_by_name() {
let state = editor();
let err = exec_err(&state, "pmacs.gpu.set_font { family = '' }");
assert!(
err.to_string().contains("`family`"),
"empty family names the field: {err}"
);
let err = exec_err(&state, "pmacs.gpu.set_font { family = 12 }");
assert!(
err.to_string().contains("`family`"),
"non-string family names the field: {err}"
);
let err = exec_err(&state, "pmacs.gpu.set_font { size = 18, sise = 20 }");
assert!(
err.to_string().contains("`sise`"),
"an unknown key is rejected by NAME: {err}"
);
assert_eq!(
pref_of(&state),
(None, None, 0),
"every rejected shape leaves the preference untouched"
);
}
#[test]
fn set_font_never_consults_metatables() {
let state = editor();
// A hostile `__index` that answers every lookup: raw reads must
// never see its values, and raw iteration must never invoke it.
exec(
&state,
r#"
_G.__mt_hits = 0
local spec = setmetatable({ size = 18 }, {
__index = function(_, _)
_G.__mt_hits = _G.__mt_hits + 1
return "Injected Family"
end,
__pairs = function()
_G.__mt_hits = _G.__mt_hits + 1
return function() return nil end
end,
})
pmacs.gpu.set_font(spec)
"#,
);
let hits: i64 = eval(&state, "return _G.__mt_hits");
assert_eq!(hits, 0, "metatables are never invoked");
assert_eq!(
pref_of(&state),
(None, Some(1800), 1),
"the metatable's `family` answer must NOT be injected — only the \
raw `size` landed"
);
}
#[test]
fn set_font_quantizes_both_sides_of_a_hundredth_and_empty_resets() {
let state = editor();
exec(&state, "pmacs.gpu.set_font { size = 15.994 }");
assert_eq!(pref_of(&state).1, Some(1599), "15.994 rounds DOWN");
exec(&state, "pmacs.gpu.set_font { size = 15.996 }");
assert_eq!(pref_of(&state).1, Some(1600), "15.996 rounds UP");
// Boundary values are in range and quantize exactly.
exec(&state, "pmacs.gpu.set_font { size = 6 }");
assert_eq!(pref_of(&state).1, Some(600));
exec(&state, "pmacs.gpu.set_font { size = 72 }");
assert_eq!(pref_of(&state).1, Some(7200));
exec(
&state,
r#"pmacs.gpu.set_font { family = "Iosevka", size = 18 }"#,
);
exec(&state, "pmacs.gpu.set_font {}");
let (family, size, _) = pref_of(&state);
assert_eq!(
(family, size),
(None, None),
"set_font {{}} resets BOTH axes to the frontend default"
);
}
#[test]
fn font_getter_returns_a_fresh_quantized_plain_table() {
let state = editor();
exec(&state, "pmacs.gpu.set_font { size = 15.996 }");
let (size, fresh, no_family): (f64, bool, bool) = eval(
&state,
r"
local a = pmacs.gpu.font()
local b = pmacs.gpu.font()
a.size = 999 -- scribbling on the returned table
local c = pmacs.gpu.font()
return c.size, rawequal(a, b) == false, c.family == nil
",
);
assert!(
(size - 16.0).abs() < f64::EPSILON,
"the getter reports the QUANTIZED value (1600 → 16.0), got {size}"
);
assert!(
fresh,
"each call returns a fresh table, never a stored handle"
);
assert!(
no_family,
"an unset axis is absent, and scribbles don't stick"
);
}
// ---------------------------------------------------------------------------
// 7 — init.lua reachability: the module installs BEFORE user config
// ---------------------------------------------------------------------------
#[test]
fn init_lua_set_font_lands_in_the_preference_the_first_frame_reads() {
use pmacs::config::load_user_config_at;
use pmacs::lua::LuaHost;
let dir = tempfile::TempDir::new().expect("tempdir");
std::fs::write(
dir.path().join("init.lua"),
r#"pmacs.gpu.set_font { family = "Iosevka", size = 18 }"#,
)
.expect("write init.lua");
let mut host = LuaHost::new().expect("LuaHost::new");
// Mirror `EditorState::new`'s ordering: the pmacs.gpu module
// installs BEFORE user config runs (src/editor.rs), so an
// init.lua set_font lands in the same state the first
// attachment's producer reads.
let handle = pmacs::lua_bindings::make_font_pref(host.lua()).expect("install pmacs.gpu");
load_user_config_at(&mut host, dir.path());
host.set_init_complete();
assert!(
host.errors().is_empty(),
"init.lua produced errors: {:?}",
host.errors()
);
let pref = handle.lock().expect("font pref lock");
assert_eq!(pref.family.as_deref(), Some("Iosevka"));
assert_eq!(pref.size_centi_px, Some(1800));
assert_eq!(pref.epoch, 1, "exactly the init.lua set landed");
}
/// The producer half of item 7: a preference already in place before
/// the first attachment (the init.lua timing) ships on that
/// attachment's FIRST frame.
#[test]
fn preference_set_before_attach_ships_on_the_first_frame() {
let state = editor();
exec(
&state,
r#"pmacs.gpu.set_font { family = "Iosevka", size = 18 }"#,
);
let mut sem = semantic(&state);
let first = sem.render_frame(&state);
assert_eq!(
font_facts_of(&first),
Some((Some("Iosevka".to_owned()), Some(1800))),
"the first frame ships the pre-attach preference"
);
}