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            [
434                "Edit",
435                "MultiEdit",
436                "NotebookEdit",
437                "Write",
438                "edit",
439                "file_change",
440                "write"
441            ]
442        );
443        assert_eq!(vocab.patch_tools, ["apply_patch"]);
444        assert_eq!(vocab.shell_tools, ["Bash", "bash", "command_execution"]);
445        assert_eq!(
446            vocab.read_tools,
447            ["Glob", "Grep", "Read", "glob", "grep", "read"]
448        );
449    }
450
451    fn src(layer: Layer, path: &str, toml_src: &str) -> DescriptorSource {
452        DescriptorSource {
453            layer,
454            path: path.to_string(),
455            toml_src: toml_src.to_string(),
456        }
457    }
458
459    /// A schema-valid user descriptor declaring a [guard] block — must be
460    /// rejected by the user-layer restriction, not the schema gate.
461    const USER_GUARD_TOML: &str = r#"
462label = "armed"
463
464[guard]
465hooks_file = ".armed/hooks.json"
466matcher = "Write"
467command_template = '"{exe}" guard-hook --harness armed "{marker}"'
468hook_entry = '{"matcher":"{matcher}","hooks":[{"type":"command","command":"{command}"}]}'
469verdict_template = '{"decision":"block","reason":"{reason}"}'
470armed_message = "x"
471"#;
472
473    #[test]
474    fn duplicate_embedded_label_errors() {
475        let mut sources = embedded_sources();
476        sources.push(sources[0].clone());
477        let err = build_registry(sources).unwrap_err().to_string();
478        assert!(err.contains("duplicate harness label"), "{err}");
479        assert!(err.contains("claude-code"), "names the label: {err}");
480        assert!(
481            err.contains("harnesses/claude-code.toml"),
482            "names the colliding source files: {err}"
483        );
484    }
485
486    #[test]
487    fn registry_entries_record_embedded_provenance() {
488        let built = build_registry(embedded_sources()).unwrap();
489        assert!(built.warnings.is_empty(), "{:?}", built.warnings);
490        assert_eq!(built.entries.len(), EMBEDDED_DESCRIPTORS.len());
491        for entry in &built.entries {
492            assert_eq!(entry.sources.len(), 1, "one contributing file per built-in");
493            assert_eq!(entry.sources[0].0, Layer::Embedded);
494            assert!(
495                entry.sources[0].1.contains(entry.label),
496                "source path names the harness: {}",
497                entry.sources[0].1
498            );
499        }
500    }
501
502    #[test]
503    fn project_layer_overrides_a_single_field_of_a_builtin() {
504        let mut sources = embedded_sources();
505        sources.push(src(
506            Layer::ProjectLocal,
507            ".eval-magic/harnesses/claude-code.toml",
508            "label = \"claude-code\"\n\n[model]\nflag = \"--model-x\"\n",
509        ));
510        let built = build_registry(sources).unwrap();
511        assert!(built.warnings.is_empty(), "{:?}", built.warnings);
512        assert_eq!(built.entries.len(), EMBEDDED_DESCRIPTORS.len());
513        let entry = built
514            .entries
515            .iter()
516            .find(|e| e.label == "claude-code")
517            .unwrap();
518        // The overridden field changed; everything else survives from the
519        // embedded base (field-level merge, not file shadowing).
520        assert_eq!(entry.adapter.cli_model_flag(), Some("--model-x".into()));
521        assert!(entry.adapter.run_capabilities().supports_guard);
522        assert_eq!(
523            entry.sources,
524            vec![
525                (Layer::Embedded, "harnesses/claude-code.toml".to_string()),
526                (
527                    Layer::ProjectLocal,
528                    ".eval-magic/harnesses/claude-code.toml".to_string()
529                ),
530            ]
531        );
532    }
533
534    #[test]
535    fn new_label_in_a_user_layer_registers_a_new_harness() {
536        let mut sources = embedded_sources();
537        sources.push(src(
538            Layer::ProjectLocal,
539            ".eval-magic/harnesses/cool.toml",
540            "label = \"cool-custom-harness\"\n",
541        ));
542        let built = build_registry(sources).unwrap();
543        assert!(built.warnings.is_empty(), "{:?}", built.warnings);
544        let entry = built
545            .entries
546            .iter()
547            .find(|e| e.label == "cool-custom-harness")
548            .expect("new harness registered");
549        assert_eq!(entry.sources[0].0, Layer::ProjectLocal);
550        assert!(entry.adapter.skills_dir(Path::new("/r")).is_none());
551    }
552
553    #[test]
554    fn discovered_file_declaring_a_guard_warns_and_is_skipped() {
555        let mut sources = embedded_sources();
556        sources.push(src(
557            Layer::ProjectLocal,
558            ".eval-magic/harnesses/armed.toml",
559            USER_GUARD_TOML,
560        ));
561        let built = build_registry(sources).unwrap();
562        assert_eq!(
563            built.entries.len(),
564            EMBEDDED_DESCRIPTORS.len(),
565            "file skipped"
566        );
567        assert_eq!(built.warnings.len(), 1);
568        assert!(
569            built.warnings[0].contains("may not declare [guard]"),
570            "{}",
571            built.warnings[0]
572        );
573    }
574
575    #[test]
576    fn discovered_overlay_breaking_invariants_warns_and_keeps_the_base() {
577        let mut sources = embedded_sources();
578        // Replacing config_dirs orphans skills_dir's parent — an invariant
579        // break that only shows post-merge.
580        sources.push(src(
581            Layer::ProjectLocal,
582            ".eval-magic/harnesses/claude-code.toml",
583            "label = \"claude-code\"\nconfig_dirs = [\".other\"]\n",
584        ));
585        let built = build_registry(sources).unwrap();
586        assert_eq!(built.warnings.len(), 1);
587        assert!(
588            built.warnings[0].contains(".eval-magic/harnesses/claude-code.toml"),
589            "names the offending file: {}",
590            built.warnings[0]
591        );
592        let entry = built
593            .entries
594            .iter()
595            .find(|e| e.label == "claude-code")
596            .unwrap();
597        assert_eq!(
598            entry.adapter.config_dir_names(),
599            vec![".claude".to_string()],
600            "embedded base survives the dropped overlay"
601        );
602        assert_eq!(
603            entry.sources.len(),
604            1,
605            "the bad overlay records no provenance"
606        );
607    }
608
609    #[test]
610    fn same_label_twice_in_one_discovered_layer_warns_and_skips_the_second() {
611        let mut sources = embedded_sources();
612        sources.push(src(
613            Layer::ProjectLocal,
614            "a.toml",
615            "label = \"claude-code\"\n\n[model]\nflag = \"--from-a\"\n",
616        ));
617        sources.push(src(
618            Layer::ProjectLocal,
619            "b.toml",
620            "label = \"claude-code\"\n\n[model]\nflag = \"--from-b\"\n",
621        ));
622        let built = build_registry(sources).unwrap();
623        assert_eq!(built.warnings.len(), 1);
624        assert!(
625            built.warnings[0].contains("a.toml"),
626            "{}",
627            built.warnings[0]
628        );
629        assert!(
630            built.warnings[0].contains("b.toml"),
631            "{}",
632            built.warnings[0]
633        );
634        let entry = built
635            .entries
636            .iter()
637            .find(|e| e.label == "claude-code")
638            .unwrap();
639        assert_eq!(entry.adapter.cli_model_flag(), Some("--from-a".into()));
640    }
641
642    #[test]
643    fn harness_file_failures_are_fatal() {
644        let mut sources = embedded_sources();
645        sources.push(src(Layer::HarnessFile, "one-off.toml", "label = "));
646        let err = build_registry(sources).unwrap_err().to_string();
647        assert!(err.contains("one-off.toml"), "{err}");
648    }
649
650    #[test]
651    fn harness_file_guard_rejection_is_fatal() {
652        let mut sources = embedded_sources();
653        sources.push(src(Layer::HarnessFile, "one-off.toml", USER_GUARD_TOML));
654        let err = build_registry(sources).unwrap_err().to_string();
655        assert!(err.contains("may not declare [guard]"), "{err}");
656    }
657
658    #[test]
659    fn harness_file_overlay_merges_on_top_of_discovered_layers() {
660        let mut sources = embedded_sources();
661        sources.push(src(
662            Layer::ProjectLocal,
663            "p.toml",
664            "label = \"claude-code\"\n\n[model]\nflag = \"--from-project\"\n",
665        ));
666        sources.push(src(
667            Layer::HarnessFile,
668            "one-off.toml",
669            "label = \"claude-code\"\n\n[model]\nflag = \"--from-file\"\n",
670        ));
671        let built = build_registry(sources).unwrap();
672        assert!(built.warnings.is_empty(), "{:?}", built.warnings);
673        let entry = built
674            .entries
675            .iter()
676            .find(|e| e.label == "claude-code")
677            .unwrap();
678        assert_eq!(entry.adapter.cli_model_flag(), Some("--from-file".into()));
679        assert_eq!(entry.sources.len(), 3);
680    }
681
682    #[test]
683    fn embedded_layer_provenance_distinguishes_built_ins_from_user_only_harnesses() {
684        let mut sources = embedded_sources();
685        sources.push(src(
686            Layer::ProjectLocal,
687            ".eval-magic/harnesses/cool.toml",
688            "label = \"cool\"\n",
689        ));
690        let built = build_registry(sources).unwrap();
691        let entry = |label: &str| built.entries.iter().find(|e| e.label == label).unwrap();
692        assert!(
693            entry("claude-code").has_embedded_layer(),
694            "built-ins carry their embedded source"
695        );
696        assert!(
697            !entry("cool").has_embedded_layer(),
698            "a user-only harness has no embedded layer"
699        );
700    }
701
702    #[test]
703    fn resolve_unknown_name_lists_known_harnesses() {
704        let err = Harness::resolve("nonexistent").unwrap_err().to_string();
705        assert!(err.contains("unknown harness 'nonexistent'"), "{err}");
706        for name in ["claude-code", "codex", "opencode"] {
707            assert!(err.contains(name), "error must name {name}: {err}");
708        }
709    }
710
711    #[test]
712    fn resolve_round_trips_every_registry_entry() {
713        for harness in Harness::known() {
714            assert_eq!(Harness::resolve(harness.name()).unwrap(), harness);
715        }
716    }
717
718    #[test]
719    fn default_harness_is_claude_code() {
720        assert_eq!(DEFAULT_HARNESS_NAME, "claude-code");
721        assert_eq!(Harness::default().name(), DEFAULT_HARNESS_NAME);
722    }
723
724    #[test]
725    fn known_iterates_in_descriptor_order() {
726        let names: Vec<_> = Harness::known().map(Harness::name).collect();
727        assert_eq!(names, ["claude-code", "codex", "opencode"]);
728    }
729
730    #[test]
731    fn labels_match_kebab_case_identifiers() {
732        for name in ["claude-code", "codex", "opencode"] {
733            let harness = Harness::resolve(name).unwrap();
734            assert_eq!(adapter_for(harness).label(), name);
735        }
736    }
737}