1+ """OVOS-INTENT-1 §3.7: an inline ``<name>`` reference in a ``.intent`` file is
2+ an authoring convenience that must expand in place from the sibling ``.voc`` of
3+ that name before the template reaches an intent engine. The padatious/padacioso
4+ bus protocol carries only samples — never the ``.voc`` content — so the
5+ reference has to be resolved during registration, otherwise the raw ``<name>``
6+ token is trained into the engine and the intent never matches."""
17import unittest
2- from tempfile import TemporaryDirectory
38from pathlib import Path
9+ from tempfile import TemporaryDirectory
10+
11+ from ovos_utils .fakebus import FakeBus
12+ from ovos_workshop .intents import IntentServiceInterface
13+ from ovos_workshop .resource_files import ResourceFile , SkillResources
414
515
616class TestInlineVocabReferences (unittest .TestCase ):
7- """OVOS-INTENT-1 §3.7: an inline ``<name> `` reference in a ``.intent`` or
8- ``.blacklist `` file must expand in place from the sibling ``.voc`` of that
9- name found in the same locale directory."""
17+ """The load-time resolution path: ``load_intent_file `` / ``load_blacklist_file``
18+ expand every inline ``<name> `` from the sibling ``.voc`` of that name found in
19+ the same locale directory."""
1020
1121 def _skill_dir (self , files : dict ) -> str :
1222 tmp = TemporaryDirectory ()
@@ -18,7 +28,6 @@ def _skill_dir(self, files: dict) -> str:
1828 return tmp .name
1929
2030 def _resources (self , skill_dir ):
21- from ovos_workshop .resource_files import SkillResources
2231 return SkillResources (skill_dir , "en-us" , skill_id = "test.skill" )
2332
2433 def test_intent_inline_vocab (self ):
@@ -48,5 +57,114 @@ def test_intent_without_inline_ref_unchanged(self):
4857 self .assertIn ("turn on the light" , intents )
4958
5059
60+ class TestInlineVocabResources (unittest .TestCase ):
61+ def _resources (self , files ):
62+ tmp = TemporaryDirectory ()
63+ self .addCleanup (tmp .cleanup )
64+ locale = Path (tmp .name , "locale" , "en-us" )
65+ locale .mkdir (parents = True )
66+ for name , content in files .items ():
67+ (locale / name ).write_text (content )
68+ return SkillResources (tmp .name , "en-us" , skill_id = "test.skill" )
69+
70+ def test_vocabularies_map (self ):
71+ resources = self ._resources ({
72+ "thing.voc" : "widget\n gadget\n # comment\n " ,
73+ "foo.intent" : "what about <thing>\n " ,
74+ })
75+ self .assertEqual (resources .vocabularies ().get ("thing" ),
76+ ["widget" , "gadget" ])
77+
78+ def test_vocabularies_expands_alternates (self ):
79+ resources = self ._resources ({
80+ "greeting.voc" : "(hello|hi)\n good morning\n " ,
81+ })
82+ self .assertEqual (sorted (resources .vocabularies ()["greeting" ]),
83+ ["good morning" , "hello" , "hi" ])
84+
85+
86+ class TestInlineVocabRegistration (unittest .TestCase ):
87+ """The samples emitted on the registration bus topic must already have the
88+ inline ``<name>`` resolved to an ``(a|b|c)`` alternation group."""
89+
90+ def _capture_samples (self , files , pass_vocabs ):
91+ tmp = TemporaryDirectory ()
92+ self .addCleanup (tmp .cleanup )
93+ locale = Path (tmp .name , "locale" , "en-us" )
94+ locale .mkdir (parents = True )
95+ for name , content in files .items ():
96+ (locale / name ).write_text (content )
97+ resources = SkillResources (tmp .name , "en-us" , skill_id = "test.skill" )
98+ filename = str (ResourceFile (resources .types .intent , "foo.intent" ).file_path )
99+
100+ bus = FakeBus ()
101+ captured = {}
102+ bus .on ("padatious:register_intent" ,
103+ lambda m : captured .__setitem__ ("samples" , m .data ["samples" ]))
104+ iface = IntentServiceInterface (bus )
105+ iface .set_id ("test.skill" )
106+ kwargs = {"vocabs" : resources .vocabularies ()} if pass_vocabs else {}
107+ iface .register_padatious_intent ("test.skill:foo.intent" , filename ,
108+ "en-us" , ** kwargs )
109+ return captured ["samples" ]
110+
111+ def test_inline_reference_resolved_before_engine (self ):
112+ samples = self ._capture_samples ({
113+ "thing.voc" : "widget\n gadget\n " ,
114+ "foo.intent" : "what about <thing>\n " ,
115+ }, pass_vocabs = True )
116+ self .assertEqual (samples , ["what about (widget|gadget)" ])
117+
118+ def test_raw_reference_without_vocabs_is_unresolved (self ):
119+ samples = self ._capture_samples ({
120+ "thing.voc" : "widget\n gadget\n " ,
121+ "foo.intent" : "what about <thing>\n " ,
122+ }, pass_vocabs = False )
123+ self .assertEqual (samples , ["what about <thing>" ])
124+
125+
126+ class TestInlineVocabEngineEndToEnd (unittest .TestCase ):
127+ """End-to-end proof through the real engines: a skill shipping a ``foo.intent``
128+ with ``<thing>`` and a sibling ``thing.voc`` matches an utterance built from a
129+ vocabulary member, but only when the reference is resolved at registration."""
130+
131+ def _samples (self , pass_vocabs ):
132+ tmp = TemporaryDirectory ()
133+ self .addCleanup (tmp .cleanup )
134+ locale = Path (tmp .name , "locale" , "en-us" )
135+ locale .mkdir (parents = True )
136+ (locale / "thing.voc" ).write_text ("widget\n gadget\n " )
137+ (locale / "foo.intent" ).write_text ("what about <thing>\n " )
138+ resources = SkillResources (tmp .name , "en-us" , skill_id = "test.skill" )
139+ filename = str (ResourceFile (resources .types .intent , "foo.intent" ).file_path )
140+
141+ bus = FakeBus ()
142+ captured = {}
143+ bus .on ("padatious:register_intent" ,
144+ lambda m : captured .__setitem__ ("samples" , m .data ["samples" ]))
145+ iface = IntentServiceInterface (bus )
146+ iface .set_id ("test.skill" )
147+ kwargs = {"vocabs" : resources .vocabularies ()} if pass_vocabs else {}
148+ iface .register_padatious_intent ("test.skill:foo.intent" , filename ,
149+ "en-us" , ** kwargs )
150+ return captured ["samples" ]
151+
152+ def test_padacioso_inline_voc_intent_matches (self ):
153+ from padacioso import IntentContainer
154+ engine = IntentContainer ()
155+ engine .add_intent ("foo" , self ._samples (pass_vocabs = True ))
156+ self .assertEqual (engine .calc_intent ("what about widget" ).get ("name" ),
157+ "foo" )
158+
159+ def test_padacioso_raw_reference_is_rejected (self ):
160+ # an unresolved <thing> never reaches the engine as a matchable sample:
161+ # padacioso cannot build an intent from the dangling reference
162+ from padacioso import IntentContainer
163+ from ovos_spec_tools .expansion import MalformedTemplate
164+ engine = IntentContainer ()
165+ with self .assertRaises (MalformedTemplate ):
166+ engine .add_intent ("foo" , self ._samples (pass_vocabs = False ))
167+
168+
51169if __name__ == "__main__" :
52170 unittest .main ()
0 commit comments