Files
aryx/sidecar/tests/Aryx.AgentHost.Tests/HookConfigLoaderTests.cs
T

205 lines
6.6 KiB
C#

using Aryx.AgentHost.Contracts;
using Aryx.AgentHost.Services;
namespace Aryx.AgentHost.Tests;
public sealed class HookConfigLoaderTests
{
[Fact]
public async Task LoadAsync_ParsesSupportedHookTypes()
{
using TestDirectory project = new();
string hooksDirectory = Directory.CreateDirectory(Path.Combine(project.Path, ".github", "hooks")).FullName;
await File.WriteAllTextAsync(
Path.Combine(hooksDirectory, "hooks.json"),
"""
{
"version": 1,
"hooks": {
"sessionStart": [
{
"type": "command",
"bash": "echo session-start",
"powershell": "Write-Output session-start",
"cwd": ".",
"env": { "HOOK_MODE": "audit" },
"timeoutSec": 15
}
],
"preToolUse": [
{
"type": "command",
"bash": "echo pre-tool",
"powershell": "Write-Output pre-tool"
}
],
"errorOccurred": [
{
"type": "command",
"bash": "echo error-hook",
"powershell": "Write-Output error-hook"
}
]
}
}
""");
ResolvedHookSet hooks = await HookConfigLoader.LoadAsync(project.Path, CancellationToken.None);
HookCommandDefinition sessionStart = Assert.Single(hooks.SessionStart);
Assert.Equal("command", sessionStart.Type);
Assert.Equal("echo session-start", sessionStart.Bash);
Assert.Equal("Write-Output session-start", sessionStart.PowerShell);
Assert.Equal(".", sessionStart.Cwd);
Assert.Equal(15, sessionStart.TimeoutSec);
Assert.NotNull(sessionStart.Env);
Assert.Equal("audit", sessionStart.Env["HOOK_MODE"]);
Assert.Single(hooks.PreToolUse);
Assert.Single(hooks.ErrorOccurred);
Assert.False(hooks.IsEmpty);
}
[Fact]
public async Task LoadAsync_MergesHookFilesInFileNameOrder()
{
using TestDirectory project = new();
string hooksDirectory = Directory.CreateDirectory(Path.Combine(project.Path, ".github", "hooks")).FullName;
await File.WriteAllTextAsync(
Path.Combine(hooksDirectory, "20-second.json"),
"""
{
"version": 1,
"hooks": {
"preToolUse": [
{
"type": "command",
"bash": "echo second",
"powershell": "Write-Output second"
}
]
}
}
""");
await File.WriteAllTextAsync(
Path.Combine(hooksDirectory, "10-first.json"),
"""
{
"version": 1,
"hooks": {
"preToolUse": [
{
"type": "command",
"bash": "echo first",
"powershell": "Write-Output first"
}
]
}
}
""");
ResolvedHookSet hooks = await HookConfigLoader.LoadAsync(project.Path, CancellationToken.None);
string[] commands = hooks.PreToolUse.Select(GetCommandText).ToArray();
Assert.Equal(2, commands.Length);
Assert.Contains("first", commands[0], StringComparison.Ordinal);
Assert.Contains("second", commands[1], StringComparison.Ordinal);
}
[Fact]
public async Task LoadAsync_ReturnsEmptyWhenHooksDirectoryIsMissing()
{
using TestDirectory project = new();
ResolvedHookSet hooks = await HookConfigLoader.LoadAsync(project.Path, CancellationToken.None);
Assert.Same(ResolvedHookSet.Empty, hooks);
}
[Fact]
public async Task LoadAsync_SkipsInvalidFilesAndUnsupportedVersions()
{
using TestDirectory project = new();
string hooksDirectory = Directory.CreateDirectory(Path.Combine(project.Path, ".github", "hooks")).FullName;
await File.WriteAllTextAsync(Path.Combine(hooksDirectory, "00-invalid.json"), "{ not-json");
await File.WriteAllTextAsync(
Path.Combine(hooksDirectory, "10-unsupported.json"),
"""
{
"version": 2,
"hooks": {
"sessionStart": [
{
"type": "command",
"bash": "echo unsupported",
"powershell": "Write-Output unsupported"
}
]
}
}
""");
await File.WriteAllTextAsync(
Path.Combine(hooksDirectory, "20-valid.json"),
"""
{
"version": 1,
"hooks": {
"sessionEnd": [
{
"type": "command",
"bash": "echo valid",
"powershell": "Write-Output valid"
}
]
}
}
""");
ResolvedHookSet hooks = await HookConfigLoader.LoadAsync(project.Path, CancellationToken.None);
HookCommandDefinition valid = Assert.Single(hooks.SessionEnd);
Assert.Contains("valid", GetCommandText(valid), StringComparison.Ordinal);
Assert.Empty(hooks.SessionStart);
}
[Fact]
public async Task LoadAsync_ReturnsEmptyForEmptyHooksObject()
{
using TestDirectory project = new();
string hooksDirectory = Directory.CreateDirectory(Path.Combine(project.Path, ".github", "hooks")).FullName;
await File.WriteAllTextAsync(
Path.Combine(hooksDirectory, "hooks.json"),
"""
{
"version": 1,
"hooks": {}
}
""");
ResolvedHookSet hooks = await HookConfigLoader.LoadAsync(project.Path, CancellationToken.None);
Assert.Same(ResolvedHookSet.Empty, hooks);
}
private static string GetCommandText(HookCommandDefinition hook)
=> hook.PowerShell ?? hook.Bash ?? string.Empty;
private sealed class TestDirectory : IDisposable
{
private readonly DirectoryInfo _directory = Directory.CreateTempSubdirectory("aryx-hooks-loader-");
public string Path => _directory.FullName;
public void Dispose()
{
if (_directory.Exists)
{
_directory.Delete(recursive: true);
}
}
}
}