From 7963bfbf4cd4c2382ca5e01a93d1a8f549117900 Mon Sep 17 00:00:00 2001 From: Diana Scharf Date: Sat, 25 Jul 2026 18:25:16 +0200 Subject: [PATCH] Add models(), inviteTeamMember(), and createWorkspaceFromLinearIssue() models() wraps the new GET /v1/models endpoint and returns per-server ServerModels resources with nested AvailableAgent resources. The other two methods close coverage gaps against the public v1 API. --- src/Actions/ManagesModels.php | 27 ++++++ src/Actions/ManagesTeam.php | 22 +++++ src/Actions/ManagesWorkspaces.php | 9 ++ src/Polyscope.php | 2 + src/Resources/AvailableAgent.php | 15 ++++ src/Resources/ServerModels.php | 31 +++++++ tests/PolyscopeSDKTest.php | 133 ++++++++++++++++++++++++++++++ 7 files changed, 239 insertions(+) create mode 100644 src/Actions/ManagesModels.php create mode 100644 src/Actions/ManagesTeam.php create mode 100644 src/Resources/AvailableAgent.php create mode 100644 src/Resources/ServerModels.php diff --git a/src/Actions/ManagesModels.php b/src/Actions/ManagesModels.php new file mode 100644 index 0000000..48b2201 --- /dev/null +++ b/src/Actions/ManagesModels.php @@ -0,0 +1,27 @@ + + */ + public function models(?string $serverId = null): array + { + $query = array_filter([ + 'server_id' => $serverId, + ], static fn (mixed $value): bool => $value !== null && $value !== ''); + + $response = $this->get('v1/models', $query); + + return $this->transformCollection( + is_array($response) ? ($response['data'] ?? []) : [], + ServerModels::class, + ); + } +} diff --git a/src/Actions/ManagesTeam.php b/src/Actions/ManagesTeam.php new file mode 100644 index 0000000..cab0b1c --- /dev/null +++ b/src/Actions/ManagesTeam.php @@ -0,0 +1,22 @@ +post('v1/team/invites', [ + 'email' => $email, + ]); + + return new GenericResource( + is_array($response) ? ($response['data'] ?? $response) : [], + $this, + ); + } +} diff --git a/src/Actions/ManagesWorkspaces.php b/src/Actions/ManagesWorkspaces.php index e35add1..c9a168f 100644 --- a/src/Actions/ManagesWorkspaces.php +++ b/src/Actions/ManagesWorkspaces.php @@ -51,6 +51,15 @@ public function createWorkspaceFromIssue(string $repositoryId, string $issueUrl, ]); } + public function createWorkspaceFromLinearIssue(string $repositoryId, string $linearIssueUrl, ?string $serverId = null): Workspace + { + return $this->createWorkspace([ + 'repository_id' => $repositoryId, + 'linear_issue_url' => $linearIssueUrl, + 'server_id' => $serverId, + ]); + } + public function createWorkspaceFromPullRequest(string $repositoryId, string $pullRequestUrl, ?string $serverId = null): Workspace { return $this->createWorkspace([ diff --git a/src/Polyscope.php b/src/Polyscope.php index b74ae71..3c21bb5 100644 --- a/src/Polyscope.php +++ b/src/Polyscope.php @@ -8,8 +8,10 @@ class Polyscope { + use Actions\ManagesModels; use Actions\ManagesRepositories; use Actions\ManagesServers; + use Actions\ManagesTeam; use Actions\ManagesWorkspaceActions; use Actions\ManagesWorkspaceMessages; use Actions\ManagesWorkspaces; diff --git a/src/Resources/AvailableAgent.php b/src/Resources/AvailableAgent.php new file mode 100644 index 0000000..f88b235 --- /dev/null +++ b/src/Resources/AvailableAgent.php @@ -0,0 +1,15 @@ + */ + public array $models = []; +} diff --git a/src/Resources/ServerModels.php b/src/Resources/ServerModels.php new file mode 100644 index 0000000..0cc8aa3 --- /dev/null +++ b/src/Resources/ServerModels.php @@ -0,0 +1,31 @@ + */ + public array $agents = []; + + /** @var array */ + public array $models = []; + + /** @var array> */ + public array $modelCapabilities = []; + + protected function fill(): void + { + parent::fill(); + + $agents = $this->attributes['agents'] ?? []; + + $this->agents = $this->transformCollection( + is_array($agents) ? $agents : [], + AvailableAgent::class, + ); + } +} diff --git a/tests/PolyscopeSDKTest.php b/tests/PolyscopeSDKTest.php index c1efd2f..1d7744a 100644 --- a/tests/PolyscopeSDKTest.php +++ b/tests/PolyscopeSDKTest.php @@ -11,8 +11,10 @@ use Polyscope\Laravel\Exceptions\ServerOfflineException; use Polyscope\Laravel\Exceptions\ValidationException; use Polyscope\Laravel\Polyscope; +use Polyscope\Laravel\Resources\AvailableAgent; use Polyscope\Laravel\Resources\DiffSnapshot; use Polyscope\Laravel\Resources\GenericResource; +use Polyscope\Laravel\Resources\ServerModels; use Polyscope\Laravel\Resources\Message; use Polyscope\Laravel\Resources\Server; use Polyscope\Laravel\Resources\Workspace; @@ -151,6 +153,137 @@ public function test_repositories_support_server_filter(): void $this->assertSame('srv-1', $repositories[0]->serverId); } + public function test_models_are_transformed_to_server_models_resources(): void + { + $sdk = new Polyscope('token', $http = Mockery::mock(Client::class)); + + $http->shouldReceive('request') + ->once() + ->with('GET', 'v1/models', [ + 'timeout' => 30, + ]) + ->andReturn(new Response(200, [], json_encode([ + 'data' => [[ + 'server_id' => 'srv-1', + 'agents' => [ + [ + 'id' => 'claude', + 'name' => 'Claude', + 'models' => ['claude-opus-5', 'claude-sonnet-5'], + ], + [ + 'id' => 'codex', + 'name' => 'Codex', + 'models' => ['gpt-5.3-codex'], + ], + ], + 'models' => ['claude-opus-5', 'claude-sonnet-5', 'gpt-5.3-codex'], + 'model_capabilities' => [ + [ + 'value' => 'claude-opus-5', + 'displayName' => 'Opus 5', + 'supportsFastMode' => true, + ], + ], + ]], + ], JSON_THROW_ON_ERROR))); + + $serverModels = $sdk->models(); + + $this->assertCount(1, $serverModels); + $this->assertInstanceOf(ServerModels::class, $serverModels[0]); + $this->assertSame('srv-1', $serverModels[0]->serverId); + $this->assertCount(2, $serverModels[0]->agents); + $this->assertInstanceOf(AvailableAgent::class, $serverModels[0]->agents[0]); + $this->assertSame('claude', $serverModels[0]->agents[0]->id); + $this->assertSame('Claude', $serverModels[0]->agents[0]->name); + $this->assertSame(['claude-opus-5', 'claude-sonnet-5'], $serverModels[0]->agents[0]->models); + $this->assertSame(['claude-opus-5', 'claude-sonnet-5', 'gpt-5.3-codex'], $serverModels[0]->models); + $this->assertSame('claude-opus-5', $serverModels[0]->modelCapabilities[0]['value']); + $this->assertTrue($serverModels[0]->modelCapabilities[0]['supportsFastMode']); + } + + public function test_models_support_server_filter(): void + { + $sdk = new Polyscope('token', $http = Mockery::mock(Client::class)); + + $http->shouldReceive('request') + ->once() + ->with('GET', 'v1/models', Mockery::on(function (array $options): bool { + return ($options['timeout'] ?? null) === 30 + && ($options['query']['server_id'] ?? null) === 'srv-1'; + })) + ->andReturn(new Response(200, [], json_encode([ + 'data' => [[ + 'server_id' => 'srv-1', + 'agents' => [], + 'models' => [], + 'model_capabilities' => [], + ]], + ], JSON_THROW_ON_ERROR))); + + $serverModels = $sdk->models('srv-1'); + + $this->assertCount(1, $serverModels); + $this->assertSame('srv-1', $serverModels[0]->serverId); + $this->assertSame([], $serverModels[0]->agents); + $this->assertSame([], $serverModels[0]->modelCapabilities); + } + + public function test_creating_workspace_from_linear_issue_sends_linear_issue_url(): void + { + $sdk = new Polyscope('token', $http = Mockery::mock(Client::class)); + + $http->shouldReceive('request') + ->once() + ->with('POST', 'v1/workspaces', [ + 'timeout' => 30, + 'json' => [ + 'repository_id' => 'repo-1', + 'linear_issue_url' => 'https://linear.app/team/issue/ABC-1', + ], + ]) + ->andReturn(new Response(201, [], json_encode([ + 'data' => [ + 'id' => 'wt-1', + 'repo_id' => 'repo-1', + 'branch' => 'abc-1', + 'status' => 'active', + ], + ], JSON_THROW_ON_ERROR))); + + $workspace = $sdk->createWorkspaceFromLinearIssue('repo-1', 'https://linear.app/team/issue/ABC-1'); + + $this->assertInstanceOf(Workspace::class, $workspace); + $this->assertSame('wt-1', $workspace->id); + } + + public function test_inviting_team_member_returns_generic_resource(): void + { + $sdk = new Polyscope('token', $http = Mockery::mock(Client::class)); + + $http->shouldReceive('request') + ->once() + ->with('POST', 'v1/team/invites', [ + 'timeout' => 30, + 'json' => [ + 'email' => 'teammate@example.com', + ], + ]) + ->andReturn(new Response(201, [], json_encode([ + 'data' => [ + 'email' => 'teammate@example.com', + 'invited' => true, + ], + ], JSON_THROW_ON_ERROR))); + + $result = $sdk->inviteTeamMember('teammate@example.com'); + + $this->assertInstanceOf(GenericResource::class, $result); + $this->assertSame('teammate@example.com', $result->email); + $this->assertTrue($result->invited); + } + public function test_creating_workspace_returns_workspace_resource(): void { $sdk = new Polyscope('token', $http = Mockery::mock(Client::class));