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
153 changes: 153 additions & 0 deletions tests/OpenCode.Sdk.Tests/Credentials/CredentialsClientLiveTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
using OpenCode.Sdk.Models;
using OpenCode.Sdk.Tests.Harness;
using OpenCode.Sdk.Tests.Harness.Ownership;

namespace OpenCode.Sdk.Tests.Credentials;

[ClassDataSource<SimulatedDriveServerFixture>(Shared = SharedType.PerTestSession)]
[NotInParallel(ParallelConstraintKeys.ServerProcess)]
public sealed class CredentialsClientLiveTests(SimulatedDriveServerFixture server)
{
private const string InitialLabel = "Owned coverage credential";
private const string ReplacementLabel = "Renamed owned credential";
private readonly string _integrationId = "sdk-credential-" + Guid.NewGuid().ToString("N");

[Test]
[Timeout(60_000)]
public async Task RemoveCredentialAsync_Should_Remove_The_Owned_Row_When_It_Exists(CancellationToken cancellationToken)
{
await WithCredentialsAsync(async (client, cleanup) =>
{
var id = await CreateOwnedAsync(client, cleanup, InitialLabel, cancellationToken);
var before = await ListOwnedAsync(client, [id], cancellationToken);
await Assert.That(before.Select(entry => entry.Id)).Contains(id);

_ = await client.Credentials.RemoveCredentialAsync(id, cancellationToken: cancellationToken);

var after = await ListOwnedAsync(client, [id], cancellationToken);
await Assert.That(after.Select(entry => entry.Id)).DoesNotContain(id);
});
}

[Test]
[Timeout(60_000)]
public async Task CreateCredentialAsync_Should_Persist_The_Row_When_The_Id_Is_Unused(CancellationToken cancellationToken)
{
await WithCredentialsAsync(async (client, cleanup) =>
{
var (request, id) = OwnCredential(client, cleanup, InitialLabel);

_ = await client.Credentials.CreateCredentialAsync(request, cancellationToken: cancellationToken);

_ = await AssertOwnedRowAsync(client, id, InitialLabel, cancellationToken);
});
}

[Test]
[Timeout(60_000)]
public async Task ActivateCredentialAsync_Should_Select_The_Older_Row_When_A_Newer_Row_Is_Active(CancellationToken cancellationToken)
{
await WithCredentialsAsync(async (client, cleanup) =>
{
var olderId = await CreateOwnedAsync(client, cleanup, InitialLabel, cancellationToken);
var newerId = await CreateOwnedAsync(client, cleanup, ReplacementLabel, cancellationToken);
var before = await ListOwnedAsync(client, [olderId, newerId], cancellationToken);
await Assert.That(before.Single(entry => entry.Id == olderId).Active).IsFalse();
await Assert.That(before.Single(entry => entry.Id == newerId).Active).IsTrue();

_ = await client.Credentials.ActivateCredentialAsync(olderId, cancellationToken: cancellationToken);

var after = await ListOwnedAsync(client, [olderId, newerId], cancellationToken);
await Assert.That(after.Single(entry => entry.Id == olderId).Active).IsTrue();
await Assert.That(after.Single(entry => entry.Id == newerId).Active).IsFalse();
});
}

[Test]
[Timeout(60_000)]
public async Task ListCredentialsAsync_Should_Return_The_Owned_Metadata_When_A_Row_Was_Created(CancellationToken cancellationToken)
{
await WithCredentialsAsync(async (client, cleanup) =>
{
var id = await CreateOwnedAsync(client, cleanup, InitialLabel, cancellationToken);

var entry = await AssertOwnedRowAsync(client, id, InitialLabel, cancellationToken);
await Assert.That(entry.Active).IsTrue();
});
}

[Test]
[Timeout(60_000)]
public async Task UpdateCredentialAsync_Should_Replace_The_Label_When_A_Row_Exists(CancellationToken cancellationToken)
{
await WithCredentialsAsync(async (client, cleanup) =>
{
var id = await CreateOwnedAsync(client, cleanup, InitialLabel, cancellationToken);

_ = await client.Credentials.UpdateCredentialAsync(id,
new CredentialUpdateRequest { Label = ReplacementLabel }, cancellationToken: cancellationToken);

var owned = await ListOwnedAsync(client, [id], cancellationToken);
await Assert.That(owned.Single().Label).IsEqualTo(ReplacementLabel);
});
}

private async Task WithCredentialsAsync(Func<OpenCodeClient, OwnedCleanup, Task> scenario)
{
using var workspace = server.CreateWorkspace();
using var client = CreateWorkspaceClient(workspace);
var cleanup = new OwnedCleanup(TimeSpan.FromSeconds(15));
Exception? failure = null;
try
{
await scenario(client, cleanup);
}
catch (Exception exception)
{
failure = exception;
}

await cleanup.CompleteAsync(failure);
}

private OpenCodeClient CreateWorkspaceClient(TestWorkspace workspace) =>
server.CreateClient(new LocationSelector { Directory = workspace.Path });

private (CredentialCreateRequest Request, string Id) OwnCredential(OpenCodeClient client, OwnedCleanup cleanup, string label)
{
var id = "cred_" + Guid.NewGuid().ToString("N");
cleanup.Own("owned credential removal", async token =>
_ = await client.Credentials.RemoveCredentialAsync(id, cancellationToken: token));
return (new CredentialCreateRequest
{
Id = id,
IntegrationId = _integrationId,
Label = label,
Value = new CredentialKey { Key = "inert-sdk-coverage-sentinel" },
}, id);
}

private async Task<string> CreateOwnedAsync(OpenCodeClient client, OwnedCleanup cleanup, string label, CancellationToken token)
{
var (request, id) = OwnCredential(client, cleanup, label);
_ = await client.Credentials.CreateCredentialAsync(request, cancellationToken: token);
return id;
}

private async Task<CredentialEntry> AssertOwnedRowAsync(OpenCodeClient client, string id, string label, CancellationToken token)
{
var owned = await ListOwnedAsync(client, [id], token);
await Assert.That(owned.Select(entry => entry.Id)).Contains(id);
var entry = owned.Single();
await Assert.That(entry.IntegrationId).IsEqualTo(_integrationId);
await Assert.That(entry.Label).IsEqualTo(label);
return entry;
}

private static async Task<IReadOnlyList<CredentialEntry>> ListOwnedAsync(OpenCodeClient client, IEnumerable<string> ids, CancellationToken token)
{
var response = await client.Credentials.ListCredentialsAsync(cancellationToken: token);
// Filter before inspecting metadata; never inspect or assert a credential's value.
return [.. response.Credentials.Where(entry => ids.Contains(entry.Id, StringComparer.Ordinal))];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ await WithMcpEntryAsync(async (client, name, probe) =>
var before = await client.McpServers.ListMcpServersAsync(cancellationToken: cancellationToken);
await Assert.That(before.McpServers.Select(item => item.Name)).DoesNotContain(name);

await AddDisabledMcpAsync(client, name, cancellationToken);
await DisabledMcpServer.AddAsync(client, name, cancellationToken);

// Check finite state before waiting for the event a no-op could never publish.
var after = await client.McpServers.ListMcpServersAsync(cancellationToken: cancellationToken);
Expand All @@ -40,7 +40,7 @@ public async Task RemoveMcpServerAsync_Should_Remove_The_Entry_When_It_Exists(Ca
{
await WithMcpEntryAsync(async (client, name, probe) =>
{
await AddDisabledMcpAsync(client, name, cancellationToken);
await DisabledMcpServer.AddAsync(client, name, cancellationToken);
var added = await WaitForMcpChangeAsync(probe, name, cancellationToken);
await AssertDisabledMcpAsync(client, name, cancellationToken);

Expand Down Expand Up @@ -138,7 +138,7 @@ private async Task WithMcpEntryAsync(Func<OpenCodeClient, string, SessionEventPr
using var client = server.CreateClient(new LocationSelector { Directory = workspace.Path });
var name = "sdk-disabled-" + Guid.NewGuid().ToString("N");
var cleanup = new OwnedCleanup(TimeSpan.FromSeconds(15));
cleanup.Own("runtime MCP removal", async cancellation => await RemoveOwnedMcpAsync(client, name, cancellation));
cleanup.Own("runtime MCP removal", async cancellation => await DisabledMcpServer.RemoveOwnedAsync(client, name, cancellation));
var reader = new OwnedEventReader(TimeSpan.FromSeconds(120), TimeSpan.FromSeconds(15), token);
cleanup.Own("MCP event reader", TimeSpan.FromSeconds(75), _ => reader.CompleteAsync(null));
var probe = new SessionEventProbe(reader);
Expand All @@ -157,17 +157,6 @@ private async Task WithMcpEntryAsync(Func<OpenCodeClient, string, SessionEventPr
await cleanup.CompleteAsync(primaryFailure);
}

private static async Task AddDisabledMcpAsync(OpenCodeClient client, string name, CancellationToken token) =>
_ = await client.Experimental.AddMcpServerAsync(name, new ExperimentalMcpAddRequest
{
Config = new McpRemoteConfig
{
Disabled = true,
OAuth = McpRemoteConfigOAuth.FromBoolean(false),
Url = "https://sdk-coverage.invalid/mcp",
},
}, cancellationToken: token);

private static async Task<McpStatusChanged> WaitForMcpChangeAsync(SessionEventProbe probe, string name, CancellationToken token)
{
using var barrier = SessionEventProbe.Barrier(token);
Expand All @@ -181,15 +170,4 @@ private static async Task AssertDisabledMcpAsync(OpenCodeClient client, string n
await Assert.That(entries).Count().IsEqualTo(1);
await Assert.That(entries[0].Status).IsTypeOf<McpStatusDisabled>();
}

private static async Task RemoveOwnedMcpAsync(OpenCodeClient client, string name, CancellationToken token)
{
var response = await client.Experimental.RemoveMcpServerAsync(name, requestOptions: OpenCodeRequestOptions.NoThrow, cancellationToken: token);
if (response.Status == 204 || (response is { Status: 404, Error: McpServerNotFoundError missing } && missing.Server == name))
{
return;
}

throw new InvalidOperationException("Owned MCP removal failed: " + response.Error?.GetType().Name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using OpenCode.Sdk.Models;
using OpenCode.Sdk.Tests.Harness;
using OpenCode.Sdk.Tests.Harness.Ownership;

namespace OpenCode.Sdk.Tests.Integrations;

[ClassDataSource<SimulatedDriveServerFixture>(Shared = SharedType.PerTestSession)]
[NotInParallel(ParallelConstraintKeys.ServerProcess)]
public sealed class IntegrationClientLiveTests(SimulatedDriveServerFixture server)
{
[Test]
[Timeout(60_000)]
public async Task ConnectWithKeyAsync_Should_Store_A_Connection_When_The_Seeded_Key_Method_Is_Used(CancellationToken cancellationToken)
{
await WithSeededIntegrationAsync(async (client, integration, cleanup) =>
{
var label = "sdk-key-" + Guid.NewGuid().ToString("N");
cleanup.Own("owned connection credential removal", async token =>
{
var owned = await ListOwnedIdsAsync(client, label, token);
foreach (var id in owned)
{
_ = await client.Credentials.RemoveCredentialAsync(id, cancellationToken: token);
}
});

_ = await integration.ConnectWithKeyAsync(new IntegrationConnectKeyRequest
{
Key = "inert-local-connection-sentinel",
Label = label,
}, cancellationToken: cancellationToken);

var createdIds = await ListOwnedIdsAsync(client, label, cancellationToken);
await Assert.That(createdIds.Count).IsEqualTo(1);
}, cancellationToken);
}

[Test]
[Timeout(60_000)]
public async Task GetAsync_Should_Return_The_Seeded_Integration_When_Its_Id_Is_Requested(CancellationToken cancellationToken)
{
await WithSeededIntegrationAsync(async (_, integration, _) =>
{
var response = await integration.GetAsync(cancellationToken: cancellationToken);

await Assert.That(response.Integration.Id).IsEqualTo(SimulationConfigSeed.ProviderId);
await Assert.That(response.Integration.Name).IsEqualTo("Simulated");
await Assert.That(response.Integration.Methods).Contains(method => method is IntegrationKeyMethod);
}, cancellationToken);
}

private async Task WithSeededIntegrationAsync(Func<OpenCodeClient, IntegrationClient, OwnedCleanup, Task> scenario, CancellationToken token)
{
using var workspace = server.CreateWorkspace();
using var client = server.CreateClient(new LocationSelector { Directory = workspace.Path });
var cleanup = new OwnedCleanup(TimeSpan.FromSeconds(15));
Exception? failure = null;
try
{
// The list handler awaits plugin activation; Get deliberately does not.
var catalog = await client.Integrations.ListIntegrationsAsync(cancellationToken: token);
var seeded = catalog.Integrations.Single(item => item.Id == SimulationConfigSeed.ProviderId);
await Assert.That(seeded.Methods).Contains(method => method is IntegrationKeyMethod);
await scenario(client, client.Integrations.GetIntegrationClient(SimulationConfigSeed.ProviderId), cleanup);
}
catch (Exception exception)
{
failure = exception;
}

await cleanup.CompleteAsync(failure);
}

private static async Task<IReadOnlyList<string>> ListOwnedIdsAsync(OpenCodeClient client, string label, CancellationToken token)
{
var response = await client.Credentials.ListCredentialsAsync(cancellationToken: token);
return [.. response.Credentials
.Where(entry => entry.Label == label && entry.IntegrationId == SimulationConfigSeed.ProviderId)
.Select(entry => entry.Id)];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using OpenCode.Sdk.Models;
using OpenCode.Sdk.Tests.Harness;

namespace OpenCode.Sdk.Tests.Integrations;

[ClassDataSource<SimulatedDriveServerFixture>(Shared = SharedType.PerTestSession)]
[NotInParallel(ParallelConstraintKeys.ServerProcess)]
public sealed class IntegrationsClientLiveTests(SimulatedDriveServerFixture server)
{
[Test]
[Timeout(60_000)]
public async Task ListIntegrationsAsync_Should_Include_The_Seeded_Provider_When_Plugins_Are_Activated(CancellationToken cancellationToken)
{
using var workspace = server.CreateWorkspace();
using var client = server.CreateClient(new LocationSelector { Directory = workspace.Path });

var response = await client.Integrations.ListIntegrationsAsync(cancellationToken: cancellationToken);

var seeded = response.Integrations.Where(item => item.Id == SimulationConfigSeed.ProviderId).ToList();
await Assert.That(seeded.Select(item => item.Id)).Contains(SimulationConfigSeed.ProviderId);
await Assert.That(seeded.Single().Name).IsEqualTo("Simulated");
await Assert.That(seeded.Single().Methods).Contains(method => method is IntegrationKeyMethod);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using OpenCode.Sdk.Models;
using OpenCode.Sdk.Tests.Harness;
using OpenCode.Sdk.Tests.Harness.Ownership;
using OpenCode.Sdk.Tests.Support;

namespace OpenCode.Sdk.Tests.McpServers;

[ClassDataSource<SimulatedDriveServerFixture>(Shared = SharedType.PerTestSession)]
[NotInParallel(ParallelConstraintKeys.ServerProcess)]
public sealed class McpServersClientLiveTests(SimulatedDriveServerFixture server)
{
[Test]
[Timeout(180_000)]
public async Task ListMcpServersAsync_Should_Report_The_Disabled_Entry_When_It_Was_Added_At_Runtime(CancellationToken cancellationToken)
{
using var workspace = server.CreateWorkspace();
using var client = server.CreateClient(new LocationSelector { Directory = workspace.Path });
var name = "sdk-catalog-" + Guid.NewGuid().ToString("N");
var cleanup = new OwnedCleanup(TimeSpan.FromSeconds(15));
cleanup.Own("owned catalog MCP removal", async token => await DisabledMcpServer.RemoveOwnedAsync(client, name, token));
var reader = new OwnedEventReader(TimeSpan.FromSeconds(120), TimeSpan.FromSeconds(15), cancellationToken);
cleanup.Own("catalog event reader", TimeSpan.FromSeconds(75), _ => reader.CompleteAsync(null));
var probe = new SessionEventProbe(reader);
Exception? failure = null;
try
{
probe.Start(client.Events.SubscribeAsync(cancellationToken: reader.Token));
await probe.WaitForConnectedAsync(cancellationToken);
await DisabledMcpServer.AddAsync(client, name, cancellationToken);

// A finite read kills an empty-list mutant before any event wait can stall.
var response = await client.McpServers.ListMcpServersAsync(cancellationToken: cancellationToken);
var owned = response.McpServers.Where(entry => entry.Name == name).ToList();
await Assert.That(owned.Select(entry => entry.Name)).Contains(name);
using var barrier = SessionEventProbe.Barrier(cancellationToken);
var changed = await probe.WaitForAsync<McpStatusChanged>(entry => entry.Data.Server == name,
"owned catalog MCP registration", barrier.Token);
await Assert.That(changed.Data.Server).IsEqualTo(name);

var reconciled = await client.McpServers.ListMcpServersAsync(cancellationToken: cancellationToken);
await Assert.That(reconciled.McpServers.Single(entry => entry.Name == name).Status).IsTypeOf<McpStatusDisabled>();
}
catch (Exception exception)
{
failure = exception;
}

await cleanup.CompleteAsync(failure);
}
}
Loading
Loading