@@ -1259,6 +1259,8 @@ def test_pi_skill_host_uses_coding_agent_dir(self) -> None:
12591259 avatar = pi_home / "skills" / "dyro-control-plane"
12601260 self .assertTrue (avatar .is_symlink () or avatar .is_dir ())
12611261 self .assertTrue ((avatar / "SKILL.md" ).is_file ())
1262+ pi_status = integration_status ("skill" , host_homes = {"pi" : pi_home })
1263+ self .assertIn ("pi" , {row .host for row in pi_status .avatars })
12621264
12631265 def test_dsh_skill_host_uses_dsh_home (self ) -> None :
12641266 spec = next (host for host in manager .HOSTS if host .host_id == "dsh" )
@@ -1271,6 +1273,138 @@ def test_dsh_skill_host_uses_dsh_home(self) -> None:
12711273 self .assertTrue (avatar .is_symlink () or avatar .is_dir ())
12721274 self .assertTrue ((avatar / "SKILL.md" ).is_file ())
12731275
1276+ def test_opencode_and_hermes_host_specs_use_existing_homes (self ) -> None :
1277+ opencode = next (host for host in manager .HOSTS if host .host_id == "opencode" )
1278+ hermes = next (host for host in manager .HOSTS if host .host_id == "hermes" )
1279+ self .assertEqual (opencode .env_var , "OPENCODE_CONFIG_DIR" )
1280+ self .assertEqual (opencode .default_dirname , ".config/opencode" )
1281+ self .assertEqual (hermes .env_var , "HERMES_HOME" )
1282+ self .assertEqual (hermes .default_dirname , ".hermes" )
1283+
1284+ opencode_home = self .root / "opencode-home"
1285+ hermes_home = self .root / "hermes-home"
1286+ homes = {"opencode" : opencode_home , "hermes" : hermes_home }
1287+ preview = integration_status ("skill" , host_homes = homes )
1288+ self .assertEqual (
1289+ {row .host : row .state for row in preview .avatars if row .host in homes },
1290+ {"opencode" : "missing" , "hermes" : "missing" },
1291+ )
1292+ self .assertEqual (
1293+ manager ._avatar_path (opencode_home , manager .CONTROL_PLANE_SPEC ),
1294+ opencode_home / "skills" / "dyro-control-plane" ,
1295+ )
1296+ self .assertEqual (
1297+ manager ._avatar_path (hermes_home , manager .CONTROL_PLANE_SPEC ),
1298+ hermes_home / "skills" / "dyro-control-plane" ,
1299+ )
1300+
1301+ installed = install_integration ("skill" , yes = True , host_homes = homes )
1302+ self .assertEqual (installed .status .state , IntegrationState .CURRENT )
1303+ opencode_avatar = opencode_home / "skills" / "dyro-control-plane"
1304+ hermes_avatar = hermes_home / "skills" / "dyro-control-plane"
1305+ self .assertTrue (opencode_avatar .is_symlink ())
1306+ self .assertTrue (hermes_avatar .is_symlink ())
1307+ self .assertEqual (opencode_avatar .resolve (), self .mirror .resolve ())
1308+ self .assertEqual (hermes_avatar .resolve (), self .mirror .resolve ())
1309+ self .assertTrue ((opencode_avatar / "SKILL.md" ).is_file ())
1310+ self .assertTrue ((hermes_avatar / "SKILL.md" ).is_file ())
1311+ self .assertEqual (
1312+ {row .host : row .state for row in installed .status .avatars if row .host in homes },
1313+ {"opencode" : "current" , "hermes" : "current" },
1314+ )
1315+
1316+ synced = sync_managed_skill (yes = True , host_homes = homes )
1317+ self .assertIsNone (synced )
1318+ self .assertEqual (
1319+ integration_status ("skill" , host_homes = homes ).state ,
1320+ IntegrationState .CURRENT ,
1321+ )
1322+
1323+ removed = uninstall_integration ("skill" , yes = True , host_homes = homes )
1324+ self .assertEqual (removed .status .state , IntegrationState .ABSENT )
1325+ self .assertFalse (opencode_avatar .exists () or opencode_avatar .is_symlink ())
1326+ self .assertFalse (hermes_avatar .exists () or hermes_avatar .is_symlink ())
1327+
1328+ def test_absent_opencode_and_hermes_homes_stay_silent (self ) -> None :
1329+ config_root = self .fake_home / ".config"
1330+ default_opencode = config_root / "opencode"
1331+ default_hermes = self .fake_home / ".hermes"
1332+
1333+ config_root .mkdir ()
1334+ status = integration_status ("skill" )
1335+ hosts = {row .host for row in status .avatars }
1336+ self .assertIn ("codex" , hosts )
1337+ self .assertNotIn ("opencode" , hosts )
1338+ self .assertNotIn ("hermes" , hosts )
1339+ self .assertIsNone (
1340+ manager ._host_home (
1341+ next (host for host in manager .HOSTS if host .host_id == "opencode" ),
1342+ None ,
1343+ )
1344+ )
1345+ self .assertIsNone (
1346+ manager ._host_home (
1347+ next (host for host in manager .HOSTS if host .host_id == "hermes" ),
1348+ None ,
1349+ )
1350+ )
1351+
1352+ install_integration ("skill" , yes = True )
1353+ self .assertFalse (default_opencode .exists ())
1354+ self .assertFalse (default_hermes .exists ())
1355+ self .assertEqual (
1356+ {row .host for row in integration_status ("skill" ).avatars },
1357+ {"codex" },
1358+ )
1359+
1360+ def test_present_opencode_and_hermes_default_homes_receive_avatars (self ) -> None :
1361+ opencode_home = self .fake_home / ".config" / "opencode"
1362+ hermes_home = self .fake_home / ".hermes"
1363+ opencode_home .mkdir (parents = True )
1364+ hermes_home .mkdir ()
1365+
1366+ status = integration_status ("skill" )
1367+ hosts = {row .host : row for row in status .avatars }
1368+ self .assertIn ("opencode" , hosts )
1369+ self .assertIn ("hermes" , hosts )
1370+ self .assertIn ("codex" , hosts )
1371+ self .assertEqual (hosts ["opencode" ].state , "missing" )
1372+ self .assertEqual (hosts ["hermes" ].state , "missing" )
1373+ self .assertEqual (
1374+ hosts ["opencode" ].path ,
1375+ opencode_home / "skills" / "dyro-control-plane" ,
1376+ )
1377+ self .assertEqual (
1378+ hosts ["hermes" ].path ,
1379+ hermes_home / "skills" / "dyro-control-plane" ,
1380+ )
1381+
1382+ installed = install_integration ("skill" , yes = True )
1383+ self .assertEqual (installed .status .state , IntegrationState .CURRENT )
1384+ opencode_avatar = opencode_home / "skills" / "dyro-control-plane"
1385+ hermes_avatar = hermes_home / "skills" / "dyro-control-plane"
1386+ self .assertTrue (opencode_avatar .is_symlink ())
1387+ self .assertTrue (hermes_avatar .is_symlink ())
1388+ self .assertEqual (
1389+ {row .host : row .state for row in installed .status .avatars },
1390+ {"codex" : "current" , "opencode" : "current" , "hermes" : "current" },
1391+ )
1392+
1393+ output = StringIO ()
1394+ with redirect_stdout (output ):
1395+ main (["integration" , "status" , "skill" ])
1396+ main (["integration" , "sync" , "skill" , "--yes" ])
1397+ text = output .getvalue ()
1398+ self .assertIn ("avatar\t opencode\t current" , text )
1399+ self .assertIn ("avatar\t hermes\t current" , text )
1400+ self .assertIn ("无需同步" , text )
1401+
1402+ uninstall_integration ("skill" , yes = True )
1403+ self .assertTrue (opencode_home .is_dir ())
1404+ self .assertTrue (hermes_home .is_dir ())
1405+ self .assertFalse (opencode_avatar .exists () or opencode_avatar .is_symlink ())
1406+ self .assertFalse (hermes_avatar .exists () or hermes_avatar .is_symlink ())
1407+
12741408
12751409if __name__ == "__main__" :
12761410 unittest .main ()
0 commit comments