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 docs/engineering/quality-gates.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ The lint job runs `dotnet run --file tools/opencode-tool.cs -- verify-clones`: t
`tools/jscpd/`, installed with the machine's Bun from its own lockfile, at a 50-token minimum over four
scopes. Hand-written `src/`, `tools/` and `tests/` are gated against the committed baselines in
`tools/jscpd/`: a clone a baseline does not hold fails the job, `tools/` also fails above 3%
duplicated lines, and `tests/` above 5.5%. The tests ceiling is the measurement plus 0.25 points,
duplicated lines, and `tests/` above 5%. The tests ceiling is the measurement plus 0.25 points,
rounded up to the next 0.5%, and it only ever falls: a change that removes test clones lowers it
to that value when it is lower than the current ceiling. Generated output is
reported, not gated. The generator's manifest decides which `src/` files are generated, so the split
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
using OpenCode.Sdk.Tests.Forms;
using OpenCode.Sdk.Tests.Harness;
using OpenCode.Sdk.Tests.Integrations;
using OpenCode.Sdk.Tests.LanguageModels;
using OpenCode.Sdk.Tests.Permissions;
using OpenCode.Sdk.Tests.Projects;
using OpenCode.Sdk.Tests.Server;
using OpenCode.Sdk.Tests.Support;
using OpenCode.Sdk.Tests.Vcs;
using OpenCode.Sdk.Tests.Worktrees;
Expand All @@ -29,6 +33,11 @@ .. FormsDeclaredErrors.Rows(),
.. FileSystemDeclaredErrors.Rows(),
.. IntegrationsDeclaredErrors.Rows(),
.. VcsDeclaredErrors.Rows(),
.. LanguageModelsDeclaredErrors.Rows(),
.. OpenCodeClientDeclaredErrors.Rows(),
.. PermissionsDeclaredErrors.Rows(),
.. ProjectsDeclaredErrors.Rows(),
.. ServerDeclaredErrors.Rows(),
];

[Test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,31 +39,6 @@ public async Task GetDefaultAsync_Should_Return_Null_When_No_Model_Is_Selected()
await Assert.That(response.Default).IsNull();
}

[Test]
public async Task GetDefaultAsync_Should_Throw_The_Declared_400_Error()
{
using var scenario = ContractScenario.Responding(HttpStatusCode.BadRequest, WireBodyData.InvalidRequestError);

var exception = await Assert
.That(async () => _ = await scenario.Client.LanguageModels.GetDefaultAsync())
.Throws<OpenCodeApiException>();

await Assert.That(exception!.Status).IsEqualTo(400);
await Assert.That(exception.Error).IsTypeOf<InvalidRequestError>();
}

[Test]
public async Task GetDefaultAsync_Should_Return_The_401_Error_On_The_NoThrow_Spine()
{
using var scenario = ContractScenario.Responding(HttpStatusCode.Unauthorized, WireBodyData.UnauthorizedError);

var response = await scenario.Client.LanguageModels.GetDefaultAsync(requestOptions: OpenCodeRequestOptions.NoThrow);

await Assert.That(response.IsError).IsTrue();
await Assert.That(response.Status).IsEqualTo(401);
await Assert.That(response.Error).IsTypeOf<UnauthorizedError>();
}

[Test]
public async Task ListModelsAsync_Should_Return_The_Typed_Models_With_Their_Location()
{
Expand Down Expand Up @@ -91,29 +66,4 @@ public async Task ListModelsAsync_Should_Return_An_Empty_List_With_Its_Location(
await Assert.That(response.Models.Count).IsEqualTo(0);
await Assert.That(response.Location.Directory).IsEqualTo(WireBodyData.ResolvedDirectory);
}

[Test]
public async Task ListModelsAsync_Should_Throw_The_Declared_400_Error()
{
using var scenario = ContractScenario.Responding(HttpStatusCode.BadRequest, WireBodyData.InvalidRequestError);

var exception = await Assert
.That(async () => _ = await scenario.Client.LanguageModels.ListModelsAsync())
.Throws<OpenCodeApiException>();

await Assert.That(exception!.Status).IsEqualTo(400);
await Assert.That(exception.Error).IsTypeOf<InvalidRequestError>();
}

[Test]
public async Task ListModelsAsync_Should_Return_The_401_Error_On_The_NoThrow_Spine()
{
using var scenario = ContractScenario.Responding(HttpStatusCode.Unauthorized, WireBodyData.UnauthorizedError);

var response = await scenario.Client.LanguageModels.ListModelsAsync(requestOptions: OpenCodeRequestOptions.NoThrow);

await Assert.That(response.IsError).IsTrue();
await Assert.That(response.Status).IsEqualTo(401);
await Assert.That(response.Error).IsTypeOf<UnauthorizedError>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System.Net;
using OpenCode.Sdk.Models;
using OpenCode.Sdk.Tests.Common.Support;
using OpenCode.Sdk.Tests.Support;

namespace OpenCode.Sdk.Tests.LanguageModels;

public static class LanguageModelsDeclaredErrors
{
private static readonly DeclaredErrorDoor GetDefaultDoor = new(
nameof(LanguageModelsClient.GetDefaultAsync),
"model.default",
"get",
"/api/model/default",
static async (client, options) => await client.LanguageModels.GetDefaultAsync(requestOptions: options));

private static readonly DeclaredErrorDoor ListModelsDoor = new(
nameof(LanguageModelsClient.ListModelsAsync),
"model.list",
"get",
"/api/model",
static async (client, options) => await client.LanguageModels.ListModelsAsync(requestOptions: options));

public static IEnumerable<Func<DeclaredErrorCase>> Rows() =>
[
static () => new(
"model.default:400:InvalidRequestError",
GetDefaultDoor,
HttpStatusCode.BadRequest,
WireBodyData.InvalidRequestError,
"InvalidRequestError",
typeof(InvalidRequestError),
"#/paths/~1api~1model~1default/get/responses/400/content/application~1json/schema"),
static () => new(
"model.default:401:UnauthorizedError",
GetDefaultDoor,
HttpStatusCode.Unauthorized,
WireBodyData.UnauthorizedError,
"UnauthorizedError",
typeof(UnauthorizedError),
"#/paths/~1api~1model~1default/get/responses/401/content/application~1json/schema"),
static () => new(
"model.list:400:InvalidRequestError",
ListModelsDoor,
HttpStatusCode.BadRequest,
WireBodyData.InvalidRequestError,
"InvalidRequestError",
typeof(InvalidRequestError),
"#/paths/~1api~1model/get/responses/400/content/application~1json/schema"),
static () => new(
"model.list:401:UnauthorizedError",
ListModelsDoor,
HttpStatusCode.Unauthorized,
WireBodyData.UnauthorizedError,
"UnauthorizedError",
typeof(UnauthorizedError),
"#/paths/~1api~1model/get/responses/401/content/application~1json/schema"),
];
}
25 changes: 0 additions & 25 deletions tests/OpenCode.Sdk.Tests/OpenCodeClientContractTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,31 +157,6 @@ await Assert.That(scenario.Requests.Single().RequestUri!.AbsoluteUri)
.IsEqualTo("http://localhost:4096/api/location?location[directory]=%2Frepo");
}

[Test]
public async Task GetLocationAsync_Should_Throw_The_Declared_400_Error()
{
using var scenario = ContractScenario.Responding(HttpStatusCode.BadRequest, WireBodyData.InvalidRequestError);

var exception = await Assert
.That(async () => _ = await scenario.Client.GetLocationAsync())
.Throws<OpenCodeApiException>();

await Assert.That(exception!.Status).IsEqualTo(400);
await Assert.That(exception.Error).IsTypeOf<InvalidRequestError>();
}

[Test]
public async Task GetLocationAsync_Should_Return_The_401_Error_On_The_NoThrow_Spine()
{
using var scenario = ContractScenario.Responding(HttpStatusCode.Unauthorized, WireBodyData.UnauthorizedError);

var response = await scenario.Client.GetLocationAsync(requestOptions: OpenCodeRequestOptions.NoThrow);

await Assert.That(response.IsError).IsTrue();
await Assert.That(response.Status).IsEqualTo(401);
await Assert.That(response.Error).IsTypeOf<UnauthorizedError>();
}

[Test]
public async Task GetMessageAsync_Should_Unwrap_The_Data_Envelope()
{
Expand Down
36 changes: 36 additions & 0 deletions tests/OpenCode.Sdk.Tests/OpenCodeClientDeclaredErrors.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.Net;
using OpenCode.Sdk.Models;
using OpenCode.Sdk.Tests.Common.Support;
using OpenCode.Sdk.Tests.Support;

namespace OpenCode.Sdk.Tests;

public static class OpenCodeClientDeclaredErrors
{
private static readonly DeclaredErrorDoor GetLocationDoor = new(
nameof(OpenCodeClient.GetLocationAsync),
"location.get",
"get",
"/api/location",
static async (client, options) => await client.GetLocationAsync(requestOptions: options));

public static IEnumerable<Func<DeclaredErrorCase>> Rows() =>
[
static () => new(
"location.get:400:InvalidRequestError",
GetLocationDoor,
HttpStatusCode.BadRequest,
WireBodyData.InvalidRequestError,
"InvalidRequestError",
typeof(InvalidRequestError),
"#/paths/~1api~1location/get/responses/400/content/application~1json/schema"),
static () => new(
"location.get:401:UnauthorizedError",
GetLocationDoor,
HttpStatusCode.Unauthorized,
WireBodyData.UnauthorizedError,
"UnauthorizedError",
typeof(UnauthorizedError),
"#/paths/~1api~1location/get/responses/401/content/application~1json/schema"),
];
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Net;
using OpenCode.Sdk.Models;
using OpenCode.Sdk.Tests.Harness;
using OpenCode.Sdk.Tests.Support;

Expand Down Expand Up @@ -53,31 +52,6 @@ await Assert.That(scenario.Requests.Single().RequestUri)
.IsEqualTo(new Uri("http://localhost:4096/api/permission/saved?projectID=prj_9"));
}

[Test]
public async Task ListSavedAsync_Should_Throw_The_Declared_400_Error()
{
using var scenario = ContractScenario.Responding(HttpStatusCode.BadRequest, WireBodyData.InvalidRequestError);

var exception = await Assert
.That(async () => _ = await scenario.Client.Permissions.ListSavedAsync())
.Throws<OpenCodeApiException>();

await Assert.That(exception!.Status).IsEqualTo(400);
await Assert.That(exception.Error).IsTypeOf<InvalidRequestError>();
}

[Test]
public async Task ListSavedAsync_Should_Return_The_401_Error_On_The_NoThrow_Spine()
{
using var scenario = ContractScenario.Responding(HttpStatusCode.Unauthorized, WireBodyData.UnauthorizedError);

var response = await scenario.Client.Permissions.ListSavedAsync(requestOptions: OpenCodeRequestOptions.NoThrow);

await Assert.That(response.IsError).IsTrue();
await Assert.That(response.Status).IsEqualTo(401);
await Assert.That(response.Error).IsTypeOf<UnauthorizedError>();
}

[Test]
public async Task RemoveSavedAsync_Should_Take_The_Id_As_An_Argument_On_The_204()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.Net;
using OpenCode.Sdk.Models;
using OpenCode.Sdk.Tests.Common.Support;
using OpenCode.Sdk.Tests.Support;

namespace OpenCode.Sdk.Tests.Permissions;

public static class PermissionsDeclaredErrors
{
private static readonly DeclaredErrorDoor ListSavedDoor = new(
nameof(PermissionsClient.ListSavedAsync),
"permission.saved.list",
"get",
"/api/permission/saved",
static async (client, options) => await client.Permissions.ListSavedAsync(requestOptions: options));

public static IEnumerable<Func<DeclaredErrorCase>> Rows() =>
[
static () => new(
"permission.saved.list:400:InvalidRequestError",
ListSavedDoor,
HttpStatusCode.BadRequest,
WireBodyData.InvalidRequestError,
"InvalidRequestError",
typeof(InvalidRequestError),
"#/paths/~1api~1permission~1saved/get/responses/400/content/application~1json/schema"),
static () => new(
"permission.saved.list:401:UnauthorizedError",
ListSavedDoor,
HttpStatusCode.Unauthorized,
WireBodyData.UnauthorizedError,
"UnauthorizedError",
typeof(UnauthorizedError),
"#/paths/~1api~1permission~1saved/get/responses/401/content/application~1json/schema"),
];
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,31 +63,6 @@ public async Task ListProjectsAsync_Should_Return_An_Empty_List()
await Assert.That(response.Projects).IsEmpty();
}

[Test]
public async Task ListProjectsAsync_Should_Throw_The_Declared_400_Error()
{
using var scenario = ContractScenario.Responding(HttpStatusCode.BadRequest, WireBodyData.InvalidRequestError);

var exception = await Assert
.That(async () => _ = await scenario.Client.Projects.ListProjectsAsync())
.Throws<OpenCodeApiException>();

await Assert.That(exception!.Status).IsEqualTo(400);
await Assert.That(exception.Error).IsTypeOf<InvalidRequestError>();
}

[Test]
public async Task ListProjectsAsync_Should_Return_The_401_Error_On_The_NoThrow_Spine()
{
using var scenario = ContractScenario.Responding(HttpStatusCode.Unauthorized, WireBodyData.UnauthorizedError);

var response = await scenario.Client.Projects.ListProjectsAsync(requestOptions: OpenCodeRequestOptions.NoThrow);

await Assert.That(response.IsError).IsTrue();
await Assert.That(response.Status).IsEqualTo(401);
await Assert.That(response.Error).IsTypeOf<UnauthorizedError>();
}

[Test]
public async Task UpdateProjectAsync_Should_Send_The_Typed_Body_And_Return_The_Updated_Project()
{
Expand Down Expand Up @@ -118,29 +93,4 @@ public async Task UpdateProjectAsync_Should_Send_An_Empty_Body_When_Omitted()

await Assert.That(scenario.Requests.Single().Body).IsEqualTo("{}");
}

[Test]
public async Task UpdateProjectAsync_Should_Throw_The_Declared_404_Error()
{
using var scenario = ContractScenario.Responding(HttpStatusCode.NotFound, WireBodyData.ProjectNotFoundError);

var exception = await Assert
.That(async () => _ = await scenario.Client.Projects.UpdateProjectAsync("prj_9"))
.Throws<OpenCodeApiException>();

await Assert.That(exception!.Status).IsEqualTo(404);
await Assert.That(exception.Error).IsTypeOf<ProjectNotFoundError>();
}

[Test]
public async Task UpdateProjectAsync_Should_Return_The_401_Error_On_The_NoThrow_Spine()
{
using var scenario = ContractScenario.Responding(HttpStatusCode.Unauthorized, WireBodyData.UnauthorizedError);

var response = await scenario.Client.Projects.UpdateProjectAsync("prj_1", requestOptions: OpenCodeRequestOptions.NoThrow);

await Assert.That(response.IsError).IsTrue();
await Assert.That(response.Status).IsEqualTo(401);
await Assert.That(response.Error).IsTypeOf<UnauthorizedError>();
}
}
Loading
Loading