Files
Agent-System-Dev/tests/test_agent_exec_config.py
T
weichen bdad8ffc91 Add project execution agent configuration
Why: Establish one validated project-local routing document before any real Agent execution is enabled.

Evidence: Cursor observed red then 12 green configuration tests; the host replays those tests, the temporary validation target, and whitespace checks during integration.

Impact: Adds the U1 configuration contract and parser only; real routing, adapters, controller execution, and governance adoption remain disabled.

Refs: docs/superpowers/plans/2026-10-15-project-agent-execution.md
2026-10-15 02:17:29 +00:00

340 lines
11 KiB
Python

"""Configuration parser and schema contract for AGENT-EXECUTION.md."""
from __future__ import annotations
import tempfile
import textwrap
import unittest
from pathlib import Path
from scripts.agent_exec_config import (
ConfigError,
ExecutionConfig,
load_config,
validate_config,
)
SCHEMA = "agent-execution/v1"
MINIMAL_BLOCK = textwrap.dedent(
"""\
schema: agent-execution/v1
execution:
fresh_process_per_unit: true
permission: run-everything
fallback_to_host: all-unavailable
max_parallel: 3
timeout_seconds: 7200
summary_bytes: 16384
raw_output_bytes: 10485760
roles:
implementation: [cursor, claude]
repair: [cursor]
test: [claude]
review: [claude]
profiles:
cursor:
adapter: cursor
executable: cursor-agent
model: auto
enabled: true
claude:
adapter: claude
executable: claude
model: auto
enabled: true
"""
)
def write_repo(root: Path, body: str, *, extra_blocks: list[str] | None = None) -> Path:
parts = ["# Fixture\n", "```agent-execution\n", body.rstrip(), "\n```\n"]
if extra_blocks:
for block in extra_blocks:
parts.extend(["\n```agent-execution\n", block.rstrip(), "\n```\n"])
(root / "AGENT-EXECUTION.md").write_text("".join(parts), encoding="utf-8")
return root
class AgentExecConfigTests(unittest.TestCase):
def setUp(self) -> None:
self.tmp = tempfile.TemporaryDirectory(prefix="agent-exec-config-")
self.addCleanup(self.tmp.cleanup)
self.repo = Path(self.tmp.name)
def test_loads_single_agent_execution_block(self) -> None:
write_repo(self.repo, MINIMAL_BLOCK)
self.assertTrue(issubclass(ConfigError, ValueError))
cfg = load_config(self.repo)
self.assertIsInstance(cfg, ExecutionConfig)
self.assertEqual(cfg.schema, SCHEMA)
self.assertTrue(cfg.execution.fresh_process_per_unit)
self.assertEqual(cfg.execution.permission, "run-everything")
self.assertEqual(cfg.execution.fallback_to_host, "all-unavailable")
self.assertEqual(cfg.roles["implementation"], ("cursor", "claude"))
self.assertEqual(cfg.profiles["cursor"].executable, "cursor-agent")
self.assertEqual(cfg.custom_adapters, {})
self.assertEqual(validate_config(self.repo), [])
def test_rejects_missing_or_duplicate_blocks(self) -> None:
(self.repo / "AGENT-EXECUTION.md").write_text("# no block\n", encoding="utf-8")
with self.assertRaises(ConfigError):
load_config(self.repo)
write_repo(self.repo, MINIMAL_BLOCK, extra_blocks=[MINIMAL_BLOCK])
with self.assertRaises(ConfigError):
load_config(self.repo)
def test_rejects_unknown_schema_and_unknown_keys(self) -> None:
bad_schema = MINIMAL_BLOCK.replace(
"schema: agent-execution/v1", "schema: agent-execution/v0"
)
write_repo(self.repo, bad_schema)
with self.assertRaises(ConfigError):
load_config(self.repo)
bad_key = MINIMAL_BLOCK + "\nextra_top_level: true\n"
write_repo(self.repo, bad_key)
with self.assertRaises(ConfigError):
load_config(self.repo)
bad_exec = MINIMAL_BLOCK.replace(
"raw_output_bytes: 10485760\n",
"raw_output_bytes: 10485760\n mystery: 1\n",
)
write_repo(self.repo, bad_exec)
with self.assertRaises(ConfigError):
load_config(self.repo)
def test_role_order_does_not_follow_profile_order(self) -> None:
# profiles listed claude before cursor; role order must stay cursor -> claude
block = textwrap.dedent(
"""\
schema: agent-execution/v1
execution:
fresh_process_per_unit: true
permission: run-everything
fallback_to_host: all-unavailable
max_parallel: 1
timeout_seconds: 60
summary_bytes: 1024
raw_output_bytes: 4096
roles:
implementation: [cursor, claude]
repair: [cursor]
test: [claude]
review: [claude]
profiles:
claude:
adapter: claude
executable: claude
model: auto
enabled: true
cursor:
adapter: cursor
executable: cursor-agent
model: auto
enabled: true
"""
)
write_repo(self.repo, block)
cfg = load_config(self.repo)
self.assertEqual(cfg.roles["implementation"], ("cursor", "claude"))
self.assertEqual(list(cfg.profiles.keys())[0], "claude")
def test_model_auto_is_preserved_as_none(self) -> None:
write_repo(self.repo, MINIMAL_BLOCK)
cfg = load_config(self.repo)
self.assertIsNone(cfg.profiles["cursor"].model)
self.assertIsNone(cfg.profiles["claude"].model)
def test_rejects_unknown_profile_reference(self) -> None:
block = MINIMAL_BLOCK.replace(
"implementation: [cursor, claude]",
"implementation: [cursor, claude, missing]",
)
write_repo(self.repo, block)
with self.assertRaises(ConfigError):
load_config(self.repo)
def test_rejects_shell_command_and_secret_shaped_values(self) -> None:
shell = MINIMAL_BLOCK.replace(
"executable: cursor-agent",
"executable: \"cursor-agent && rm -rf /\"",
)
write_repo(self.repo, shell)
with self.assertRaises(ConfigError):
load_config(self.repo)
secret_key = MINIMAL_BLOCK.replace(
"enabled: true\n\n claude:",
"enabled: true\n api_token: \"sk-secret-value\"\n\n claude:",
)
write_repo(self.repo, secret_key)
with self.assertRaises(ConfigError):
load_config(self.repo)
secret_val = MINIMAL_BLOCK.replace(
"executable: claude",
"executable: \"Bearer abcdef0123456789abcdef0123456789\"",
)
write_repo(self.repo, secret_val)
with self.assertRaises(ConfigError):
load_config(self.repo)
def test_custom_placeholders_must_fill_whole_argv_items(self) -> None:
good = textwrap.dedent(
"""\
schema: agent-execution/v1
execution:
fresh_process_per_unit: true
permission: run-everything
fallback_to_host: all-unavailable
max_parallel: 1
timeout_seconds: 60
summary_bytes: 1024
raw_output_bytes: 4096
roles:
implementation: [mycustom]
repair: [mycustom]
test: [mycustom]
review: [mycustom]
profiles:
mycustom:
adapter: custom
executable: mycli
model: auto
enabled: true
custom_adapters:
mycustom:
base_argv: ["run", "${unit_packet}"]
run_everything_argv: ["--yes"]
workspace_argv: ["--cwd", "${workspace}"]
model_argv: ["--model", "${model}"]
prompt_transport: stdin
output_format: json
"""
)
write_repo(self.repo, good)
cfg = load_config(self.repo)
self.assertEqual(
cfg.custom_adapters["mycustom"].workspace_argv,
("--cwd", "${workspace}"),
)
bad = good.replace(
'workspace_argv: ["--cwd", "${workspace}"]',
'workspace_argv: ["--cwd=${workspace}"]',
)
write_repo(self.repo, bad)
with self.assertRaises(ConfigError):
load_config(self.repo)
unknown_ph = good.replace(
'base_argv: ["run", "${unit_packet}"]',
'base_argv: ["run", "${other}"]',
)
write_repo(self.repo, unknown_ph)
with self.assertRaises(ConfigError):
load_config(self.repo)
def test_numeric_limits_are_positive_and_bounded(self) -> None:
zero = MINIMAL_BLOCK.replace("max_parallel: 3", "max_parallel: 0")
write_repo(self.repo, zero)
with self.assertRaises(ConfigError):
load_config(self.repo)
negative = MINIMAL_BLOCK.replace("timeout_seconds: 7200", "timeout_seconds: -1")
write_repo(self.repo, negative)
with self.assertRaises(ConfigError):
load_config(self.repo)
huge = MINIMAL_BLOCK.replace("summary_bytes: 16384", "summary_bytes: 999999999")
write_repo(self.repo, huge)
with self.assertRaises(ConfigError):
load_config(self.repo)
inverted = MINIMAL_BLOCK.replace(
"summary_bytes: 16384\n raw_output_bytes: 10485760",
"summary_bytes: 10485760\n raw_output_bytes: 16384",
)
write_repo(self.repo, inverted)
with self.assertRaises(ConfigError):
load_config(self.repo)
def test_fresh_process_per_unit_must_be_true(self) -> None:
block = MINIMAL_BLOCK.replace(
"fresh_process_per_unit: true",
"fresh_process_per_unit: false",
)
write_repo(self.repo, block)
with self.assertRaises(ConfigError) as ctx:
load_config(self.repo)
self.assertIn("fresh_process_per_unit", str(ctx.exception))
def test_rejects_indentation_jump(self) -> None:
# root mapping child jumps from 0 spaces to 4 spaces (must be exactly +2)
jumped = textwrap.dedent(
"""\
schema: agent-execution/v1
execution:
fresh_process_per_unit: true
permission: run-everything
fallback_to_host: all-unavailable
max_parallel: 1
timeout_seconds: 60
summary_bytes: 1024
raw_output_bytes: 4096
roles:
implementation: [cursor]
repair: [cursor]
test: [cursor]
review: [cursor]
profiles:
cursor:
adapter: cursor
executable: cursor-agent
model: auto
enabled: true
"""
)
write_repo(self.repo, jumped)
with self.assertRaises(ConfigError) as ctx:
load_config(self.repo)
self.assertRegex(str(ctx.exception).lower(), r"indent|indentation")
def test_quoted_unicode_string_round_trips(self) -> None:
block = MINIMAL_BLOCK.replace(
"executable: claude",
'executable: "模型-β-测试"',
)
write_repo(self.repo, block)
cfg = load_config(self.repo)
self.assertEqual(cfg.profiles["claude"].executable, "模型-β-测试")
escaped = MINIMAL_BLOCK.replace(
"executable: claude",
r'executable: "say \"hi\" 模型"',
)
write_repo(self.repo, escaped)
cfg = load_config(self.repo)
self.assertEqual(cfg.profiles["claude"].executable, 'say "hi" 模型')
if __name__ == "__main__":
unittest.main()