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/// The resolved merged descriptor value for a harness, suitable for persisting
331/// into a runner-owned dispatch artifact.
332pub fn descriptor_value_for(harness: Harness) -> &'static serde_json::Value {
333    &registry()
334        .iter()
335        .find(|entry| entry.label == harness.name())
336        .expect("Harness handles originate from the registry")
337        .value
338}
339
340/// True when `harness` has an embedded built-in descriptor among its sources
341/// — i.e. it is not defined by user-supplied descriptor files alone. Preflight
342/// uses this to hard-reject `--guard` on user-only harnesses (the write guard
343/// stays restricted to built-ins because it fails open).
344pub fn has_embedded_layer(harness: Harness) -> bool {
345    registry()
346        .iter()
347        .find(|e| e.label == harness.name())
348        .expect("Harness handles originate from the registry")
349        .has_embedded_layer()
350}
351
352/// One registered harness as `harness list`/`show`/`lint` see it: identity,
353/// contributing sources in layer order, the merged descriptor value, and the
354/// resolved descriptor.
355pub struct HarnessInfo {
356    pub label: &'static str,
357    pub sources: &'static [(Layer, String)],
358    pub value: &'static serde_json::Value,
359    pub descriptor: &'static HarnessDescriptor,
360}
361
362/// Every registered harness in registry order, with provenance and the
363/// resolved descriptor — the data source for the `harness` subcommands.
364pub fn harness_info() -> impl Iterator<Item = HarnessInfo> {
365    registry().iter().map(|e| HarnessInfo {
366        label: e.label,
367        sources: &e.sources,
368        value: &e.value,
369        descriptor: e.adapter.descriptor(),
370    })
371}
372
373/// The effective default harness name: the `--harness-file` descriptor's
374/// label when one is loaded, else [`DEFAULT_HARNESS_NAME`].
375pub fn default_harness_name() -> &'static str {
376    SESSION_DEFAULT_HARNESS
377        .get()
378        .copied()
379        .unwrap_or(DEFAULT_HARNESS_NAME)
380}
381
382/// The union of every harness's project-local config dir names (sorted,
383/// deduplicated): the dirs harness-agnostic code must treat as protected —
384/// staging's sibling-asset filter, the guard's Bash tamper rule, and
385/// detect-stray-writes' staging-dir lookbehind.
386pub fn all_config_dir_names() -> Vec<String> {
387    let mut names: Vec<String> = registry()
388        .iter()
389        .flat_map(|e| e.adapter.config_dir_names())
390        .collect();
391    names.sort_unstable();
392    names.dedup();
393    names
394}
395
396/// The union of every harness's tool vocabulary (each list sorted,
397/// deduplicated). Computed once behind a `LazyLock` — the guard arbiter
398/// consults it on every hooked tool call.
399pub fn all_tool_vocabulary() -> &'static ToolVocabulary {
400    static ALL: LazyLock<ToolVocabulary> = LazyLock::new(|| {
401        let mut union = ToolVocabulary::default();
402        for entry in registry().iter() {
403            let vocab = entry.adapter.tool_vocabulary();
404            union.write_tools.extend(vocab.write_tools);
405            union.patch_tools.extend(vocab.patch_tools);
406            union.shell_tools.extend(vocab.shell_tools);
407            union.read_tools.extend(vocab.read_tools);
408        }
409        for list in [
410            &mut union.write_tools,
411            &mut union.patch_tools,
412            &mut union.shell_tools,
413            &mut union.read_tools,
414        ] {
415            list.sort_unstable();
416            list.dedup();
417        }
418        union
419    });
420    &ALL
421}
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426
427    use crate::adapters::descriptor::EMBEDDED_DESCRIPTORS;
428    use crate::adapters::descriptor::layers::{Layer, embedded_sources};
429
430    #[test]
431    fn all_config_dir_names_unions_every_adapter() {
432        assert_eq!(
433            all_config_dir_names(),
434            [".agents", ".claude", ".codex", ".opencode"]
435        );
436    }
437
438    #[test]
439    fn all_tool_vocabulary_unions_every_adapter() {
440        let vocab = all_tool_vocabulary();
441        assert_eq!(
442            vocab.write_tools,
443            [
444                "Edit",
445                "MultiEdit",
446                "NotebookEdit",
447                "Write",
448                "edit",
449                "file_change",
450                "write"
451            ]
452        );
453        assert_eq!(vocab.patch_tools, ["apply_patch"]);
454        assert_eq!(vocab.shell_tools, ["Bash", "bash", "command_execution"]);
455        assert_eq!(
456            vocab.read_tools,
457            ["Glob", "Grep", "Read", "glob", "grep", "read"]
458        );
459    }
460
461    fn src(layer: Layer, path: &str, toml_src: &str) -> DescriptorSource {
462        DescriptorSource {
463            layer,
464            path: path.to_string(),
465            toml_src: toml_src.to_string(),
466        }
467    }
468
469    /// A schema-valid user descriptor declaring a [guard] block — must be
470    /// rejected by the user-layer restriction, not the schema gate.
471    const USER_GUARD_TOML: &str = r#"
472label = "armed"
473
474[guard]
475hooks_file = ".armed/hooks.json"
476matcher = "Write"
477command_template = '"{exe}" guard-hook --harness armed "{marker}"'
478hook_entry = '{"matcher":"{matcher}","hooks":[{"type":"command","command":"{command}"}]}'
479verdict_template = '{"decision":"block","reason":"{reason}"}'
480armed_message = "x"
481"#;
482
483    #[test]
484    fn duplicate_embedded_label_errors() {
485        let mut sources = embedded_sources();
486        sources.push(sources[0].clone());
487        let err = build_registry(sources).unwrap_err().to_string();
488        assert!(err.contains("duplicate harness label"), "{err}");
489        assert!(err.contains("claude-code"), "names the label: {err}");
490        assert!(
491            err.contains("harnesses/claude-code.toml"),
492            "names the colliding source files: {err}"
493        );
494    }
495
496    #[test]
497    fn registry_entries_record_embedded_provenance() {
498        let built = build_registry(embedded_sources()).unwrap();
499        assert!(built.warnings.is_empty(), "{:?}", built.warnings);
500        assert_eq!(built.entries.len(), EMBEDDED_DESCRIPTORS.len());
501        for entry in &built.entries {
502            assert_eq!(entry.sources.len(), 1, "one contributing file per built-in");
503            assert_eq!(entry.sources[0].0, Layer::Embedded);
504            assert!(
505                entry.sources[0].1.contains(entry.label),
506                "source path names the harness: {}",
507                entry.sources[0].1
508            );
509        }
510    }
511
512    #[test]
513    fn project_layer_overrides_a_single_field_of_a_builtin() {
514        let mut sources = embedded_sources();
515        sources.push(src(
516            Layer::ProjectLocal,
517            ".eval-magic/harnesses/claude-code.toml",
518            "label = \"claude-code\"\n\n[model]\nflag = \"--model-x\"\n",
519        ));
520        let built = build_registry(sources).unwrap();
521        assert!(built.warnings.is_empty(), "{:?}", built.warnings);
522        assert_eq!(built.entries.len(), EMBEDDED_DESCRIPTORS.len());
523        let entry = built
524            .entries
525            .iter()
526            .find(|e| e.label == "claude-code")
527            .unwrap();
528        // The overridden field changed; everything else survives from the
529        // embedded base (field-level merge, not file shadowing).
530        assert_eq!(entry.adapter.cli_model_flag(), Some("--model-x".into()));
531        assert!(entry.adapter.run_capabilities().supports_guard);
532        assert_eq!(
533            entry.sources,
534            vec![
535                (Layer::Embedded, "harnesses/claude-code.toml".to_string()),
536                (
537                    Layer::ProjectLocal,
538                    ".eval-magic/harnesses/claude-code.toml".to_string()
539                ),
540            ]
541        );
542    }
543
544    #[test]
545    fn new_label_in_a_user_layer_registers_a_new_harness() {
546        let mut sources = embedded_sources();
547        sources.push(src(
548            Layer::ProjectLocal,
549            ".eval-magic/harnesses/cool.toml",
550            "label = \"cool-custom-harness\"\n",
551        ));
552        let built = build_registry(sources).unwrap();
553        assert!(built.warnings.is_empty(), "{:?}", built.warnings);
554        let entry = built
555            .entries
556            .iter()
557            .find(|e| e.label == "cool-custom-harness")
558            .expect("new harness registered");
559        assert_eq!(entry.sources[0].0, Layer::ProjectLocal);
560        assert!(entry.adapter.skills_dir(Path::new("/r")).is_none());
561    }
562
563    #[test]
564    fn discovered_file_declaring_a_guard_warns_and_is_skipped() {
565        let mut sources = embedded_sources();
566        sources.push(src(
567            Layer::ProjectLocal,
568            ".eval-magic/harnesses/armed.toml",
569            USER_GUARD_TOML,
570        ));
571        let built = build_registry(sources).unwrap();
572        assert_eq!(
573            built.entries.len(),
574            EMBEDDED_DESCRIPTORS.len(),
575            "file skipped"
576        );
577        assert_eq!(built.warnings.len(), 1);
578        assert!(
579            built.warnings[0].contains("may not declare [guard]"),
580            "{}",
581            built.warnings[0]
582        );
583    }
584
585    #[test]
586    fn discovered_overlay_breaking_invariants_warns_and_keeps_the_base() {
587        let mut sources = embedded_sources();
588        // Replacing config_dirs orphans skills_dir's parent — an invariant
589        // break that only shows post-merge.
590        sources.push(src(
591            Layer::ProjectLocal,
592            ".eval-magic/harnesses/claude-code.toml",
593            "label = \"claude-code\"\nconfig_dirs = [\".other\"]\n",
594        ));
595        let built = build_registry(sources).unwrap();
596        assert_eq!(built.warnings.len(), 1);
597        assert!(
598            built.warnings[0].contains(".eval-magic/harnesses/claude-code.toml"),
599            "names the offending file: {}",
600            built.warnings[0]
601        );
602        let entry = built
603            .entries
604            .iter()
605            .find(|e| e.label == "claude-code")
606            .unwrap();
607        assert_eq!(
608            entry.adapter.config_dir_names(),
609            vec![".claude".to_string()],
610            "embedded base survives the dropped overlay"
611        );
612        assert_eq!(
613            entry.sources.len(),
614            1,
615            "the bad overlay records no provenance"
616        );
617    }
618
619    #[test]
620    fn same_label_twice_in_one_discovered_layer_warns_and_skips_the_second() {
621        let mut sources = embedded_sources();
622        sources.push(src(
623            Layer::ProjectLocal,
624            "a.toml",
625            "label = \"claude-code\"\n\n[model]\nflag = \"--from-a\"\n",
626        ));
627        sources.push(src(
628            Layer::ProjectLocal,
629            "b.toml",
630            "label = \"claude-code\"\n\n[model]\nflag = \"--from-b\"\n",
631        ));
632        let built = build_registry(sources).unwrap();
633        assert_eq!(built.warnings.len(), 1);
634        assert!(
635            built.warnings[0].contains("a.toml"),
636            "{}",
637            built.warnings[0]
638        );
639        assert!(
640            built.warnings[0].contains("b.toml"),
641            "{}",
642            built.warnings[0]
643        );
644        let entry = built
645            .entries
646            .iter()
647            .find(|e| e.label == "claude-code")
648            .unwrap();
649        assert_eq!(entry.adapter.cli_model_flag(), Some("--from-a".into()));
650    }
651
652    #[test]
653    fn harness_file_failures_are_fatal() {
654        let mut sources = embedded_sources();
655        sources.push(src(Layer::HarnessFile, "one-off.toml", "label = "));
656        let err = build_registry(sources).unwrap_err().to_string();
657        assert!(err.contains("one-off.toml"), "{err}");
658    }
659
660    #[test]
661    fn harness_file_guard_rejection_is_fatal() {
662        let mut sources = embedded_sources();
663        sources.push(src(Layer::HarnessFile, "one-off.toml", USER_GUARD_TOML));
664        let err = build_registry(sources).unwrap_err().to_string();
665        assert!(err.contains("may not declare [guard]"), "{err}");
666    }
667
668    #[test]
669    fn harness_file_overlay_merges_on_top_of_discovered_layers() {
670        let mut sources = embedded_sources();
671        sources.push(src(
672            Layer::ProjectLocal,
673            "p.toml",
674            "label = \"claude-code\"\n\n[model]\nflag = \"--from-project\"\n",
675        ));
676        sources.push(src(
677            Layer::HarnessFile,
678            "one-off.toml",
679            "label = \"claude-code\"\n\n[model]\nflag = \"--from-file\"\n",
680        ));
681        let built = build_registry(sources).unwrap();
682        assert!(built.warnings.is_empty(), "{:?}", built.warnings);
683        let entry = built
684            .entries
685            .iter()
686            .find(|e| e.label == "claude-code")
687            .unwrap();
688        assert_eq!(entry.adapter.cli_model_flag(), Some("--from-file".into()));
689        assert_eq!(entry.sources.len(), 3);
690    }
691
692    #[test]
693    fn embedded_layer_provenance_distinguishes_built_ins_from_user_only_harnesses() {
694        let mut sources = embedded_sources();
695        sources.push(src(
696            Layer::ProjectLocal,
697            ".eval-magic/harnesses/cool.toml",
698            "label = \"cool\"\n",
699        ));
700        let built = build_registry(sources).unwrap();
701        let entry = |label: &str| built.entries.iter().find(|e| e.label == label).unwrap();
702        assert!(
703            entry("claude-code").has_embedded_layer(),
704            "built-ins carry their embedded source"
705        );
706        assert!(
707            !entry("cool").has_embedded_layer(),
708            "a user-only harness has no embedded layer"
709        );
710    }
711
712    #[test]
713    fn resolve_unknown_name_lists_known_harnesses() {
714        let err = Harness::resolve("nonexistent").unwrap_err().to_string();
715        assert!(err.contains("unknown harness 'nonexistent'"), "{err}");
716        for name in ["claude-code", "codex", "opencode"] {
717            assert!(err.contains(name), "error must name {name}: {err}");
718        }
719    }
720
721    #[test]
722    fn resolve_round_trips_every_registry_entry() {
723        for harness in Harness::known() {
724            assert_eq!(Harness::resolve(harness.name()).unwrap(), harness);
725        }
726    }
727
728    #[test]
729    fn default_harness_is_claude_code() {
730        assert_eq!(DEFAULT_HARNESS_NAME, "claude-code");
731        assert_eq!(Harness::default().name(), DEFAULT_HARNESS_NAME);
732    }
733
734    #[test]
735    fn known_iterates_in_descriptor_order() {
736        let names: Vec<_> = Harness::known().map(Harness::name).collect();
737        assert_eq!(names, ["claude-code", "codex", "opencode"]);
738    }
739
740    #[test]
741    fn labels_match_kebab_case_identifiers() {
742        for name in ["claude-code", "codex", "opencode"] {
743            let harness = Harness::resolve(name).unwrap();
744            assert_eq!(adapter_for(harness).label(), name);
745        }
746    }
747}