| #!/usr/bin/env fuchsia-vendored-python |
| # Copyright 2026 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Unit tests for permissions definitions and expansion logic.""" |
| |
| from __future__ import annotations |
| |
| import pathlib |
| import re |
| import tempfile |
| import unittest |
| from unittest import mock |
| |
| from agents.lib import permissions |
| |
| |
| class PermissionsTest(unittest.TestCase): |
| """Tests for permissions module functions.""" |
| |
| def setUp(self) -> None: |
| self.temp_dir = tempfile.TemporaryDirectory() |
| self.addCleanup(self.temp_dir.cleanup) |
| self.mock_root = pathlib.Path(self.temp_dir.name) |
| |
| def test_expand_empty_or_comment(self) -> None: |
| self.assertEqual(permissions.expand_command_variants(""), []) |
| self.assertEqual(permissions.expand_command_variants(" "), []) |
| self.assertEqual( |
| permissions.expand_command_variants("# this is a comment"), [] |
| ) |
| # Inline comments should be stripped |
| grants = permissions.expand_command_variants( |
| "fx test # run unit tests" |
| ) |
| self.assertEqual(len(grants), 1) |
| self.assertTrue(grants[0].startswith("command(regex:")) |
| pattern = grants[0][14:-1] |
| self.assertTrue(re.fullmatch(pattern, "fx test")) |
| |
| def test_expand_comment_with_quoted_hash(self) -> None: |
| # Hash inside quotes must not be stripped as a comment |
| grants = permissions.expand_command_variants( |
| 'git commit -m "fix #1234" # inline comment' |
| ) |
| self.assertEqual(len(grants), 1) |
| pattern = grants[0][14:-1] |
| self.assertTrue(re.fullmatch(pattern, 'git commit -m "fix #1234"')) |
| |
| def test_expand_explicit_syntax(self) -> None: |
| self.assertEqual( |
| permissions.expand_command_variants("command(foo bar)"), |
| ["command(foo bar)"], |
| ) |
| self.assertEqual( |
| permissions.expand_command_variants("command(regex:foo.*)"), |
| ["command(regex:foo.*)"], |
| ) |
| |
| def test_expand_git_subcommand_regex(self) -> None: |
| grants = permissions.expand_command_variants("git status") |
| self.assertEqual(len(grants), 1) |
| self.assertTrue(grants[0].startswith("command(regex:")) |
| pattern = grants[0][14:-1] |
| |
| # Test pattern matching |
| self.assertTrue(re.fullmatch(pattern, "git status")) |
| self.assertTrue(re.fullmatch(pattern, "/usr/bin/git status")) |
| self.assertTrue(re.fullmatch(pattern, "git -C /some/dir status")) |
| self.assertTrue( |
| re.fullmatch(pattern, 'git -C "/dir with spaces" status') |
| ) |
| self.assertTrue(re.fullmatch(pattern, "git --no-pager status")) |
| self.assertTrue( |
| re.fullmatch(pattern, 'git -c user.name="John Doe" status') |
| ) |
| self.assertTrue(re.fullmatch(pattern, "FOO=bar git status")) |
| self.assertTrue(re.fullmatch(pattern, "git status --short")) |
| |
| # Boundary checks |
| self.assertFalse(re.fullmatch(pattern, "git-lfs status")) |
| |
| def test_expand_git_force_push_matching(self) -> None: |
| grants = permissions.expand_command_variants("git push --force") |
| self.assertEqual(len(grants), 1) |
| self.assertTrue(grants[0].startswith("command(regex:")) |
| pattern = grants[0][14:-1] |
| |
| self.assertTrue(re.fullmatch(pattern, "git push --force")) |
| self.assertTrue(re.fullmatch(pattern, "git push origin main --force")) |
| self.assertTrue( |
| re.fullmatch(pattern, 'git push "my branch with spaces" --force') |
| ) |
| self.assertTrue( |
| re.fullmatch(pattern, "git -C /dir push origin HEAD --force") |
| ) |
| self.assertFalse(re.fullmatch(pattern, "git push origin main")) |
| |
| def test_expand_git_quoted_arguments(self) -> None: |
| grants = permissions.expand_command_variants( |
| 'git commit -m "fix -f bug"' |
| ) |
| self.assertTrue(len(grants) >= 1) |
| patterns = [g[14:-1] for g in grants if g.startswith("command(regex:")] |
| self.assertTrue( |
| any(re.fullmatch(p, 'git commit -m "fix -f bug"') for p in patterns) |
| ) |
| |
| def test_expand_quoted_binary_path(self) -> None: |
| grants = permissions.expand_command_variants( |
| '"/custom/path to/git" status' |
| ) |
| self.assertTrue(len(grants) >= 1) |
| self.assertTrue(any("git" in g and "status" in g for g in grants)) |
| |
| def test_expand_fx_variants(self) -> None: |
| grants = permissions.expand_command_variants("fx test") |
| self.assertEqual(len(grants), 1) |
| self.assertTrue(grants[0].startswith("command(regex:")) |
| pattern = grants[0][14:-1] |
| |
| # Positive matching |
| self.assertTrue(re.fullmatch(pattern, "fx test")) |
| self.assertTrue(re.fullmatch(pattern, "fx -t target-name test")) |
| self.assertTrue( |
| re.fullmatch(pattern, "fx -t target-name test //foo:bar") |
| ) |
| self.assertTrue( |
| re.fullmatch(pattern, "fx -t [fe80::1%eth0]:8022 test //foo:bar") |
| ) |
| self.assertTrue(re.fullmatch(pattern, 'fx -t="my-device" test')) |
| self.assertTrue(re.fullmatch(pattern, "fx --dir out/default test")) |
| self.assertTrue( |
| re.fullmatch( |
| pattern, |
| "fx --dir=out/default -t my-dev -i -x test //pkg:test", |
| ) |
| ) |
| self.assertTrue(re.fullmatch(pattern, "scripts/fx -t my-device test")) |
| self.assertTrue( |
| re.fullmatch(pattern, "./scripts/fx --enable=incremental test") |
| ) |
| self.assertTrue(re.fullmatch(pattern, "tools/fx test")) |
| self.assertTrue(re.fullmatch(pattern, ".jiri_root/bin/fx test")) |
| self.assertTrue(re.fullmatch(pattern, "ENV_VAR=1 fx -t my-dev test")) |
| |
| # Negative matching |
| self.assertFalse(re.fullmatch(pattern, "fx build")) |
| self.assertFalse(re.fullmatch(pattern, "fx testing")) |
| self.assertFalse(re.fullmatch(pattern, "fxx test")) |
| |
| def test_expand_fx_empty_args(self) -> None: |
| grants = permissions.expand_command_variants("fx") |
| self.assertEqual(len(grants), 1) |
| self.assertTrue(grants[0].startswith("command(regex:")) |
| pattern = grants[0][14:-1] |
| self.assertTrue(re.fullmatch(pattern, "fx")) |
| self.assertTrue(re.fullmatch(pattern, "fx -x")) |
| self.assertTrue(re.fullmatch(pattern, "scripts/fx -t my-device build")) |
| |
| def test_expand_ffx_variants(self) -> None: |
| grants = permissions.expand_command_variants("ffx target list") |
| self.assertEqual(len(grants), 1) |
| self.assertTrue(grants[0].startswith("command(regex:")) |
| pattern = grants[0][14:-1] |
| |
| # Positive matching |
| self.assertTrue(re.fullmatch(pattern, "ffx target list")) |
| self.assertTrue(re.fullmatch(pattern, "ffx -t my-device target list")) |
| self.assertTrue(re.fullmatch(pattern, "ffx --machine json target list")) |
| self.assertTrue( |
| re.fullmatch( |
| pattern, |
| "ffx -c log.level=Debug -t my-device --machine json target list", |
| ) |
| ) |
| self.assertTrue(re.fullmatch(pattern, "ffx -v --strict target list")) |
| self.assertTrue(re.fullmatch(pattern, "fx ffx target list")) |
| self.assertTrue( |
| re.fullmatch(pattern, "fx -t my-dev ffx --machine json target list") |
| ) |
| self.assertTrue( |
| re.fullmatch( |
| pattern, "scripts/fx -t my-dev ffx -t my-dev2 target list" |
| ) |
| ) |
| self.assertTrue( |
| re.fullmatch(pattern, "ENV_VAR=1 ffx -t dev target list") |
| ) |
| |
| # Negative matching |
| self.assertFalse(re.fullmatch(pattern, "ffx target show")) |
| self.assertFalse(re.fullmatch(pattern, "ffx emu start")) |
| |
| def test_expand_ffx_empty_args(self) -> None: |
| grants = permissions.expand_command_variants("ffx") |
| self.assertEqual(len(grants), 1) |
| self.assertTrue(grants[0].startswith("command(regex:")) |
| pattern = grants[0][14:-1] |
| self.assertTrue(re.fullmatch(pattern, "ffx")) |
| self.assertTrue(re.fullmatch(pattern, "ffx -v")) |
| self.assertTrue(re.fullmatch(pattern, "fx ffx target list")) |
| self.assertTrue( |
| re.fullmatch(pattern, "scripts/fx -t my-dev ffx target list") |
| ) |
| |
| def test_expand_jiri_variants(self) -> None: |
| grants = permissions.expand_command_variants("jiri status") |
| self.assertEqual(len(grants), 1) |
| self.assertTrue(grants[0].startswith("command(regex:")) |
| pattern = grants[0][14:-1] |
| |
| # Positive matching |
| self.assertTrue(re.fullmatch(pattern, "jiri status")) |
| self.assertTrue(re.fullmatch(pattern, "jiri -v status")) |
| self.assertTrue(re.fullmatch(pattern, "jiri -vv status")) |
| self.assertTrue(re.fullmatch(pattern, "jiri -j 50 status")) |
| self.assertTrue( |
| re.fullmatch( |
| pattern, "jiri -root /path/to/root -color never status" |
| ) |
| ) |
| self.assertTrue( |
| re.fullmatch( |
| pattern, ".jiri_root/bin/jiri -show-progress=false status" |
| ) |
| ) |
| self.assertTrue(re.fullmatch(pattern, "ENV_VAR=1 jiri -q status")) |
| self.assertTrue(re.fullmatch(pattern, "jiri -time status")) |
| self.assertTrue(re.fullmatch(pattern, "jiri --time status")) |
| self.assertTrue(re.fullmatch(pattern, "jiri --quiet status")) |
| self.assertTrue(re.fullmatch(pattern, "jiri --show-progress status")) |
| self.assertTrue( |
| re.fullmatch(pattern, "jiri -time-log-threshold 5s status") |
| ) |
| self.assertTrue( |
| re.fullmatch(pattern, "jiri -timefile /tmp/timing.txt status") |
| ) |
| self.assertTrue( |
| re.fullmatch(pattern, "jiri -progress-window 10 status") |
| ) |
| |
| # Negative matching |
| self.assertFalse(re.fullmatch(pattern, "jiri update")) |
| self.assertFalse(re.fullmatch(pattern, "jiri snapshot")) |
| |
| def test_expand_jiri_empty_args(self) -> None: |
| grants = permissions.expand_command_variants("jiri") |
| self.assertEqual(len(grants), 1) |
| self.assertTrue(grants[0].startswith("command(regex:")) |
| pattern = grants[0][14:-1] |
| self.assertTrue(re.fullmatch(pattern, "jiri")) |
| self.assertTrue(re.fullmatch(pattern, "jiri -v")) |
| self.assertTrue(re.fullmatch(pattern, "jiri -q status")) |
| self.assertTrue( |
| re.fullmatch(pattern, ".jiri_root/bin/jiri -j 10 update") |
| ) |
| |
| def test_tool_spec_custom_tool(self) -> None: |
| spec = permissions.ToolSpec( |
| binary_prefix=r"(\S+/)?custom", |
| global_flags_pattern=r"(\s+--flag)*", |
| allow_flags_anywhere=True, |
| ) |
| grants = spec.expand("build --opt") |
| self.assertEqual(len(grants), 1) |
| pattern = grants[0][14:-1] |
| self.assertTrue(re.fullmatch(pattern, "custom --flag build --opt")) |
| self.assertTrue( |
| re.fullmatch(pattern, "/path/custom build file.txt --opt") |
| ) |
| |
| def test_expand_python_interpreter_variants(self) -> None: |
| py_grants = permissions.expand_command_variants("python3 script.py") |
| self.assertIn("command(python script.py)", py_grants) |
| self.assertIn("command(python3 script.py)", py_grants) |
| self.assertIn("command(fuchsia-vendored-python script.py)", py_grants) |
| self.assertIn( |
| "command(scripts/fuchsia-vendored-python script.py)", py_grants |
| ) |
| |
| def test_expand_sed_inplace_regex(self) -> None: |
| sed_grants = permissions.expand_command_variants("sed -i 's/foo/bar/g'") |
| self.assertIn("command(sed -i 's/foo/bar/g')", sed_grants) |
| regex_grants = [g for g in sed_grants if g.startswith("command(regex:")] |
| self.assertTrue(len(regex_grants) > 0) |
| pattern = regex_grants[0][14:-1] |
| |
| self.assertTrue(re.fullmatch(pattern, "sed -i 's/foo/bar/g' file.txt")) |
| self.assertTrue(re.fullmatch(pattern, "/usr/bin/sed -i.bak 's/a/b/' f")) |
| self.assertTrue(re.fullmatch(pattern, "sed -E -i '' 's/a/b/' f")) |
| self.assertTrue( |
| re.fullmatch(pattern, "sed --in-place=suffix 's/a/b/' file.txt") |
| ) |
| |
| # Read-only stream commands containing '-i' in expressions must not trigger in-place regex |
| readonly_grants = permissions.expand_command_variants( |
| "sed 's/foo -i bar/baz/' file.txt" |
| ) |
| readonly_regexes = [ |
| g for g in readonly_grants if g.startswith("command(regex:") |
| ] |
| self.assertEqual(len(readonly_regexes), 0) |
| |
| def test_expand_system_binary_resolution(self) -> None: |
| with mock.patch("shutil.which", return_value="/usr/bin/ls"): |
| with mock.patch("pathlib.Path.exists", return_value=True): |
| grants = permissions.expand_command_variants("ls -la") |
| self.assertIn("command(ls -la)", grants) |
| self.assertIn("command(/usr/bin/ls -la)", grants) |
| self.assertIn("command(/bin/ls -la)", grants) |
| |
| def test_read_command_list_file(self) -> None: |
| list_file = self.mock_root / "commands.txt" |
| list_file.write_text( |
| "# A comment\n" |
| "git status\n" |
| "\n" |
| "command(explicit_rule)\n" |
| "fx build\n", |
| encoding="utf-8", |
| ) |
| grants = permissions.read_command_list_file(list_file) |
| self.assertTrue( |
| any( |
| g.startswith("command(regex:") and "git" in g and "status" in g |
| for g in grants |
| ) |
| ) |
| self.assertIn("command(explicit_rule)", grants) |
| self.assertTrue( |
| any( |
| g.startswith("command(regex:") and "fx" in g and "build" in g |
| for g in grants |
| ) |
| ) |
| |
| def test_read_command_list_file_nonexistent(self) -> None: |
| self.assertEqual( |
| permissions.read_command_list_file( |
| self.mock_root / "nonexistent.txt" |
| ), |
| [], |
| ) |
| |
| def test_find_config_dirs(self) -> None: |
| fuchsia_dir = self.mock_root |
| public_cfg = fuchsia_dir / ".agents" / "config" |
| public_cfg.mkdir(parents=True, exist_ok=True) |
| vendor_cfg = fuchsia_dir / "vendor" / "google" / ".agents" / "config" |
| vendor_cfg.mkdir(parents=True, exist_ok=True) |
| |
| config_dirs = permissions.find_config_dirs(fuchsia_dir) |
| self.assertIn(public_cfg, config_dirs) |
| self.assertIn(vendor_cfg, config_dirs) |
| |
| def test_load_profile_grants(self) -> None: |
| fuchsia_dir = self.mock_root |
| perm_dir = fuchsia_dir / ".agents" / "config" / "permissions" |
| perm_dir.mkdir(parents=True, exist_ok=True) |
| (perm_dir / "read_only.txt").write_text( |
| "git status\nfx status\n", encoding="utf-8" |
| ) |
| (perm_dir / "local_changes.txt").write_text( |
| "git commit\n", encoding="utf-8" |
| ) |
| (perm_dir / "never_allow.txt").write_text( |
| "git reset --hard\n", encoding="utf-8" |
| ) |
| (perm_dir / "device_ops.txt").write_text("fx ota\n", encoding="utf-8") |
| (perm_dir / "cache_destruction.txt").write_text( |
| "fx clean\n", encoding="utf-8" |
| ) |
| (perm_dir / "batch_execution.txt").write_text( |
| "find\n", encoding="utf-8" |
| ) |
| |
| grants = permissions.load_profile_grants(fuchsia_dir, "read-only") |
| self.assertTrue(any("git" in g and "status" in g for g in grants.allow)) |
| self.assertTrue(any("git" in g and "commit" in g for g in grants.deny)) |
| self.assertTrue(any("fx" in g and "ota" in g for g in grants.ask)) |
| self.assertTrue(any("find" in g for g in grants.ask)) |
| |
| grants_local = permissions.load_profile_grants( |
| fuchsia_dir, "local-changes" |
| ) |
| self.assertTrue( |
| any("git" in g and "commit" in g for g in grants_local.allow) |
| ) |
| self.assertTrue(any("find" in g for g in grants_local.ask)) |
| |
| grants_ext = permissions.load_profile_grants( |
| fuchsia_dir, "external-changes" |
| ) |
| self.assertTrue(any("find" in g for g in grants_ext.ask)) |
| |
| def test_find_fuchsia_dir_from_tree(self) -> None: |
| fake_root = self.mock_root / "workspace" |
| (fake_root / ".jiri_root").mkdir(parents=True) |
| sub_file = fake_root / "tools" / "agents" / "lib" / "permissions.py" |
| sub_file.parent.mkdir(parents=True) |
| sub_file.write_text("# placeholder", encoding="utf-8") |
| |
| with mock.patch("agents.lib.permissions.__file__", str(sub_file)): |
| with mock.patch.dict( |
| "os.environ", {"FUCHSIA_DIR": "/different/root"} |
| ): |
| found = permissions.find_fuchsia_dir() |
| self.assertEqual(found, fake_root) |
| |
| def test_find_fuchsia_dir_fallback_env(self) -> None: |
| outside_file = self.mock_root / "outside" / "script.py" |
| outside_file.parent.mkdir(parents=True) |
| outside_file.write_text("# placeholder", encoding="utf-8") |
| target_root = self.mock_root / "env_root" |
| target_root.mkdir(parents=True) |
| |
| with mock.patch("agents.lib.permissions.__file__", str(outside_file)): |
| with mock.patch.dict( |
| "os.environ", {"FUCHSIA_DIR": str(target_root)} |
| ): |
| found = permissions.find_fuchsia_dir() |
| self.assertEqual(found, target_root) |
| |
| def test_find_fuchsia_dir_not_found_raises(self) -> None: |
| outside_file = self.mock_root / "outside" / "script.py" |
| outside_file.parent.mkdir(parents=True) |
| outside_file.write_text("# placeholder", encoding="utf-8") |
| |
| with mock.patch("agents.lib.permissions.__file__", str(outside_file)): |
| with mock.patch.dict("os.environ", {}, clear=True): |
| with self.assertRaises(RuntimeError) as ctx: |
| permissions.find_fuchsia_dir() |
| self.assertIn( |
| "Could not locate Fuchsia root directory", |
| str(ctx.exception), |
| ) |
| |
| |
| if __name__ == "__main__": |
| unittest.main() |