Skip to content

Commit fdef7b0

Browse files
committed
test(manifests): cover catalog discovery, plugins, schema, and accessors
Mirror the manifests package layout under tests/manifests/ (test_manifest, test_catalog, test_schema, test_init) and refocus the rails/llm shim tests on re-export identity and the discovery helpers. Adds coverage for RailCatalog discovery and plugin entry-point loading, the config schema primitives, and the package-level accessors, which were previously untested. Signed-off-by: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com>
1 parent 4d4152a commit fdef7b0

4 files changed

Lines changed: 342 additions & 48 deletions

File tree

tests/manifests/test_catalog.py

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
import types
17+
18+
import pytest
19+
20+
import nemoguardrails.manifests.catalog as catalog_module
21+
from nemoguardrails.manifests import (
22+
ActionRef,
23+
Binding,
24+
ConfigSpecRef,
25+
RailActions,
26+
RailCatalog,
27+
RailConfigSchema,
28+
RailDirection,
29+
RailManifest,
30+
RailManifestRecord,
31+
RailSpec,
32+
RailSurface,
33+
)
34+
35+
36+
def _action(name: str = "check") -> ActionRef:
37+
return ActionRef(name=name, target="pathlib:Path.cwd")
38+
39+
40+
def _record(name: str, *, action: ActionRef | None = None, surface_name: str | None = None) -> RailManifestRecord:
41+
action = action or _action(f"{name}_check")
42+
surfaces = ()
43+
if surface_name is not None:
44+
surfaces = (
45+
RailSurface(
46+
name=surface_name,
47+
direction=RailDirection.INPUT,
48+
action=action,
49+
bindings=(Binding.context("text", "user_message"),),
50+
),
51+
)
52+
manifest = RailManifest(
53+
name=name,
54+
spec=RailSpec(actions=RailActions(refs=(action,)), surfaces=surfaces),
55+
)
56+
return RailManifestRecord(manifest=manifest, source=f"test:{name}")
57+
58+
59+
def test_catalog_indexes_manifests_and_surfaces():
60+
catalog = RailCatalog((_record("alpha", surface_name="check alpha"), _record("beta")))
61+
62+
assert set(catalog.manifests) == {"alpha", "beta"}
63+
assert set(catalog.surfaces()) == {(RailDirection.INPUT, "check alpha")}
64+
65+
66+
def test_catalog_rejects_duplicate_manifest_names():
67+
with pytest.raises(ValueError, match="already provided"):
68+
RailCatalog((_record("duplicate"), _record("duplicate")))
69+
70+
71+
def test_catalog_rejects_duplicate_action_names():
72+
action = _action("shared")
73+
74+
with pytest.raises(ValueError, match="already provided"):
75+
RailCatalog((_record("alpha", action=action), _record("beta", action=action)))
76+
77+
78+
def test_catalog_rejects_duplicate_surface_keys():
79+
with pytest.raises(ValueError, match="already provided"):
80+
RailCatalog((_record("alpha", surface_name="shared"), _record("beta", surface_name="shared")))
81+
82+
83+
def test_catalog_rejects_duplicate_config_key():
84+
shared_spec = ConfigSpecRef(target="pathlib:Path.cwd")
85+
86+
def record(name):
87+
manifest = RailManifest(
88+
name=name, spec=RailSpec(config_schema=RailConfigSchema(key="shared", spec=shared_spec))
89+
)
90+
return RailManifestRecord(manifest=manifest, source=f"test:{name}")
91+
92+
with pytest.raises(ValueError, match="config key"):
93+
RailCatalog((record("alpha"), record("beta")))
94+
95+
96+
def test_catalog_rejects_surface_with_undeclared_action():
97+
declared = _action("declared")
98+
undeclared = _action("undeclared")
99+
manifest = RailManifest(
100+
name="invalid",
101+
spec=RailSpec(
102+
actions=RailActions(refs=(declared,)),
103+
surfaces=(RailSurface(name="invalid", direction="input", action=undeclared),),
104+
),
105+
)
106+
107+
with pytest.raises(ValueError, match="not declared"):
108+
RailCatalog((RailManifestRecord(manifest=manifest, source="test:invalid"),))
109+
110+
111+
def test_discover_built_ins_loads_rail_modules(tmp_path, monkeypatch):
112+
rail_package = tmp_path / "content_safety"
113+
rail_package.mkdir()
114+
(rail_package / "rail.py").write_text("RAIL = None\n")
115+
116+
discovered_manifest = RailManifest(name="content_safety")
117+
fake_module = types.SimpleNamespace(RAIL=discovered_manifest)
118+
119+
def fake_import(module_name):
120+
assert module_name == "nemoguardrails.library.content_safety.rail"
121+
return fake_module
122+
123+
monkeypatch.setattr(catalog_module.importlib, "import_module", fake_import)
124+
125+
catalog = RailCatalog.discover_built_ins(library_path=tmp_path)
126+
127+
assert set(catalog.manifests) == {"content_safety"}
128+
assert catalog.manifests["content_safety"].origin == "nemoguardrails.library.content_safety.rail"
129+
130+
131+
def test_discover_built_ins_rejects_non_manifest_rail(tmp_path, monkeypatch):
132+
rail_package = tmp_path / "broken"
133+
rail_package.mkdir()
134+
(rail_package / "rail.py").write_text("RAIL = 1\n")
135+
136+
monkeypatch.setattr(
137+
catalog_module.importlib,
138+
"import_module",
139+
lambda module_name: types.SimpleNamespace(RAIL="not-a-manifest"),
140+
)
141+
142+
with pytest.raises(TypeError, match="must define RAIL"):
143+
RailCatalog.discover_built_ins(library_path=tmp_path)
144+
145+
146+
class _FakeDistribution:
147+
def __init__(self, name):
148+
self.name = name
149+
150+
151+
class _FakeEntryPoint:
152+
def __init__(self, name, value, manifest, distribution="plugin-dist"):
153+
self.name = name
154+
self.value = value
155+
self.dist = _FakeDistribution(distribution)
156+
self._manifest = manifest
157+
158+
def load(self):
159+
return self._manifest
160+
161+
162+
def _patch_entry_points(monkeypatch, entry_points):
163+
class _FakeEntryPoints:
164+
def select(self, group):
165+
assert group == "nemoguardrails.rails"
166+
return list(entry_points)
167+
168+
monkeypatch.setattr(catalog_module.importlib.metadata, "entry_points", lambda: _FakeEntryPoints())
169+
170+
171+
def test_with_plugins_empty_returns_same_catalog():
172+
catalog = RailCatalog()
173+
assert catalog.with_plugins([]) is catalog
174+
175+
176+
def test_with_plugins_loads_entry_point(monkeypatch):
177+
plugin_manifest = RailManifest(name="my_plugin")
178+
_patch_entry_points(monkeypatch, [_FakeEntryPoint("my_plugin", "my_pkg:RAIL", plugin_manifest)])
179+
180+
extended = RailCatalog().with_plugins(["my_plugin"])
181+
182+
assert "my_plugin" in extended.manifests
183+
assert extended.records["my_plugin"].distribution == "plugin-dist"
184+
assert extended.records["my_plugin"].built_in is False
185+
186+
187+
def test_with_plugins_rejects_missing_plugin(monkeypatch):
188+
_patch_entry_points(monkeypatch, [])
189+
190+
with pytest.raises(ValueError, match="not installed"):
191+
RailCatalog().with_plugins(["absent"])
192+
193+
194+
def test_with_plugins_rejects_ambiguous_distribution(monkeypatch):
195+
manifest = RailManifest(name="dup")
196+
_patch_entry_points(
197+
monkeypatch,
198+
[
199+
_FakeEntryPoint("dup", "one:RAIL", manifest, distribution="pkg-one"),
200+
_FakeEntryPoint("dup", "two:RAIL", manifest, distribution="pkg-two"),
201+
],
202+
)
203+
204+
with pytest.raises(ValueError, match="multiple distributions"):
205+
RailCatalog().with_plugins(["dup"])
206+
207+
208+
def test_with_plugins_rejects_name_mismatch(monkeypatch):
209+
_patch_entry_points(monkeypatch, [_FakeEntryPoint("declared", "pkg:RAIL", RailManifest(name="actual"))])
210+
211+
with pytest.raises(ValueError, match="returned manifest"):
212+
RailCatalog().with_plugins(["declared"])

tests/manifests/test_init.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
import nemoguardrails.manifests as manifests_pkg
17+
from nemoguardrails.manifests import RailCatalog
18+
19+
20+
def test_package_accessors_with_no_built_in_rails():
21+
manifests_pkg._reset_rail_manifest_cache()
22+
23+
catalog = manifests_pkg.default_rail_catalog()
24+
assert isinstance(catalog, RailCatalog)
25+
# a second call returns the cached instance rather than rediscovering
26+
assert manifests_pkg.default_rail_catalog() is catalog
27+
28+
assert manifests_pkg.all_rail_manifests() == {}
29+
# no enabled plugins short-circuits back to the default catalog
30+
assert manifests_pkg.rail_catalog() is catalog
31+
assert manifests_pkg.rail_surfaces("input") == {}
32+
assert manifests_pkg.rail_surfaces() == {}
33+
assert manifests_pkg.surface_names("input") == ()
34+
assert manifests_pkg.configured_rail_surfaces("input", []) == {}
35+
36+
manifests_pkg._reset_rail_manifest_cache()
Lines changed: 58 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -18,46 +18,28 @@
1818

1919
from nemoguardrails.manifests import (
2020
ActionRef,
21-
Binding,
2221
ConfigSpecRef,
2322
RailActions,
24-
RailCatalog,
2523
RailConfigSchema,
2624
RailDirection,
2725
RailManifest,
28-
RailManifestRecord,
2926
RailMetadata,
3027
RailSpec,
3128
RailSurface,
3229
import_ref_target,
30+
iter_manifest_import_refs,
3331
iter_manifest_import_targets,
32+
normalize_configured_surface_name,
33+
parse_configured_surface,
3434
resolve_import_ref,
3535
)
36+
from nemoguardrails.manifests.manifest import configured_rail_surfaces
3637

3738

3839
def _action(name: str = "check") -> ActionRef:
3940
return ActionRef(name=name, target="pathlib:Path.cwd")
4041

4142

42-
def _record(name: str, *, action: ActionRef | None = None, surface_name: str | None = None) -> RailManifestRecord:
43-
action = action or _action(f"{name}_check")
44-
surfaces = ()
45-
if surface_name is not None:
46-
surfaces = (
47-
RailSurface(
48-
name=surface_name,
49-
direction=RailDirection.INPUT,
50-
action=action,
51-
bindings=(Binding.context("text", "user_message"),),
52-
),
53-
)
54-
manifest = RailManifest(
55-
name=name,
56-
spec=RailSpec(actions=RailActions(refs=(action,)), surfaces=surfaces),
57-
)
58-
return RailManifestRecord(manifest=manifest, source=f"test:{name}")
59-
60-
6143
def test_manifest_round_trips_with_typed_refs():
6244
action = _action()
6345
manifest = RailManifest(
@@ -105,40 +87,68 @@ def test_import_refs_resolve_nested_attributes():
10587
assert callable(resolve_import_ref(ref))
10688

10789

108-
def test_catalog_indexes_manifests_and_surfaces():
109-
catalog = RailCatalog((_record("alpha", surface_name="check alpha"), _record("beta")))
90+
def test_import_ref_target_rejects_non_ref():
91+
with pytest.raises(TypeError):
92+
import_ref_target("pathlib:Path.cwd")
11093

111-
assert set(catalog.manifests) == {"alpha", "beta"}
112-
assert set(catalog.surfaces()) == {(RailDirection.INPUT, "check alpha")}
11394

95+
def test_iter_manifest_import_refs_covers_config_actions_and_surfaces():
96+
action = _action("cwd")
97+
manifest = RailManifest(
98+
name="sample",
99+
spec=RailSpec(
100+
config_schema=RailConfigSchema(key="cfg", spec=ConfigSpecRef(target="pathlib:Path.cwd")),
101+
actions=RailActions(refs=(action,)),
102+
surfaces=(RailSurface(name="surface", direction=RailDirection.INPUT, action=action),),
103+
),
104+
)
114105

115-
def test_catalog_rejects_duplicate_manifest_names():
116-
with pytest.raises(ValueError, match="already provided"):
117-
RailCatalog((_record("duplicate"), _record("duplicate")))
106+
assert len(iter_manifest_import_refs(manifest)) == 3
118107

119108

120-
def test_catalog_rejects_duplicate_action_names():
121-
action = _action("shared")
109+
def test_parse_configured_surface_parenthesized_form():
110+
name, parameters = parse_configured_surface("content safety check($model=abc, $threshold=0.5)")
122111

123-
with pytest.raises(ValueError, match="already provided"):
124-
RailCatalog((_record("alpha", action=action), _record("beta", action=action)))
112+
assert name == "content safety check"
113+
assert parameters == {"model": "abc", "threshold": "0.5"}
125114

126115

127-
def test_catalog_rejects_duplicate_surface_keys():
128-
with pytest.raises(ValueError, match="already provided"):
129-
RailCatalog((_record("alpha", surface_name="shared"), _record("beta", surface_name="shared")))
116+
def test_parse_configured_surface_dollar_form():
117+
name, parameters = parse_configured_surface("self check input $model=gpt-4o")
130118

119+
assert name == "self check input"
120+
assert parameters == {"model": "gpt-4o"}
121+
assert normalize_configured_surface_name("self check input $model=gpt-4o") == "self check input"
131122

132-
def test_catalog_rejects_surface_with_undeclared_action():
133-
declared = _action("declared")
134-
undeclared = _action("undeclared")
135-
manifest = RailManifest(
136-
name="invalid",
137-
spec=RailSpec(
138-
actions=RailActions(refs=(declared,)),
139-
surfaces=(RailSurface(name="invalid", direction="input", action=undeclared),),
140-
),
141-
)
142123

143-
with pytest.raises(ValueError, match="not declared"):
144-
RailCatalog((RailManifestRecord(manifest=manifest, source="test:invalid"),))
124+
def test_parse_configured_surface_bare_name():
125+
assert parse_configured_surface("plain flow") == ("plain flow", {})
126+
127+
128+
def test_configured_rail_surfaces_selects_declared_surfaces():
129+
action = _action()
130+
surface = RailSurface(name="check", direction=RailDirection.INPUT, action=action)
131+
surfaces = {(RailDirection.INPUT, "check"): surface}
132+
133+
selected = configured_rail_surfaces(RailDirection.INPUT, ["check($model=abc)", "unknown"], surfaces)
134+
135+
assert selected == {"check": surface}
136+
137+
138+
def test_flat_manifest_is_normalized_into_spec():
139+
flat_manifest = {
140+
"name": "flat",
141+
"actions": {"refs": [{"name": "act", "target": "pathlib:Path.cwd"}]},
142+
"surfaces": [
143+
{
144+
"name": "surface",
145+
"direction": "input",
146+
"action": {"name": "act", "target": "pathlib:Path.cwd"},
147+
}
148+
],
149+
}
150+
151+
manifest = RailManifest.model_validate(flat_manifest)
152+
153+
assert manifest.actions.refs[0].name == "act"
154+
assert manifest.surfaces[0].name == "surface"

0 commit comments

Comments
 (0)