Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/Infrastructure/AirlockRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public static int Run(
DebugLogger.Log($"Using external network: {externalNetwork}");
}

var appImage = $"{ImagePrefix}:{imageTag}";
var appImage = ContainerRunner.IsAbsoluteImageReference(imageTag) ? imageTag : $"{ImagePrefix}:{imageTag}";
var proxyImage = $"{ImagePrefix}:proxy";

Console.WriteLine("🛡️ Starting in Airlock mode...");
Expand Down
10 changes: 9 additions & 1 deletion app/Infrastructure/ContainerRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,16 @@ public static class ContainerRunner

/// <summary>
/// Gets the full image name for a given tag.
/// If the tag contains a '/', it is treated as an absolute image reference and used as-is.
/// </summary>
public static string GetImageName(string tag) => $"{ImagePrefix}:{tag}";
public static string GetImageName(string tag) =>
IsAbsoluteImageReference(tag) ? tag : $"{ImagePrefix}:{tag}";

/// <summary>
/// Determines whether a value is an absolute image reference (e.g., "myregistry.io/myimage:v1")
/// rather than a simple tag (e.g., "dotnet-rust").
/// </summary>
public static bool IsAbsoluteImageReference(string value) => value.Contains('/');

/// <summary>
/// Runs a container command with the given arguments.
Expand Down
4 changes: 4 additions & 0 deletions app/Tools/EchoTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ public class EchoTool : Infrastructure.ICliTool

public string GetImageName(string tag)
{
// If the tag is an absolute image reference (contains '/'), use it as-is
if (Infrastructure.ContainerRunner.IsAbsoluteImageReference(tag))
return tag;

// Echo uses the same images as GitHub Copilot (copilot-* tags)
// It doesn't need separate images - it just echoes config instead of running the tool
const string imagePrefix = "ghcr.io/gordonbeeming/copilot_here";
Expand Down
4 changes: 4 additions & 0 deletions app/Tools/GitHubCopilotTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ public sealed class GitHubCopilotTool : ICliTool

public string GetImageName(string tag)
{
// If the tag is an absolute image reference (contains '/'), use it as-is
if (ContainerRunner.IsAbsoluteImageReference(tag))
return tag;

// Image name format: ghcr.io/gordonbeeming/copilot_here:copilot-{variant}
// Tag matches what users invoke: "copilot" (not "github-copilot")
const string imagePrefix = "ghcr.io/gordonbeeming/copilot_here";
Expand Down
93 changes: 93 additions & 0 deletions tests/CopilotHere.UnitTests/ContainerRunnerImageTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using CopilotHere.Infrastructure;
using TUnit.Core;

namespace CopilotHere.Tests;

public class ContainerRunnerImageTests
{
// === IsAbsoluteImageReference ===

[Test]
public async Task IsAbsoluteImageReference_WithSimpleTag_ReturnsFalse()
{
await Assert.That(ContainerRunner.IsAbsoluteImageReference("latest")).IsFalse();
}

[Test]
public async Task IsAbsoluteImageReference_WithDotnetTag_ReturnsFalse()
{
await Assert.That(ContainerRunner.IsAbsoluteImageReference("dotnet-rust")).IsFalse();
}

[Test]
public async Task IsAbsoluteImageReference_WithRegistryImage_ReturnsTrue()
{
await Assert.That(ContainerRunner.IsAbsoluteImageReference("myregistry.io/myimage:v1")).IsTrue();
}

[Test]
public async Task IsAbsoluteImageReference_WithDockerHubImage_ReturnsTrue()
{
await Assert.That(ContainerRunner.IsAbsoluteImageReference("myuser/myimage:latest")).IsTrue();
}

[Test]
public async Task IsAbsoluteImageReference_WithGhcrImage_ReturnsTrue()
{
await Assert.That(ContainerRunner.IsAbsoluteImageReference("ghcr.io/myorg/myimage:v2")).IsTrue();
}

[Test]
public async Task IsAbsoluteImageReference_WithLocalRegistryImage_ReturnsTrue()
{
await Assert.That(ContainerRunner.IsAbsoluteImageReference("localhost:5000/myimage:dev")).IsTrue();
}

[Test]
public async Task IsAbsoluteImageReference_WithNestedPath_ReturnsTrue()
{
await Assert.That(ContainerRunner.IsAbsoluteImageReference("registry.example.com/org/team/image:tag")).IsTrue();
}

// === GetImageName ===

[Test]
public async Task GetImageName_WithSimpleTag_ReturnsFullImageWithPrefix()
{
// Act
var imageName = ContainerRunner.GetImageName("latest");

// Assert
await Assert.That(imageName).IsEqualTo("ghcr.io/gordonbeeming/copilot_here:latest");
}

[Test]
public async Task GetImageName_WithDotnetTag_ReturnsFullImageWithPrefix()
{
// Act
var imageName = ContainerRunner.GetImageName("dotnet-rust");

// Assert
await Assert.That(imageName).IsEqualTo("ghcr.io/gordonbeeming/copilot_here:dotnet-rust");
}

[Test]
public async Task GetImageName_WithAbsoluteImage_ReturnsAsIs()
{
// Act
var imageName = ContainerRunner.GetImageName("myregistry.io/myimage:v1");

// Assert
await Assert.That(imageName).IsEqualTo("myregistry.io/myimage:v1");
}

[Test]
public async Task GetImageName_WithDockerHubImage_ReturnsAsIs()
{
// Act
var imageName = ContainerRunner.GetImageName("myuser/custom-copilot:latest");

// Assert
await Assert.That(imageName).IsEqualTo("myuser/custom-copilot:latest");
}
}
20 changes: 20 additions & 0 deletions tests/CopilotHere.UnitTests/EchoToolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,26 @@ public async Task GetImageName_WithDotnet_ReturnsCorrectFormat()
await Assert.That(imageName).IsEqualTo("ghcr.io/gordonbeeming/copilot_here:copilot-dotnet");
}

[Test]
public async Task GetImageName_WithAbsoluteImage_ReturnsAsIs()
{
// Act
var imageName = _tool.GetImageName("myregistry.io/custom-image:v1");

// Assert
await Assert.That(imageName).IsEqualTo("myregistry.io/custom-image:v1");
}

[Test]
public async Task GetImageName_WithDockerHubAbsoluteImage_ReturnsAsIs()
{
// Act
var imageName = _tool.GetImageName("myuser/custom-image:latest");

// Assert
await Assert.That(imageName).IsEqualTo("myuser/custom-image:latest");
}

[Test]
public async Task GetDockerfile_ReturnsCorrectPath()
{
Expand Down
20 changes: 20 additions & 0 deletions tests/CopilotHere.UnitTests/GitHubCopilotToolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,26 @@ public async Task GetImageName_WithEmptyTag_ReturnsDefault()
await Assert.That(imageName).IsEqualTo("ghcr.io/gordonbeeming/copilot_here:copilot-latest");
}

[Test]
public async Task GetImageName_WithAbsoluteImage_ReturnsAsIs()
{
// Act
var imageName = _tool.GetImageName("myregistry.io/custom-copilot:v1");

// Assert
await Assert.That(imageName).IsEqualTo("myregistry.io/custom-copilot:v1");
}

[Test]
public async Task GetImageName_WithDockerHubAbsoluteImage_ReturnsAsIs()
{
// Act
var imageName = _tool.GetImageName("myuser/copilot-custom:latest");

// Assert
await Assert.That(imageName).IsEqualTo("myuser/copilot-custom:latest");
}

[Test]
public async Task GetDockerfile_ReturnsCorrectPath()
{
Expand Down
Loading