tapes-harnesses 0.1.0

Shared, open-source client-side harness knowledge for Tapes capture: the harness registry, launch recipes, config patch grammars, plugin artifacts, per-harness session attribution, and transcript discovery.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
//! The Codex desktop app's hook-plugin manifests, as templates.
//!
//! Capturing the desktop app needs a Codex *plugin*: a manifest naming the
//! plugin plus a hooks file subscribing a command to the five lifecycle
//! boundaries [`crate::attribution::codex_app`] parses. Unlike pi's extension
//! (a fixed file, installed by copying), a Codex plugin is installed by
//! Codex's own plugin manager from a consumer-packaged source directory, and
//! two of its ingredients are irreducibly the consumer's:
//!
//! * **The hook command line.** Each hook runs an executable that receives
//!   the lifecycle payload on stdin and reports it to the consumer's local
//!   runtime. Which executable, and how it is located without depending on
//!   the app's `PATH`, is deployment knowledge — the branded launcher script a
//!   consumer ships is exactly the part that cannot live here.
//! * **The plugin's identity.** The name, description, and developer strings
//!   Codex shows the user must say who is actually asking for hook trust.
//!
//! So the crate ships the manifests as **templates**: the JSON structure and
//! the event set are crate-owned (and pinned against the attribution module's
//! event list), while the command and identity are slots the consumer fills
//! through [`render_hooks_manifest`] and [`render_plugin_manifest`]. Both
//! installers — a closed-source one today, a tapesctl installer later — render
//! the same
//! bytes around their own strings, which is the same anti-drift bargain the
//! pi asset struck, adapted to a plugin that cannot be vendor-complete.
//!
//! The templates carry no endpoint and read no environment: a hook plugin is
//! inert until the *rendered command* does something, so the inertness
//! obligation [`crate::plugin::GATEWAY_URL_ENV`] discharges for pi rests here
//! on the consumer's command instead.
//!
//! Rendered manifests are still not an *installed* plugin. [`manager`] owns
//! the rest: the marketplace wrapper that makes them installable, and the
//! `codex` CLI invocation that installs them.

pub mod manager;

use super::slots::render_slots;

/// Slot in [`HOOKS_MANIFEST_TEMPLATE`] that a consumer's hook command line
/// replaces. The slot is the entire JSON string value, so substitution is
/// JSON-escaped by [`render_hooks_manifest`]; a consumer never edits the
/// template text itself.
pub const HOOK_COMMAND_SLOT: &str = "__TAPES_HOOK_COMMAND__";

/// The hooks manifest template — `hooks/hooks.json` in the packaged plugin.
///
/// Structure is Codex's hook-file contract: one key per lifecycle event, each
/// holding a single registration with a single `type: "command"` hook whose
/// command is [`HOOK_COMMAND_SLOT`].
pub const HOOKS_MANIFEST_TEMPLATE: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/assets/codex-app/hooks.json"
));

/// The plugin manifest template — `.codex-plugin/plugin.json` in the packaged
/// plugin. Identity fields are slots for [`HookPluginIdentity`]; the manifest
/// deliberately registers no tool, app, or skill, so an installed plugin is
/// hook-only by construction.
pub const PLUGIN_MANIFEST_TEMPLATE: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/assets/codex-app/plugin.json"
));

/// The two manifests a consumer packages into its plugin source directory,
/// as the registry hands them out.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct HookPluginTemplates {
    /// [`PLUGIN_MANIFEST_TEMPLATE`], destined for `.codex-plugin/plugin.json`.
    pub plugin_manifest: &'static str,
    /// [`HOOKS_MANIFEST_TEMPLATE`], destined for `hooks/hooks.json`.
    pub hooks_manifest: &'static str,
}

/// The Codex desktop app's manifest templates.
pub const CODEX_APP_TEMPLATES: HookPluginTemplates = HookPluginTemplates {
    plugin_manifest: PLUGIN_MANIFEST_TEMPLATE,
    hooks_manifest: HOOKS_MANIFEST_TEMPLATE,
};

/// The consumer-supplied identity a rendered plugin manifest presents to the
/// user in Codex's plugin UI.
///
/// All fields are plain strings; [`render_plugin_manifest`] JSON-escapes them,
/// so quotes and backslashes in any field are safe.
///
/// Build one with [`HookPluginIdentity::new`] and the `with_*` setters. The
/// fields stay public — reading and patching one is useful, and the fixture
/// oracle style elsewhere in the crate relies on it — but the type is
/// `#[non_exhaustive]`, so a struct literal only compiles inside this crate.
/// Without the constructor a downstream installer got E0639 and could not call
/// [`render_plugin_manifest`] at all, which is the whole public point of the
/// module.
///
/// # Examples
///
/// ```
/// use tapes_harnesses::plugin::codex_app::{HookPluginIdentity, render_plugin_manifest};
///
/// let identity = HookPluginIdentity::new("acme-codex", "0.1.0")
///     .with_display_name("Acme for Codex")
///     .with_developer_name("Acme");
/// let manifest = render_plugin_manifest(&identity);
/// assert!(!manifest.contains("__TAPES_"));
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct HookPluginIdentity<'a> {
    /// The plugin id — what Codex records trust and enablement against.
    pub name: &'a str,
    /// Plugin version. Bumping it is how a consumer invalidates the app's
    /// cached copy of an installed plugin.
    pub version: &'a str,
    /// One-line description shown beside the plugin.
    pub description: &'a str,
    /// Display name in the plugin UI.
    pub display_name: &'a str,
    /// Short marketplace description.
    pub short_description: &'a str,
    /// Long marketplace description.
    pub long_description: &'a str,
    /// Developer/author name shown to the user granting hook trust.
    pub developer_name: &'a str,
}

impl<'a> HookPluginIdentity<'a> {
    /// A hook plugin's identity, from the two fields that carry meaning
    /// beyond presentation.
    ///
    /// `name` is what Codex records trust and enablement against, and
    /// `version` is how a consumer invalidates the app's cached copy of an
    /// installed plugin — get either wrong and an install misbehaves, so they
    /// are the arguments rather than defaults.
    ///
    /// The five remaining fields are strings Codex only *shows*, and each
    /// starts as `name`. That default is deliberate: every slot in
    /// [`PLUGIN_MANIFEST_TEMPLATE`] must be filled or a literal
    /// `__TAPES_PLUGIN_…` string appears in the user-facing plugin UI, so the
    /// worst outcome of a forgotten `with_*` call is a repetitive UI, never a
    /// blank field and never a leaked slot. Override each with its setter.
    #[must_use]
    pub const fn new(name: &'a str, version: &'a str) -> Self {
        Self {
            name,
            version,
            description: name,
            display_name: name,
            short_description: name,
            long_description: name,
            developer_name: name,
        }
    }

    /// Set the one-line description shown beside the plugin.
    #[must_use]
    pub const fn with_description(mut self, description: &'a str) -> Self {
        self.description = description;
        self
    }

    /// Set the display name shown in the plugin UI.
    #[must_use]
    pub const fn with_display_name(mut self, display_name: &'a str) -> Self {
        self.display_name = display_name;
        self
    }

    /// Set the short marketplace description.
    #[must_use]
    pub const fn with_short_description(mut self, short_description: &'a str) -> Self {
        self.short_description = short_description;
        self
    }

    /// Set the long marketplace description.
    #[must_use]
    pub const fn with_long_description(mut self, long_description: &'a str) -> Self {
        self.long_description = long_description;
        self
    }

    /// Set the developer/author name shown to the user granting hook trust.
    #[must_use]
    pub const fn with_developer_name(mut self, developer_name: &'a str) -> Self {
        self.developer_name = developer_name;
        self
    }

    /// The slot each field fills, paired with its value. One table so the
    /// render loop and the template-coverage test share a single spelling.
    fn slots(&self) -> [(&'static str, &str); 7] {
        [
            ("__TAPES_PLUGIN_NAME__", self.name),
            ("__TAPES_PLUGIN_VERSION__", self.version),
            ("__TAPES_PLUGIN_DESCRIPTION__", self.description),
            ("__TAPES_PLUGIN_DISPLAY_NAME__", self.display_name),
            ("__TAPES_PLUGIN_SHORT_DESCRIPTION__", self.short_description),
            ("__TAPES_PLUGIN_LONG_DESCRIPTION__", self.long_description),
            ("__TAPES_PLUGIN_DEVELOPER_NAME__", self.developer_name),
        ]
    }
}

/// Render the hooks manifest with the consumer's hook command line.
///
/// The command is substituted as a JSON string value, escaping included, so a
/// command containing quotes, backslashes (a Windows path), or `${...}`
/// expansions passes through byte-exact to Codex.
#[must_use]
pub fn render_hooks_manifest(hook_command: &str) -> String {
    render_slots(
        HOOKS_MANIFEST_TEMPLATE,
        &[(HOOK_COMMAND_SLOT, hook_command)],
    )
}

/// Render the plugin manifest with the consumer's identity strings.
#[must_use]
pub fn render_plugin_manifest(identity: &HookPluginIdentity) -> String {
    render_slots(PLUGIN_MANIFEST_TEMPLATE, &identity.slots())
}

/// `value` as a single POSIX shell word.
///
/// Two places need this and they must not answer it differently: the hook
/// command a consumer renders into [`HOOKS_MANIFEST_TEMPLATE`] is executed by
/// a shell, and the recovery commands [`manager::PluginManager::manual_commands`]
/// prints are copied into one. A home directory containing a space is
/// ordinary, and either use getting it wrong silently changes the arguments —
/// the executed hook runs against the wrong path, the pasted command registers
/// the wrong directory.
///
/// Quoting is applied only when the value needs it, so ordinary paths and
/// plugin specs print bare. The safe set is a deliberately short allowlist —
/// ASCII alphanumerics plus `._-/@:+,=` — every member of which a POSIX shell
/// leaves alone in a non-leading word. Everything else, including the empty
/// string, is wrapped in single quotes, which suppress every expansion the
/// shell performs; the only character then needing care is the closing quote
/// itself, spliced out, escaped, and spliced back in.
#[must_use]
pub fn shell_quote(value: &str) -> String {
    let safe =
        |character: char| character.is_ascii_alphanumeric() || "._-/@:+,=".contains(character);
    if !value.is_empty() && value.chars().all(safe) {
        return value.to_owned();
    }
    format!("'{}'", value.replace('\'', r"'\''"))
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
    use super::*;
    use crate::attribution::codex_app::LIFECYCLE_EVENTS;
    use std::collections::BTreeMap;

    fn identity() -> HookPluginIdentity<'static> {
        // Built through the public constructor, not a struct literal: this is
        // the shape a downstream installer is limited to, so the whole
        // template-coverage suite below runs against it.
        HookPluginIdentity::new("acme-codex", "0.1.0")
            .with_description("Keeps Codex connected to acmed.")
            .with_display_name("Acme for Codex")
            .with_short_description("Keep Codex connected to Acme.")
            .with_long_description("Forwards lifecycle metadata to local acmed.")
            .with_developer_name("Acme")
    }

    /// A bare `new` fills every presentation slot with the plugin name. The
    /// property that matters is not the choice of default but that no slot is
    /// left unfilled: an unset field must never render as an empty string or
    /// as a literal `__TAPES_…` placeholder in the plugin UI.
    #[test]
    fn a_minimal_identity_fills_every_slot_with_the_plugin_name() {
        let rendered = render_plugin_manifest(&HookPluginIdentity::new("bare-codex", "2.0.0"));
        let parsed: serde_json::Value = serde_json::from_str(&rendered).unwrap();

        assert!(
            !rendered.contains("__TAPES_"),
            "a slot survived a minimal render: {rendered}"
        );
        assert_eq!(parsed["name"], "bare-codex");
        assert_eq!(parsed["version"], "2.0.0");
        assert_eq!(parsed["interface"]["displayName"], "bare-codex");
        assert_eq!(parsed["interface"]["developerName"], "bare-codex");
        assert_eq!(parsed["author"]["name"], "bare-codex");
    }

    /// Each setter reaches exactly one slot. Distinct values per field would
    /// pass even if two setters wrote the same slot, so assert the whole
    /// rendered mapping rather than one field at a time.
    #[test]
    fn each_setter_reaches_its_own_slot() {
        let identity = HookPluginIdentity::new("n", "v")
            .with_description("d")
            .with_display_name("dn")
            .with_short_description("sd")
            .with_long_description("ld")
            .with_developer_name("dev");

        assert_eq!(
            identity.slots().map(|(_, value)| value),
            ["n", "v", "d", "dn", "sd", "ld", "dev"],
        );
    }

    /// The shape Codex parses a hooks file into, mirrored here so the test
    /// fails if the template stops being a valid hook file rather than only
    /// if the JSON stops parsing.
    #[derive(Debug, serde::Deserialize)]
    #[serde(deny_unknown_fields)]
    struct HookFile {
        hooks: BTreeMap<String, Vec<HookRegistration>>,
    }

    #[derive(Debug, serde::Deserialize)]
    #[serde(deny_unknown_fields)]
    struct HookRegistration {
        hooks: Vec<CommandHook>,
    }

    #[derive(Debug, serde::Deserialize)]
    #[serde(deny_unknown_fields)]
    struct CommandHook {
        #[serde(rename = "type")]
        kind: String,
        command: String,
    }

    /// The rendered hooks file subscribes the supplied command to exactly the
    /// lifecycle events the attribution module parses — the two ends of the
    /// hook contract, pinned to one list.
    #[test]
    fn the_rendered_hooks_manifest_subscribes_the_command_to_every_lifecycle_event() {
        let command = r#"/bin/sh "${PLUGIN_ROOT}/scripts/capture-hook""#;
        let rendered = render_hooks_manifest(command);
        let parsed: HookFile = serde_json::from_str(&rendered).unwrap();

        let mut events: Vec<&str> = parsed.hooks.keys().map(String::as_str).collect();
        let mut expected: Vec<&str> = LIFECYCLE_EVENTS.to_vec();
        events.sort_unstable();
        expected.sort_unstable();
        assert_eq!(events, expected);

        for (event, registrations) in &parsed.hooks {
            assert_eq!(registrations.len(), 1, "{event} has multiple registrations");
            assert_eq!(registrations[0].hooks.len(), 1);
            assert_eq!(registrations[0].hooks[0].kind, "command");
            assert_eq!(
                registrations[0].hooks[0].command, command,
                "{event}'s command did not survive rendering byte-exact"
            );
        }
    }

    /// Substituted values are output, not template: an identity value that
    /// contains — or *is* — another slot's placeholder must survive
    /// verbatim, not get substituted itself. The sharpest case is exact
    /// equality: the value's own JSON-literal quotes complete the quoted
    /// `"__SLOT__"` pattern, so a sequential per-slot `replace` re-scanning
    /// its earlier insertions would swap the name for the version. Values
    /// merely embedding the spelling ride along as regression cover.
    #[test]
    fn a_value_containing_another_slots_placeholder_survives_verbatim() {
        let mut identity = identity();
        identity.name = "__TAPES_PLUGIN_VERSION__";
        identity.long_description = "mentions __TAPES_PLUGIN_NAME__ in prose";
        let rendered = render_plugin_manifest(&identity);
        let parsed: serde_json::Value = serde_json::from_str(&rendered).unwrap();

        assert_eq!(
            parsed["name"], "__TAPES_PLUGIN_VERSION__",
            "the name was re-substituted as if it were template text"
        );
        assert_eq!(
            parsed["interface"]["longDescription"],
            "mentions __TAPES_PLUGIN_NAME__ in prose"
        );
        // And the real slots still rendered normally around them.
        assert_eq!(parsed["version"], "0.1.0");
        assert_eq!(parsed["interface"]["displayName"], "Acme for Codex");
    }

    /// Same property on the hooks side: a command containing the command
    /// slot's own quoted spelling is emitted once, escaped, and the five
    /// real slots are the only things substituted.
    #[test]
    fn a_command_containing_the_slot_spelling_survives_verbatim() {
        let command = "run --note '\"__TAPES_HOOK_COMMAND__\"'";
        let rendered = render_hooks_manifest(command);
        let parsed: HookFile = serde_json::from_str(&rendered).unwrap();
        for registrations in parsed.hooks.values() {
            assert_eq!(registrations[0].hooks[0].command, command);
        }
    }

    /// The substitution is real JSON escaping, not text splicing: quotes and
    /// backslashes in the command round-trip through a JSON parse.
    #[test]
    fn rendering_escapes_the_command_as_a_json_string() {
        let command = "C:\\tools\\hook.exe --label \"two words\"\twith\ncontrol\u{1}chars";
        let rendered = render_hooks_manifest(command);
        let parsed: HookFile = serde_json::from_str(&rendered).unwrap();
        let registrations = parsed.hooks.get("Stop").unwrap();
        assert_eq!(registrations[0].hooks[0].command, command);
    }

    #[test]
    fn the_rendered_plugin_manifest_carries_the_identity_and_no_slots() {
        let rendered = render_plugin_manifest(&identity());
        let parsed: serde_json::Value = serde_json::from_str(&rendered).unwrap();

        assert_eq!(parsed["name"], "acme-codex");
        assert_eq!(parsed["version"], "0.1.0");
        assert_eq!(parsed["author"]["name"], "Acme");
        assert_eq!(parsed["interface"]["displayName"], "Acme for Codex");
        assert_eq!(parsed["interface"]["developerName"], "Acme");
        assert!(
            !rendered.contains("__TAPES_"),
            "an identity slot survived rendering: {rendered}"
        );
        // Hook-only by construction: the manifest points at no hooks path
        // override (default discovery finds hooks/hooks.json) and registers
        // no tool, app, or skill surface.
        for absent in ["hooks", "tools", "apps", "skills"] {
            assert!(
                parsed.get(absent).is_none(),
                "the manifest unexpectedly declares {absent:?}"
            );
        }
    }

    /// Every slot the identity fills exists in the template exactly once —
    /// except the developer name, which the manifest shows in two places —
    /// and no template carries a slot nothing fills. A drifted spelling
    /// would otherwise render a manifest with a literal `__TAPES_...` string
    /// in the user-facing plugin UI.
    #[test]
    fn identity_slots_and_template_slots_cover_each_other() {
        for (slot, _) in identity().slots() {
            assert!(
                PLUGIN_MANIFEST_TEMPLATE.contains(&format!("\"{slot}\"")),
                "template is missing slot {slot}"
            );
        }
        assert_eq!(
            PLUGIN_MANIFEST_TEMPLATE.matches("__TAPES_").count(),
            identity().slots().len() + 1, // developerName repeats author.name's slot
        );
        assert_eq!(
            HOOKS_MANIFEST_TEMPLATE.matches("__TAPES_").count(),
            LIFECYCLE_EVENTS.len(),
            "the hooks template must carry exactly one command slot per event"
        );
        assert!(HOOKS_MANIFEST_TEMPLATE.contains(&format!("\"{HOOK_COMMAND_SLOT}\"")));
    }

    /// The de-branding bar the pi asset set applies to these templates too:
    /// the crate-owned halves name no vendor. Branding enters only through
    /// the consumer's identity strings and command line.
    #[test]
    fn the_templates_carry_no_vendor_branding() {
        for template in [PLUGIN_MANIFEST_TEMPLATE, HOOKS_MANIFEST_TEMPLATE] {
            let lowered = template.to_ascii_lowercase();
            for token in ["paper", "papercompute"] {
                assert!(
                    !lowered.contains(token),
                    "a crate-owned template mentions {token:?}"
                );
            }
        }
    }

    /// Like the pi asset's no-built-in-endpoint rule: a template must not
    /// smuggle in a default destination. The only executable content in a
    /// rendered plugin is the consumer's command.
    #[test]
    fn the_templates_have_no_built_in_endpoint() {
        for template in [PLUGIN_MANIFEST_TEMPLATE, HOOKS_MANIFEST_TEMPLATE] {
            for literal in ["127.0.0.1:", "localhost:", "http://"] {
                assert!(
                    !template.contains(literal),
                    "a template hard-codes {literal:?}"
                );
            }
        }
    }

    /// The registry hands out these exact templates; a drifted copy would
    /// mean `find("codex-app")` and this module disagree about the bytes a
    /// consumer packages.
    #[test]
    fn the_registry_reaches_these_templates() {
        let harness = crate::harness::find("codex-app").expect("codex-app is registered");
        match harness.plugin() {
            crate::harness::PluginDelivery::HookManifestTemplates(templates) => {
                assert_eq!(*templates, CODEX_APP_TEMPLATES);
            }
            other => panic!("codex-app declares {other:?}, not hook manifest templates"),
        }
    }

    /// The quoter's contract, stated against a real shell rather than against
    /// an expected string: whatever it returns must come back out of `/bin/sh`
    /// as exactly one word equal to the input. Hard-coding the expected
    /// quoting would pass even if both the quoter and the expectation were
    /// wrong in the same way.
    #[cfg(unix)]
    #[test]
    fn a_quoted_value_returns_from_the_shell_as_one_unchanged_word() {
        for value in [
            "/tmp/plain/path",
            "acme-codex@acme",
            "",
            "/tmp/two words/plugin",
            "/tmp/it's here/plugin",
            "/tmp/$HOME/plugin",
            "/tmp/`whoami`/plugin",
            "/tmp/a;rm -rf b/plugin",
            "/tmp/new\nline/plugin",
            "/tmp/glob*?[x]/plugin",
            "~/not-expanded",
            "/tmp/\u{e9}t\u{e9}/plugin",
        ] {
            let output = std::process::Command::new("/bin/sh")
                .arg("-c")
                .arg(format!("printf '%s' {}", shell_quote(value)))
                .output()
                .unwrap();
            assert!(
                output.status.success(),
                "{value:?} produced unparseable shell text: {}",
                String::from_utf8_lossy(&output.stderr)
            );
            assert_eq!(
                String::from_utf8(output.stdout).unwrap(),
                value,
                "{value:?} did not survive the shell"
            );
        }
    }

    /// Values that need nothing are left alone, so ordinary printed commands
    /// stay readable. The allowlist is the whole reason this is safe, so it is
    /// pinned rather than left to inspection.
    #[test]
    fn only_values_needing_quotes_get_them() {
        for bare in ["plugin", "acme-codex@acme", "/a/b_c.d-e", "K=V", "a:b+c,d"] {
            assert_eq!(shell_quote(bare), bare);
        }
        for quoted in [
            "", " ", "a b", "a~b", "a*b", "a$b", "a'b", "a\\b", "a#b", "a%b",
        ] {
            assert!(
                shell_quote(quoted).starts_with('\''),
                "{quoted:?} was left unquoted"
            );
        }
    }
}