"""Unit: agent `author` identity -> bottle.git_user (PRD 0082). Identity moved off `git-gate.user` (bottle) onto the trusted agent's `author` block. At `load_for_agent` the agent's author populates the effective bottle's `git_user` (a `ManifestGitUser`). This locks the `author` validation/rejection paths and the bottle `git-gate.user` migration die.""" import unittest from bot_bottle.manifest import ManifestError, ManifestGitUser, ManifestIndex def _error_message(callable_, *args, **kwargs) -> str: # type: ignore """Run `callable_` expecting a ManifestError; return its message.""" try: callable_(*args, **kwargs) except ManifestError as e: return str(e) raise AssertionError("expected ManifestError was not raised") def _manifest(author): # type: ignore """Build an index with one agent 'demo' carrying the given `author` block, then load it, returning the composed Manifest.""" agent: dict = {"skills": [], "prompt": "", "bottle": "dev"} # type: ignore if author is not None: agent["author"] = author return ManifestIndex.from_json_obj({ "bottles": {"dev": {}}, "agents": {"demo": agent}, }).load_for_agent("demo") class TestAuthorIdentity(unittest.TestCase): """The agent's `author` block populates bottle.git_user.""" def test_parses_both_fields(self): m = _manifest({ "name": "Eric Bauerfeld", "email": "eric+claude@dideric.is", }) u = m.bottle.git_user self.assertEqual("Eric Bauerfeld", u.name) self.assertEqual("eric+claude@dideric.is", u.email) self.assertFalse(u.is_empty()) def test_omitted_author_defaults_to_empty(self): # No author block at all -> empty git_user, is_empty True -> # provisioner skips the `git config` step entirely. m = _manifest(None) self.assertTrue(m.bottle.git_user.is_empty()) def test_missing_name_dies(self): # `author` is present but name is absent -> both fields required. msg = _error_message(_manifest, {"email": "bot@example.com"}) self.assertIn("author.name must be a non-empty string", msg) def test_missing_email_dies(self): msg = _error_message(_manifest, {"name": "Bot"}) self.assertIn("author.email must be a non-empty string", msg) def test_both_empty_strings_dies(self): # An explicit `author: {name: "", email: ""}` is a typo / # half-finished edit; fail loudly rather than silently no-op. msg = _error_message(_manifest, {"name": "", "email": ""}) self.assertIn("author.name must be a non-empty string", msg) def test_unknown_key_dies(self): msg = _error_message( _manifest, {"name": "Bot", "email": "b@x", "username": "bot"}, ) self.assertIn("unknown key", msg) self.assertIn("username", msg) def test_non_string_name_dies(self): msg = _error_message(_manifest, {"name": 42, "email": "b@x"}) self.assertIn("author.name must be a non-empty string", msg) def test_non_string_email_dies(self): msg = _error_message(_manifest, {"name": "Bot", "email": ["x@y.z"]}) self.assertIn("author.email must be a non-empty string", msg) def test_email_with_whitespace_dies(self): msg = _error_message(_manifest, {"name": "Bot", "email": "a @b"}) self.assertIn("author.email must not contain whitespace", msg) class TestBottleGitGateUserMigration(unittest.TestCase): """`git-gate.user` on a bottle is no longer supported: it raises a ManifestError pointing at the agent's `author` block.""" def test_bottle_git_gate_user_dies(self): msg = _error_message( ManifestIndex.from_json_obj, { "bottles": {"dev": {"git-gate": {"user": {"name": "Bot"}}}}, "agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}}, }, ) self.assertIn("git-gate.user is no longer supported", msg) self.assertIn("author", msg) class TestGitUserDirect(unittest.TestCase): """Direct GitUser dataclass exercises (no manifest wrapper). The dataclass is still the runtime carrier on ManifestBottle.git_user.""" def test_is_empty_default(self): self.assertTrue(ManifestGitUser().is_empty()) def test_is_empty_false_when_name_set(self): self.assertFalse(ManifestGitUser(name="x").is_empty()) def test_is_empty_false_when_email_set(self): self.assertFalse(ManifestGitUser(email="x@y").is_empty()) if __name__ == "__main__": unittest.main()