Skip to main content

eval_magic/adapters/
registry.rs

1//! The harness descriptor registry — the resolution authority for harness
2//! identifiers.
3//!
4//! Descriptor sources load into label-keyed entries, and every way the rest of
5//! the crate names a harness funnels through them: [`Harness::resolve`] turns
6//! a string into a validated handle (resolving `--harness` after parsing, and
7//! behind the artifact `Deserialize`), [`Harness::known`] enumerates the
8//! entries, and [`adapter_for`] serves each handle's [`HarnessAdapter`].
9
10use std::path::Path;
11use std::sync::{LazyLock, OnceLock};
12
13use crate::core::Harness;
14
15use super::descriptor::layers::{
16    DescriptorSource, HarnessFileError, Layer, check_user_layer_restrictions, default_config_root,
17    discover_sources, embedded_sources,
18};
19use super::descriptor::{
20    DescriptorError, HarnessDescriptor, finalize_descriptor, merge_descriptor_value,
21    parse_descriptor_value,
22};
23use super::descriptor_adapter::DescriptorAdapter;
24use super::harness::{HarnessAdapter, ToolVocabulary};
25
26/// One registry entry: a harness identity (the descriptor's `label`), the
27/// descriptor sources that contributed to it (for `harness list`/`show`
28/// provenance), the merged descriptor value (the base `harness lint` merges
29/// candidate overlays onto), and its descriptor-backed adapter.
30#[derive(Debug)]
31struct RegistryEntry {
32    label: &'static str,
33    sources: Vec<(Layer, String)>,
34    value: serde_json::Value,
35    adapter: DescriptorAdapter,
36}
37
38impl RegistryEntry {
39    /// True when any contributing source is an embedded built-in — the
40    /// provenance check behind guard gating (guards are embedded-only).
41    fn has_embedded_layer(&self) -> bool {
42        self.sources
43            .iter()
44            .any(|(layer, _)| *layer == Layer::Embedded)
45    }
46}
47
48/// Set once by `init_registry` (layered sources), or lazily to the embedded
49/// built-ins on first pre-init access. The lazy fallback keeps unit tests
50/// hermetic: they never call `init_registry`, so user-supplied descriptor
51/// layers can't leak into them.
52static REGISTRY: OnceLock<Vec<RegistryEntry>> = OnceLock::new();
53
54fn registry() -> &'static Vec<RegistryEntry> {
55    REGISTRY.get_or_init(|| {
56        // The embedded descriptors are bundled and known-valid, so a failure
57        // here is a programmer error (a bad descriptor edit) and panics —
58        // mirroring the bundled-schema panics in `validation::schema` — with
59        // the descriptor's own actionable message.
60        build_registry(embedded_sources())
61            .unwrap_or_else(|e| panic!("bundled harness descriptor is invalid: {e}"))
62            .entries
63    })
64}
65
66/// The registry could not be initialized from the layered sources.
67#[derive(Debug, thiserror::Error)]
68pub enum RegistryInitError {
69    #[error(transparent)]
70    HarnessFile(#[from] HarnessFileError),
71    #[error(transparent)]
72    Build(#[from] RegistryBuildError),
73    #[error("the harness registry is already initialized")]
74    AlreadyInitialized,
75}
76
77/// When `--harness-file` is in play, its descriptor's label — consulted by
78/// `Default for Harness` so the one-off harness doesn't have to be repeated
79/// via `--harness`.
80static SESSION_DEFAULT_HARNESS: OnceLock<&'static str> = OnceLock::new();
81
82/// Initialize the registry from every descriptor layer: embedded built-ins →
83/// user-global (`<config-root>/harnesses/*.toml`) → project-local
84/// (`<cwd>/.eval-magic/harnesses/*.toml`) → the optional one-off
85/// `--harness-file`. Called once by the CLI entry point, before anything
86/// resolves a harness.
87///
88/// Discovered files that fail to load are skipped with a warning on stderr
89/// (pointing at `harness lint`); a broken `--harness-file` is a hard error.
90/// When `--harness-file` names a descriptor, its label becomes the session's
91/// default harness.
92pub fn init_registry(harness_file: Option<&Path>) -> Result<(), RegistryInitError> {
93    let project_root = std::env::current_dir().unwrap_or_default();
94    let (sources, io_warnings) = discover_sources(
95        default_config_root().as_deref(),
96        &project_root,
97        harness_file,
98    )?;
99    let built = build_registry(sources)?;
100    for warning in io_warnings.iter().chain(&built.warnings) {
101        eprintln!("⚠ {warning}");
102    }
103    if harness_file.is_some()
104        && let Some(entry) = built
105            .entries
106            .iter()
107            .find(|e| e.sources.iter().any(|(l, _)| *l == Layer::HarnessFile))
108    {
109        let _ = SESSION_DEFAULT_HARNESS.set(entry.label);
110    }
111    REGISTRY
112        .set(built.entries)
113        .map_err(|_| RegistryInitError::AlreadyInitialized)
114}
115
116/// A descriptor source set that cannot form a registry. Only embedded and
117/// `--harness-file` sources can produce this — discovered layer files fail
118/// soft (skipped with a warning in [`BuiltRegistry::warnings`]) so a broken
119/// user descriptor never bricks the CLI.
120#[derive(Debug, thiserror::Error)]
121pub enum RegistryBuildError {
122    #[error(transparent)]
123    Descriptor(#[from] DescriptorError),
124    #[error("duplicate harness label {label:?}: {first} and {second}")]
125    DuplicateLabel {
126        label: String,
127        first: String,
128        second: String,
129    },
130}
131
132/// The outcome of loading a layered source set: the label-keyed entries plus
133/// the warnings for every discovered file that was skipped.
134#[derive(Debug)]
135struct BuiltRegistry {
136    entries: Vec<RegistryEntry>,
137    warnings: Vec<String>,
138}
139
140/// One label's in-progress state while folding layered sources.
141struct PendingEntry {
142    label: String,
143    value: serde_json::Value,
144    descriptor: HarnessDescriptor,
145    sources: Vec<(Layer, String)>,
146}
147
148/// Load descriptor sources into label-keyed entries with layered overrides.
149///
150/// Sources arrive in precedence order. The first source for a label defines
151/// it; a later source with the same label from a *different* layer merges
152/// field-by-field on top ([`merge_descriptor_value`]) and the merged result is
153/// re-validated with the full provenance chain in its error messages. Two
154/// same-label files within one layer have no precedence between them, so the
155/// second is rejected (fatal for embedded, a skip-warning otherwise).
156///
157/// Failure policy per layer: embedded sources are known-valid (a failure is a
158/// programmer error, propagated), `--harness-file` was explicitly named (a
159/// failure is the user's answer, propagated), and discovered user/project
160/// files fail soft — the file is skipped, the message lands in
161/// [`BuiltRegistry::warnings`], and any earlier state for that label survives.
162fn build_registry(sources: Vec<DescriptorSource>) -> Result<BuiltRegistry, RegistryBuildError> {
163    let mut pending: Vec<PendingEntry> = Vec::new();
164    let mut warnings: Vec<String> = Vec::new();
165    for source in sources {
166        let strict = matches!(source.layer, Layer::Embedded | Layer::HarnessFile);
167        let mut fail_soft = |e: RegistryBuildError| -> Result<(), RegistryBuildError> {
168            if strict {
169                Err(e)
170            } else {
171                warnings.push(format!(
172                    "skipping harness descriptor: {e}\n  (run `eval-magic harness lint <file>` \
173                     for the full report)"
174                ));
175                Ok(())
176            }
177        };
178
179        let value = match parse_descriptor_value(&source.toml_src, &source.path) {
180            Ok(value) => value,
181            Err(e) => {
182                fail_soft(e.into())?;
183                continue;
184            }
185        };
186        if source.layer != Layer::Embedded
187            && let Err(e) = check_user_layer_restrictions(&value, &source.path)
188        {
189            fail_soft(e.into())?;
190            continue;
191        }
192        let label = value
193            .get("label")
194            .and_then(serde_json::Value::as_str)
195            .expect("the schema gate requires a string label")
196            .to_string();
197
198        match pending.iter().position(|p| p.label == label) {
199            None => match finalize_descriptor(&value, &source.path) {
200                Ok(descriptor) => pending.push(PendingEntry {
201                    label,
202                    value,
203                    descriptor,
204                    sources: vec![(source.layer, source.path)],
205                }),
206                Err(e) => fail_soft(e.into())?,
207            },
208            Some(index) => {
209                let entry = &mut pending[index];
210                let (last_layer, last_path) = entry
211                    .sources
212                    .last()
213                    .expect("every entry records its source");
214                if *last_layer == source.layer {
215                    fail_soft(RegistryBuildError::DuplicateLabel {
216                        label,
217                        first: last_path.clone(),
218                        second: source.path,
219                    })?;
220                    continue;
221                }
222                let mut merged = entry.value.clone();
223                merge_descriptor_value(&mut merged, value);
224                let provenance = provenance_chain(&entry.sources, &source);
225                match finalize_descriptor(&merged, &provenance) {
226                    Ok(descriptor) => {
227                        entry.value = merged;
228                        entry.descriptor = descriptor;
229                        entry.sources.push((source.layer, source.path));
230                    }
231                    Err(e) => fail_soft(e.into())?,
232                }
233            }
234        }
235    }
236
237    let entries = pending
238        .into_iter()
239        .map(|p| RegistryEntry {
240            // Leaked once per registry entry per process: the label becomes
241            // the `'static` identity the rest of the crate passes around by
242            // handle.
243            label: Box::leak(p.label.into_boxed_str()),
244            sources: p.sources,
245            value: p.value,
246            adapter: DescriptorAdapter::from_descriptor(p.descriptor),
247        })
248        .collect();
249    Ok(BuiltRegistry { entries, warnings })
250}
251
252/// `base.toml (built-in) + overlay.toml (project)` — the provenance string a
253/// merged descriptor's validation errors carry.
254fn provenance_chain(sources: &[(Layer, String)], next: &DescriptorSource) -> String {
255    sources
256        .iter()
257        .map(|(layer, path)| format!("{path} ({})", layer.display_name()))
258        .chain([format!("{} ({})", next.path, next.layer.display_name())])
259        .collect::<Vec<_>>()
260        .join(" + ")
261}
262
263/// The registry-level default harness — what `--harness` falls back to when
264/// absent. A registry concept rather than a descriptor field, so layered
265/// user-supplied descriptor files (#136) can never fight over an
266/// exactly-one-default invariant.
267pub const DEFAULT_HARNESS_NAME: &str = "claude-code";
268
269/// A harness name that no registry entry matches. Names every registered
270/// harness so the rejection is actionable wherever it surfaces (artifact
271/// deserialization, direct resolution).
272#[derive(Debug, thiserror::Error)]
273#[error("unknown harness '{name}'; known harnesses: {}", known.join(", "))]
274pub struct UnknownHarnessError {
275    pub name: String,
276    pub known: Vec<&'static str>,
277}
278
279impl Harness {
280    /// Resolve a kebab-case identifier against the descriptor registry — the
281    /// only way to obtain a [`Harness`], so every held handle is valid.
282    pub fn resolve(name: &str) -> Result<Harness, UnknownHarnessError> {
283        registry()
284            .iter()
285            .find(|e| e.label == name)
286            .map(|e| Harness::from_static_name(e.label))
287            .ok_or_else(|| UnknownHarnessError {
288                name: name.to_string(),
289                known: Harness::known().map(Harness::name).collect(),
290            })
291    }
292
293    /// Every registered harness, in registry (embedded descriptor) order —
294    /// for code that must sweep all of them (e.g. guard teardown scans each
295    /// harness's skills dir for a marker).
296    pub fn known() -> impl Iterator<Item = Harness> {
297        registry()
298            .iter()
299            .map(|e| Harness::from_static_name(e.label))
300    }
301}
302
303impl Default for Harness {
304    fn default() -> Self {
305        // A `--harness-file` descriptor becomes the session default, so the
306        // one-off harness doesn't have to be repeated via `--harness`.
307        Harness::resolve(default_harness_name())
308            .expect("the session default resolves against the registry")
309    }
310}
311
312impl<'de> serde::Deserialize<'de> for Harness {
313    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
314        let name = String::deserialize(deserializer)?;
315        Harness::resolve(&name).map_err(serde::de::Error::custom)
316    }
317}
318
319/// Resolve the adapter for a [`Harness`]. This is the single dispatch point on
320/// the harness identifier for all harness-specific behavior; every other
321/// module goes through the returned trait object.
322pub fn adapter_for(harness: Harness) -> &'static dyn HarnessAdapter {
323    &registry()
324        .iter()
325        .find(|e| e.label == harness.name())
326        .expect("Harness handles originate from the registry")
327        .adapter
328}
329
330/// True when `harness` has an embedded built-in descriptor among its sources
331/// — i.e. it is not defined by user-supplied descriptor files alone. Preflight
332/// uses this to hard-reject `--guard` on user-only harnesses (the write guard
333/// stays restricted to built-ins because it fails open).
334pub fn has_embedded_layer(harness: Harness) -> bool {
335    registry()
336        .iter()
337        .find(|e| e.label == harness.name())
338        .expect("Harness handles originate from the registry")
339        .has_embedded_layer()
340}
341
342/// One registered harness as `harness list`/`show`/`lint` see it: identity,
343/// contributing sources in layer order, the merged descriptor value, and the
344/// resolved descriptor.
345pub struct HarnessInfo {
346    pub label: &'static str,
347    pub sources: &'static [(Layer, String)],
348    pub value: &'static serde_json::Value,
349    pub descriptor: &'static HarnessDescriptor,
350}
351
352/// Every registered harness in registry order, with provenance and the
353/// resolved descriptor — the data source for the `harness` subcommands.
354pub fn harness_info() -> impl Iterator<Item = HarnessInfo> {
355    registry().iter().map(|e| HarnessInfo {
356        label: e.label,
357        sources: &e.sources,
358        value: &e.value,
359        descriptor: e.adapter.descriptor(),
360    })
361}
362
363/// The effective default harness name: the `--harness-file` descriptor's
364/// label when one is loaded, else [`DEFAULT_HARNESS_NAME`].
365pub fn default_harness_name() -> &'static str {
366    SESSION_DEFAULT_HARNESS
367        .get()
368        .copied()
369        .unwrap_or(DEFAULT_HARNESS_NAME)
370}
371
372/// The union of every harness's project-local config dir names (sorted,
373/// deduplicated): the dirs harness-agnostic code must treat as protected —
374/// staging's sibling-asset filter, the guard's Bash tamper rule, and
375/// detect-stray-writes' staging-dir lookbehind.
376pub fn all_config_dir_names() -> Vec<String> {
377    let mut names: Vec<String> = registry()
378        .iter()
379        .flat_map(|e| e.adapter.config_dir_names())
380        .collect();
381    names.sort_unstable();
382    names.dedup();
383    names
384}
385
386/// The union of every harness's tool vocabulary (each list sorted,
387/// deduplicated). Computed once behind a `LazyLock` — the guard arbiter
388/// consults it on every hooked tool call.
389pub fn all_tool_vocabulary() -> &'static ToolVocabulary {
390    static ALL: LazyLock<ToolVocabulary> = LazyLock::new(|| {
391        let mut union = ToolVocabulary::default();
392        for entry in registry().iter() {
393            let vocab = entry.adapter.tool_vocabulary();
394            union.write_tools.extend(vocab.write_tools);
395            union.patch_tools.extend(vocab.patch_tools);
396            union.shell_tools.extend(vocab.shell_tools);
397            union.read_tools.extend(vocab.read_tools);
398        }
399        for list in [
400            &mut union.write_tools,
401            &mut union.patch_tools,
402            &mut union.shell_tools,
403            &mut union.read_tools,
404        ] {
405            list.sort_unstable();
406            list.dedup();
407        }
408        union
409    });
410    &ALL
411}
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416
417    use crate::adapters::descriptor::EMBEDDED_DESCRIPTORS;
418    use crate::adapters::descriptor::layers::{Layer, embedded_sources};
419
420    #[test]
421    fn all_config_dir_names_unions_every_adapter() {
422        assert_eq!(
423            all_config_dir_names(),
424            [".agents", ".claude", ".codex", ".opencode"]
425        );
426    }
427
428    #[test]
429    fn all_tool_vocabulary_unions_every_adapter() {
430        let vocab = all_tool_vocabulary();
431        assert_eq!(
432            vocab.write_tools,
433            ["Edit", "MultiEdit", "NotebookEdit", "Write", "file_change"]
434        );
435        assert_eq!(vocab.patch_tools, ["apply_patch"]);
436        assert_eq!(vocab.shell_tools, ["Bash", "command_execution"]);
437        assert_eq!(vocab.read_tools, ["Glob", "Grep", "Read"]);
438    }
439
440    fn src(layer: Layer, path: &str, toml_src: &str) -> DescriptorSource {
441        DescriptorSource {
442            layer,
443            path: path.to_string(),
444            toml_src: toml_src.to_string(),
445        }
446    }
447
448    /// A schema-valid user descriptor declaring a [guard] block — must be
449    /// rejected by the user-layer restriction, not the schema gate.
450    const USER_GUARD_TOML: &str = r#"
451label = "armed"
452
453[guard]
454hooks_file = ".armed/hooks.json"
455matcher = "Write"
456command_template = '"{exe}" guard-hook --harness armed "{marker}"'
457hook_entry = '{"matcher":"{matcher}","hooks":[{"type":"command","command":"{command}"}]}'
458verdict_template = '{"decision":"block","reason":"{reason}"}'
459armed_message = "x"
460"#;
461
462    #[test]
463    fn duplicate_embedded_label_errors() {
464        let mut sources = embedded_sources();
465        sources.push(sources[0].clone());
466        let err = build_registry(sources).unwrap_err().to_string();
467        assert!(err.contains("duplicate harness label"), "{err}");
468        assert!(err.contains("claude-code"), "names the label: {err}");
469        assert!(
470            err.contains("harnesses/claude-code.toml"),
471            "names the colliding source files: {err}"
472        );
473    }
474
475    #[test]
476    fn registry_entries_record_embedded_provenance() {
477        let built = build_registry(embedded_sources()).unwrap();
478        assert!(built.warnings.is_empty(), "{:?}", built.warnings);
479        assert_eq!(built.entries.len(), EMBEDDED_DESCRIPTORS.len());
480        for entry in &built.entries {
481            assert_eq!(entry.sources.len(), 1, "one contributing file per built-in");
482            assert_eq!(entry.sources[0].0, Layer::Embedded);
483            assert!(
484                entry.sources[0].1.contains(entry.label),
485                "source path names the harness: {}",
486                entry.sources[0].1
487            );
488        }
489    }
490
491    #[test]
492    fn project_layer_overrides_a_single_field_of_a_builtin() {
493        let mut sources = embedded_sources();
494        sources.push(src(
495            Layer::ProjectLocal,
496            ".eval-magic/harnesses/claude-code.toml",
497            "label = \"claude-code\"\n\n[model]\nflag = \"--model-x\"\n",
498        ));
499        let built = build_registry(sources).unwrap();
500        assert!(built.warnings.is_empty(), "{:?}", built.warnings);
501        assert_eq!(built.entries.len(), EMBEDDED_DESCRIPTORS.len());
502        let entry = built
503            .entries
504            .iter()
505            .find(|e| e.label == "claude-code")
506            .unwrap();
507        // The overridden field changed; everything else survives from the
508        // embedded base (field-level merge, not file shadowing).
509        assert_eq!(entry.adapter.cli_model_flag(), Some("--model-x".into()));
510        assert!(entry.adapter.run_capabilities().supports_guard);
511        assert_eq!(
512            entry.sources,
513            vec![
514                (Layer::Embedded, "harnesses/claude-code.toml".to_string()),
515                (
516                    Layer::ProjectLocal,
517                    ".eval-magic/harnesses/claude-code.toml".to_string()
518                ),
519            ]
520        );
521    }
522
523    #[test]
524    fn new_label_in_a_user_layer_registers_a_new_harness() {
525        let mut sources = embedded_sources();
526        sources.push(src(
527            Layer::ProjectLocal,
528            ".eval-magic/harnesses/cool.toml",
529            "label = \"cool-custom-harness\"\n",
530        ));
531        let built = build_registry(sources).unwrap();
532        assert!(built.warnings.is_empty(), "{:?}", built.warnings);
533        let entry = built
534            .entries
535            .iter()
536            .find(|e| e.label == "cool-custom-harness")
537            .expect("new harness registered");
538        assert_eq!(entry.sources[0].0, Layer::ProjectLocal);
539        assert!(entry.adapter.skills_dir(Path::new("/r")).is_none());
540    }
541
542    #[test]
543    fn discovered_file_declaring_a_guard_warns_and_is_skipped() {
544        let mut sources = embedded_sources();
545        sources.push(src(
546            Layer::ProjectLocal,
547            ".eval-magic/harnesses/armed.toml",
548            USER_GUARD_TOML,
549        ));
550        let built = build_registry(sources).unwrap();
551        assert_eq!(
552            built.entries.len(),
553            EMBEDDED_DESCRIPTORS.len(),
554            "file skipped"
555        );
556        assert_eq!(built.warnings.len(), 1);
557        assert!(
558            built.warnings[0].contains("may not declare [guard]"),
559            "{}",
560            built.warnings[0]
561        );
562    }
563
564    #[test]
565    fn discovered_overlay_breaking_invariants_warns_and_keeps_the_base() {
566        let mut sources = embedded_sources();
567        // Replacing config_dirs orphans skills_dir's parent — an invariant
568        // break that only shows post-merge.
569        sources.push(src(
570            Layer::ProjectLocal,
571            ".eval-magic/harnesses/claude-code.toml",
572            "label = \"claude-code\"\nconfig_dirs = [\".other\"]\n",
573        ));
574        let built = build_registry(sources).unwrap();
575        assert_eq!(built.warnings.len(), 1);
576        assert!(
577            built.warnings[0].contains(".eval-magic/harnesses/claude-code.toml"),
578            "names the offending file: {}",
579            built.warnings[0]
580        );
581        let entry = built
582            .entries
583            .iter()
584            .find(|e| e.label == "claude-code")
585            .unwrap();
586        assert_eq!(
587            entry.adapter.config_dir_names(),
588            vec![".claude".to_string()],
589            "embedded base survives the dropped overlay"
590        );
591        assert_eq!(
592            entry.sources.len(),
593            1,
594            "the bad overlay records no provenance"
595        );
596    }
597
598    #[test]
599    fn same_label_twice_in_one_discovered_layer_warns_and_skips_the_second() {
600        let mut sources = embedded_sources();
601        sources.push(src(
602            Layer::ProjectLocal,
603            "a.toml",
604            "label = \"claude-code\"\n\n[model]\nflag = \"--from-a\"\n",
605        ));
606        sources.push(src(
607            Layer::ProjectLocal,
608            "b.toml",
609            "label = \"claude-code\"\n\n[model]\nflag = \"--from-b\"\n",
610        ));
611        let built = build_registry(sources).unwrap();
612        assert_eq!(built.warnings.len(), 1);
613        assert!(
614            built.warnings[0].contains("a.toml"),
615            "{}",
616            built.warnings[0]
617        );
618        assert!(
619            built.warnings[0].contains("b.toml"),
620            "{}",
621            built.warnings[0]
622        );
623        let entry = built
624            .entries
625            .iter()
626            .find(|e| e.label == "claude-code")
627            .unwrap();
628        assert_eq!(entry.adapter.cli_model_flag(), Some("--from-a".into()));
629    }
630
631    #[test]
632    fn harness_file_failures_are_fatal() {
633        let mut sources = embedded_sources();
634        sources.push(src(Layer::HarnessFile, "one-off.toml", "label = "));
635        let err = build_registry(sources).unwrap_err().to_string();
636        assert!(err.contains("one-off.toml"), "{err}");
637    }
638
639    #[test]
640    fn harness_file_guard_rejection_is_fatal() {
641        let mut sources = embedded_sources();
642        sources.push(src(Layer::HarnessFile, "one-off.toml", USER_GUARD_TOML));
643        let err = build_registry(sources).unwrap_err().to_string();
644        assert!(err.contains("may not declare [guard]"), "{err}");
645    }
646
647    #[test]
648    fn harness_file_overlay_merges_on_top_of_discovered_layers() {
649        let mut sources = embedded_sources();
650        sources.push(src(
651            Layer::ProjectLocal,
652            "p.toml",
653            "label = \"claude-code\"\n\n[model]\nflag = \"--from-project\"\n",
654        ));
655        sources.push(src(
656            Layer::HarnessFile,
657            "one-off.toml",
658            "label = \"claude-code\"\n\n[model]\nflag = \"--from-file\"\n",
659        ));
660        let built = build_registry(sources).unwrap();
661        assert!(built.warnings.is_empty(), "{:?}", built.warnings);
662        let entry = built
663            .entries
664            .iter()
665            .find(|e| e.label == "claude-code")
666            .unwrap();
667        assert_eq!(entry.adapter.cli_model_flag(), Some("--from-file".into()));
668        assert_eq!(entry.sources.len(), 3);
669    }
670
671    #[test]
672    fn embedded_layer_provenance_distinguishes_built_ins_from_user_only_harnesses() {
673        let mut sources = embedded_sources();
674        sources.push(src(
675            Layer::ProjectLocal,
676            ".eval-magic/harnesses/cool.toml",
677            "label = \"cool\"\n",
678        ));
679        let built = build_registry(sources).unwrap();
680        let entry = |label: &str| built.entries.iter().find(|e| e.label == label).unwrap();
681        assert!(
682            entry("claude-code").has_embedded_layer(),
683            "built-ins carry their embedded source"
684        );
685        assert!(
686            !entry("cool").has_embedded_layer(),
687            "a user-only harness has no embedded layer"
688        );
689    }
690
691    #[test]
692    fn resolve_unknown_name_lists_known_harnesses() {
693        let err = Harness::resolve("nonexistent").unwrap_err().to_string();
694        assert!(err.contains("unknown harness 'nonexistent'"), "{err}");
695        for name in ["claude-code", "codex", "opencode"] {
696            assert!(err.contains(name), "error must name {name}: {err}");
697        }
698    }
699
700    #[test]
701    fn resolve_round_trips_every_registry_entry() {
702        for harness in Harness::known() {
703            assert_eq!(Harness::resolve(harness.name()).unwrap(), harness);
704        }
705    }
706
707    #[test]
708    fn default_harness_is_claude_code() {
709        assert_eq!(DEFAULT_HARNESS_NAME, "claude-code");
710        assert_eq!(Harness::default().name(), DEFAULT_HARNESS_NAME);
711    }
712
713    #[test]
714    fn known_iterates_in_descriptor_order() {
715        let names: Vec<_> = Harness::known().map(Harness::name).collect();
716        assert_eq!(names, ["claude-code", "codex", "opencode"]);
717    }
718
719    #[test]
720    fn labels_match_kebab_case_identifiers() {
721        for name in ["claude-code", "codex", "opencode"] {
722            let harness = Harness::resolve(name).unwrap();
723            assert_eq!(adapter_for(harness).label(), name);
724        }
725    }
726}