diff options
Diffstat (limited to 'tests/Agent.Core.Tests/ConfigurationTests.cs')
| -rw-r--r-- | tests/Agent.Core.Tests/ConfigurationTests.cs | 216 |
1 files changed, 122 insertions, 94 deletions
diff --git a/tests/Agent.Core.Tests/ConfigurationTests.cs b/tests/Agent.Core.Tests/ConfigurationTests.cs index 954792b..0e5ec61 100644 --- a/tests/Agent.Core.Tests/ConfigurationTests.cs +++ b/tests/Agent.Core.Tests/ConfigurationTests.cs @@ -16,7 +16,7 @@ public sealed class ConfigurationTests using var directory = new TestDirectory(); string config = CreateValidConfiguration(directory); AgentConfiguration result = ConfigurationLoader.Load(config); - Assert.Equal(1, result.SchemaVersion); + Assert.Equal(2, result.SchemaVersion); Assert.Equal("model", result.Ollama.Model); Assert.Equal("none", result.Sandbox.Network.Access); } @@ -35,7 +35,7 @@ public sealed class ConfigurationTests { using var directory = new TestDirectory(); string config = CreateValidConfiguration(directory); - System.IO.File.WriteAllText(config, System.IO.File.ReadAllText(config).Replace("schema_version: 1", "schema_version: 2", StringComparison.Ordinal)); + System.IO.File.WriteAllText(config, System.IO.File.ReadAllText(config).Replace("schema_version: 2", "schema_version: 1", StringComparison.Ordinal)); Assert.Throws<ConfigurationException>(() => ConfigurationLoader.Load(config)); } @@ -70,20 +70,23 @@ public sealed class ConfigurationTests } [Fact] - public void CommandLineLanguageOverrideIsCaptured() + public void Removed_commands_and_configuration_overrides_are_rejected() { - CommandLine result = CommandLine.Parse(["serve", "--lang", "ja", "--transport", "http"]); - Assert.Equal("ja", result.Language); - Assert.Equal("http", result.Transport); + Assert.Throws<CommandLineException>(() => CommandLine.Parse(["serve"])); + Assert.Throws<CommandLineException>(() => CommandLine.Parse(["check"])); + Assert.Throws<CommandLineException>(() => CommandLine.Parse(["executable", "list"])); + Assert.Throws<CommandLineException>(() => CommandLine.Parse(["sandbox", "show"])); + Assert.Throws<CommandLineException>(() => CommandLine.Parse(["--lang", "ja"])); + Assert.Throws<CommandLineException>(() => CommandLine.Parse(["--transport", "http"])); } [Fact] - public void CommandLine_defaults_to_serve_when_command_is_omitted() + public void CommandLine_starts_server_when_command_is_omitted() { - Assert.Equal(AppCommand.Serve, CommandLine.Parse([]).Command); - CommandLine withOption = CommandLine.Parse(["--lang", "ja"]); - Assert.Equal(AppCommand.Serve, withOption.Command); - Assert.Equal("ja", withOption.Language); + Assert.Equal(AppCommand.Start, CommandLine.Parse([]).Command); + CommandLine withPath = CommandLine.Parse(["custom.yaml"]); + Assert.Equal(AppCommand.Start, withPath.Command); + Assert.Equal("custom.yaml", withPath.ConfigPath); } [Fact] @@ -130,7 +133,7 @@ public sealed class ConfigurationTests ConfigurationException error = Assert.Throws<ConfigurationException>(() => ConfigurationLoader.Load(config)); Assert.Contains("Legacy sandbox.filesystem/sandbox.process", error.Message, StringComparison.Ordinal); - Assert.Contains("sandbox resolve", error.Message, StringComparison.Ordinal); + Assert.Contains("agent-mcp apply", error.Message, StringComparison.Ordinal); } [Fact] @@ -155,38 +158,17 @@ public sealed class ConfigurationTests } [Fact] - public void Management_commands_require_config_and_capture_options() + public void Apply_and_show_accept_only_a_positional_config_path_and_apply_yes() { - Assert.Throws<CommandLineException>(() => CommandLine.Parse(["sandbox", "resolve"])); - CommandLine approve = CommandLine.Parse(["executable", "approve", "python", "--path", "C:/python.exe", "--no-runtime", "--yes", "--config", "agent.yaml"]); - Assert.Equal(AppCommand.ExecutableApprove, approve.Command); - Assert.Equal("python", approve.Alias); - Assert.True(approve.NoRuntime); - Assert.True(approve.Yes); - } - - [Fact] - public void Executable_alias_ambiguity_is_reported_without_automatic_selection() - { - using var directory = new TestDirectory(); - string first = Path.Combine(directory.Path, "first"); - string second = Path.Combine(directory.Path, "second"); - Directory.CreateDirectory(first); - Directory.CreateDirectory(second); - File.WriteAllText(Path.Combine(first, "fixture.exe"), "first"); - File.WriteAllText(Path.Combine(second, "fixture.exe"), "second"); - string? original = Environment.GetEnvironmentVariable("PATH"); - try - { - Environment.SetEnvironmentVariable("PATH", string.Join(Path.PathSeparator, first, second)); - - IReadOnlyList<string> candidates = ExecutableResolver.ResolveCandidates("fixture", null); - - Assert.Equal(2, candidates.Count); - Assert.Contains(candidates, item => item.Equals(Path.Combine(first, "fixture.exe"), StringComparison.OrdinalIgnoreCase)); - Assert.Contains(candidates, item => item.Equals(Path.Combine(second, "fixture.exe"), StringComparison.OrdinalIgnoreCase)); - } - finally { Environment.SetEnvironmentVariable("PATH", original); } + CommandLine apply = CommandLine.Parse(["apply", "agent.yaml", "--yes"]); + Assert.Equal(AppCommand.Apply, apply.Command); + Assert.Equal("agent.yaml", apply.ConfigPath); + Assert.True(apply.Yes); + CommandLine show = CommandLine.Parse(["show", "agent.yaml"]); + Assert.Equal(AppCommand.Show, show.Command); + Assert.False(show.Yes); + Assert.Throws<CommandLineException>(() => CommandLine.Parse(["show", "--yes"])); + Assert.Throws<CommandLineException>(() => CommandLine.Parse(["apply", "a.yaml", "b.yaml"])); } [Fact] @@ -198,10 +180,10 @@ public sealed class ConfigurationTests } [Fact] - public void Resolution_confirmation_is_invalidated_by_config_fingerprint_change() + public void Applied_snapshot_is_invalidated_by_configuration_change() { using var directory = new TestDirectory(); - string configPath = directory.File("agent.yaml", "schema_version: 1"); + string configPath = directory.File("agent.yaml", "schema_version: 2"); string workspace = Path.Combine(directory.Path, "workspace"); Directory.CreateDirectory(workspace); string stores = Path.Combine(directory.Path, "stores"); @@ -209,72 +191,95 @@ public sealed class ConfigurationTests { Sandbox = new SandboxOptions { Profile = "development", Workspaces = [workspace] } }; - var resolutionStore = new SandboxResolutionStore(configPath, stores, protectDirectory: false); - var approvalStore = new ExecutableApprovalStore(Path.Combine(stores, "approvals.json"), protectDirectory: false); - var resolver = new SandboxEnvironmentResolver(configPath, configuration, resolutionStore, approvalStore); - _ = resolver.Confirm(); + var store = new AppliedConfigurationStore(configPath, stores, protectDirectory: false); + var resolver = new AppliedSandboxConfiguration(configPath, configuration, store); + _ = resolver.Apply(); _ = resolver.Compile(Path.Combine(directory.Path, "journals"), Path.Combine(directory.Path, "backups")); configuration.Sandbox.Resources.Profile = "extended"; ConfigurationException error = Assert.Throws<ConfigurationException>(() => resolver.Compile(Path.Combine(directory.Path, "journals"), Path.Combine(directory.Path, "backups"))); - Assert.Contains("changed after confirmation", error.Message, StringComparison.Ordinal); + Assert.Equal("sandbox.configuration_not_applied", error.Code); } [Fact] - public void Approval_store_never_refreshes_a_changed_executable_identity() + public void Applied_snapshot_never_refreshes_a_changed_executable_identity() { using var directory = new TestDirectory(); - string configPath = directory.File("agent.yaml", "schema_version: 1"); + string configPath = directory.File("agent.yaml", "schema_version: 2"); string executable = directory.File("fixture.exe", "first"); string stores = Path.Combine(directory.Path, "stores"); var configuration = new AgentConfiguration(); - configuration.Sandbox.Execution.Executables.Add("fixture"); - var resolutionStore = new SandboxResolutionStore(configPath, stores, protectDirectory: false); - var approvalStore = new ExecutableApprovalStore(Path.Combine(stores, "approvals.json"), protectDirectory: false); - approvalStore.Approve(new("fixture", "executable", executable, ResolvedPath.Resolve(executable), - EntryExecutablePolicy.ComputeSha256(executable), null, DateTimeOffset.UtcNow)); - var resolver = new SandboxEnvironmentResolver(configPath, configuration, resolutionStore, approvalStore); - _ = resolver.Confirm(); + configuration.Sandbox.Execution.Entries.Add(new() { Alias = "fixture", Path = executable }); + var store = new AppliedConfigurationStore(configPath, stores, protectDirectory: false); + var resolver = new AppliedSandboxConfiguration(configPath, configuration, store); + _ = resolver.Apply(); File.WriteAllText(executable, "second"); ConfigurationException error = Assert.Throws<ConfigurationException>(() => resolver.Compile(Path.Combine(directory.Path, "journals"), Path.Combine(directory.Path, "backups"))); - Assert.Contains("SHA-256 changed", error.Message, StringComparison.Ordinal); - Assert.Single(approvalStore.List()); - Assert.True(approvalStore.Revoke("fixture")); - Assert.Empty(approvalStore.List()); + Assert.Equal("sandbox.configuration_not_applied", error.Code); + Assert.Single(store.Load()!.Execution); Assert.Empty(Directory.EnumerateFiles(stores, "*.tmp")); } [Fact] - public void Approval_store_rejects_malformed_identity_records() + public void Reapply_replaces_the_complete_snapshot_and_removes_deleted_entries() + { + using var directory = new TestDirectory(); + string configPath = directory.File("agent.yaml", "schema_version: 2"); + string executable = directory.File("fixture.exe", "first"); + string stores = Path.Combine(directory.Path, "stores"); + var configuration = new AgentConfiguration(); + configuration.Sandbox.Execution.Entries.Add(new() { Alias = "fixture", Path = executable }); + var store = new AppliedConfigurationStore(configPath, stores, protectDirectory: false); + var applied = new AppliedSandboxConfiguration(configPath, configuration, store); + + _ = applied.Apply(); + Assert.Single(store.Load()!.Execution); + configuration.Sandbox.Execution.Entries.Clear(); + _ = applied.Apply(); + + AppliedConfigurationDocument document = store.Load()!; + Assert.Empty(document.Execution); + Assert.Equal("none", document.Network.Access); + Assert.Equal(4096, document.Resources.MemoryMb); + } + + [Fact] + public void Applied_store_rejects_malformed_records() { using var directory = new TestDirectory(); - string storePath = directory.File("stores/approvals.json", """ + string configPath = directory.File("agent.yaml", "schema_version: 2"); + var store = new AppliedConfigurationStore(configPath, Path.Combine(directory.Path, "stores"), protectDirectory: false); + Directory.CreateDirectory(Path.GetDirectoryName(store.StorePath)!); + File.WriteAllText(store.StorePath, """ { - "schemaVersion": 1, - "approvals": [{ + "schemaVersion": 2, + "configPath": "relative/agent.yaml", + "configFingerprint": "not-a-hash", + "filesystem": [], + "execution": [{ "alias": "Python With Spaces", "classification": "unknown", "logicalPath": "python.exe", "resolvedPath": "relative/python.exe", "sha256": "not-a-hash", - "runtimeRoot": null, - "approvedAt": "2026-08-12T00:00:00Z" - }] + "logicalRuntimeRoot": null, + "resolvedRuntimeRoot": null + }], + "appliedAt": "2026-08-12T00:00:00Z" } """); - var store = new ExecutableApprovalStore(storePath, protectDirectory: false); - Assert.Throws<ConfigurationException>(() => store.List()); + Assert.Throws<ConfigurationException>(() => store.Load()); } [Fact] public void Narrow_nested_mount_is_rejected_instead_of_silently_widened() { using var directory = new TestDirectory(); - string configPath = directory.File("agent.yaml", "schema_version: 1"); + string configPath = directory.File("agent.yaml", "schema_version: 2"); string workspace = Path.Combine(directory.Path, "workspace"); string nested = Path.Combine(workspace, "read-only-data"); Directory.CreateDirectory(nested); @@ -288,10 +293,9 @@ public sealed class ConfigurationTests Mounts = [new MountOptions { Path = nested, Access = "read_only" }] } }; - var resolver = new SandboxEnvironmentResolver(configPath, configuration, - new SandboxResolutionStore(configPath, stores, protectDirectory: false), - new ExecutableApprovalStore(Path.Combine(stores, "approvals.json"), protectDirectory: false)); - _ = resolver.Confirm(); + var resolver = new AppliedSandboxConfiguration(configPath, configuration, + new AppliedConfigurationStore(configPath, stores, protectDirectory: false)); + _ = resolver.Apply(); ConfigurationException error = Assert.Throws<ConfigurationException>(() => resolver.Compile(Path.Combine(directory.Path, "journals"), Path.Combine(directory.Path, "backups"))); @@ -299,10 +303,36 @@ public sealed class ConfigurationTests } [Fact] + public void Execution_entry_inside_development_workspace_inherits_workspace_capability() + { + using var directory = new TestDirectory(); + string configPath = directory.File("agent.yaml", "schema_version: 2"); + string workspace = Path.Combine(directory.Path, "workspace"); + Directory.CreateDirectory(workspace); + string executable = Path.Combine(workspace, "tool.exe"); + File.WriteAllText(executable, "fixture"); + var configuration = new AgentConfiguration + { + Sandbox = new SandboxOptions { Profile = "development", Workspaces = [workspace] } + }; + configuration.Sandbox.Execution.Entries.Add(new() { Alias = "tool", Path = executable }); + var applied = new AppliedSandboxConfiguration(configPath, configuration, + new AppliedConfigurationStore(configPath, Path.Combine(directory.Path, "stores"), protectDirectory: false)); + _ = applied.Apply(); + + Agent.Windows.Tools.EffectiveSandboxPolicy policy = applied.Compile( + Path.Combine(directory.Path, "journals"), Path.Combine(directory.Path, "backups")); + + Assert.Contains(policy.EntryExecutables, item => item.Alias == "tool"); + Assert.Contains(policy.Files, item => item.Path.Equals(workspace, StringComparison.OrdinalIgnoreCase) && + item.Capabilities.HasFlag(FileCapability.Execute)); + } + + [Fact] public void Profile_network_and_resource_configuration_compile_to_effective_policy() { using var directory = new TestDirectory(); - string configPath = directory.File("agent.yaml", "schema_version: 1"); + string configPath = directory.File("agent.yaml", "schema_version: 2"); string workspace = Path.Combine(directory.Path, "workspace"); Directory.CreateDirectory(workspace); string stores = Path.Combine(directory.Path, "stores"); @@ -316,10 +346,9 @@ public sealed class ConfigurationTests Resources = new ResourceOptions { Profile = "constrained", ProcessLimit = 6, ExecutionTimeoutSeconds = 450 } } }; - var resolver = new SandboxEnvironmentResolver(configPath, configuration, - new SandboxResolutionStore(configPath, stores, protectDirectory: false), - new ExecutableApprovalStore(Path.Combine(stores, "approvals.json"), protectDirectory: false)); - _ = resolver.Confirm(); + var resolver = new AppliedSandboxConfiguration(configPath, configuration, + new AppliedConfigurationStore(configPath, stores, protectDirectory: false)); + _ = resolver.Apply(); Agent.Windows.Tools.EffectiveSandboxPolicy policy = resolver.Compile( Path.Combine(directory.Path, "journals"), Path.Combine(directory.Path, "backups")); @@ -338,15 +367,14 @@ public sealed class ConfigurationTests public void Readonly_profile_never_creates_an_artifact_root_or_execute_capability() { using var directory = new TestDirectory(); - string configPath = directory.File("agent.yaml", "schema_version: 1"); + string configPath = directory.File("agent.yaml", "schema_version: 2"); string workspace = Path.Combine(directory.Path, "workspace"); Directory.CreateDirectory(workspace); string stores = Path.Combine(directory.Path, "stores"); var configuration = new AgentConfiguration { Sandbox = new SandboxOptions { Profile = "readonly", Workspaces = [workspace] } }; - var resolver = new SandboxEnvironmentResolver(configPath, configuration, - new SandboxResolutionStore(configPath, stores, protectDirectory: false), - new ExecutableApprovalStore(Path.Combine(stores, "approvals.json"), protectDirectory: false)); - _ = resolver.Confirm(); + var resolver = new AppliedSandboxConfiguration(configPath, configuration, + new AppliedConfigurationStore(configPath, stores, protectDirectory: false)); + _ = resolver.Apply(); Agent.Windows.Tools.EffectiveSandboxPolicy policy = resolver.Compile( Path.Combine(directory.Path, "journals"), Path.Combine(directory.Path, "backups")); @@ -380,15 +408,15 @@ public sealed class ConfigurationTests [Fact] public void Startup_errors_follow_the_cli_locale() { - string message = Program.FormatStartupError(["--lang", "ja"], new CommandLineException("bad option")); - Assert.Contains("コマンドラインが不正です", message, StringComparison.Ordinal); + string message = Program.FormatStartupError([], new CommandLineException("bad option")); + Assert.Contains("Invalid command line", message, StringComparison.Ordinal); Assert.Contains("bad option", message, StringComparison.Ordinal); - string sandbox = Program.FormatStartupError(["--lang", "ja"], new ConfigurationException( - "internal", code: "executable.hash_changed", category: "sandbox", target: "C:/Python/python.exe", + string sandbox = Program.FormatStartupError([], new ConfigurationException( + "internal", code: "sandbox.configuration_not_applied", category: "sandbox", target: "C:/config/agent.yaml", resolution: "internal resolution")); - Assert.Contains("SHA-256が変更されました", sandbox, StringComparison.Ordinal); - Assert.Contains("明示的に再承認", sandbox, StringComparison.Ordinal); + Assert.Contains("not been applied", sandbox, StringComparison.Ordinal); + Assert.Contains("agent-mcp apply", sandbox, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("internal resolution", sandbox, StringComparison.Ordinal); } @@ -447,7 +475,7 @@ public sealed class ConfigurationTests Directory.CreateDirectory(System.IO.Path.Combine(directory.Path, "config/locales")); Directory.CreateDirectory(System.IO.Path.Combine(directory.Path, "skills")); return directory.File("agent.yaml", """ - schema_version: 1 + schema_version: 2 application: language: en agent: |
