mirror of
https://github.com/coder/coder.git
synced 2026-10-16 14:48:56 +08:00
- Experiment, behind `chat-projects` (off by default). First of three stacked PRs: #29301 (memory), #29583 (UI). This PR is API only; the screenshots below are from the UI in #29583. - CRUD under `/api/experimental/organizations/{organization}/chats/projects/{project}`. `GET /api/experimental/chats/projects` lists the caller's projects across all organizations. - `project_id` on chat create and as a chat list filter. A chat joins a project only at creation; there is no move. - Projects are identified by ID. Names are display labels (64 characters, description 1024) with no uniqueness constraint. An optional `icon` URL (usually an emoji image under `/emojis`) is capped at 256 characters and audited. - A user can own at most 100 projects across all organizations; creating another answers 409. Creates count and insert under a per-owner advisory lock so concurrent requests cannot overshoot. - Projects are private to their `owner_id`. Members may only create their own, and a chat can only be bound to a project its owner owns. An unreadable or foreign project on chat create answers 404 so IDs cannot be probed; a project from another organization answers 400. - Permissions ride on the `agents-access` role next to chats (after #30004), so whoever can use Coder Agents can use projects. Owners and org admins reach every project. ### Examples Create a project in an organization: ```http POST /api/experimental/organizations/{organization}/chats/projects {"name": "Platform", "description": "Infra and deploy work", "icon": "/emojis/1f680.png"} ``` ```json { "id": "7c0e4a3e-5b1f-4f7d-9a51-2f3c8f6d2b10", "organization_id": "bb640d07-ca8a-4869-b6bc-ae61ebb2fda1", "owner_id": "0ed9befc-4911-4ccf-a8e2-559bf72daa94", "name": "Platform", "description": "Infra and deploy work", "icon": "/emojis/1f680.png", "created_at": "2026-09-30T15:04:05Z", "updated_at": "2026-09-30T15:04:05Z" } ``` List the caller's projects across organizations with `GET /api/experimental/chats/projects` (an array of the object above). Update with `PATCH .../chats/projects/{project}` and any of `name`, `description`, `icon`; delete with `DELETE`, which detaches its chats. Create a chat in the project, and list only its chats: ```http POST /api/v2/chats {"organization_id": "bb640d07-…", "project_id": "7c0e4a3e-…", "content": [{"type": "text", "text": "Plan the deploy"}]} GET /api/v2/chats?project_id=7c0e4a3e-… ``` At the limit: ```json 409 {"message": "You can have at most 100 chat projects. Delete a project to create another."} ``` <details> <summary>Screenshots (UI from #29583)</summary>      </details> --- Generated with Coder Agents on behalf of @f0ssel. --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
73 lines
2.6 KiB
Go
73 lines
2.6 KiB
Go
package coderd_test
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/dbgen"
|
|
"github.com/coder/coder/v2/testutil"
|
|
)
|
|
|
|
func TestChatRouteMounts(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
client, db := coderdtest.NewWithDatabase(t, nil)
|
|
firstUser := coderdtest.CreateFirstUser(t, client)
|
|
model := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
|
|
OrganizationID: firstUser.OrganizationID,
|
|
})
|
|
chat := dbgen.Chat(t, db, database.Chat{
|
|
OrganizationID: firstUser.OrganizationID,
|
|
OwnerID: firstUser.UserID,
|
|
LastModelConfigID: model.ID,
|
|
})
|
|
|
|
for _, route := range []string{
|
|
"/api/v2/chats",
|
|
"/api/v2/chats/config/system-prompt",
|
|
fmt.Sprintf("/api/experimental/chats/%s/debug/runs", chat.ID),
|
|
"/api/experimental/chats/projects",
|
|
} {
|
|
res, err := client.Request(ctx, http.MethodGet, route, nil)
|
|
require.NoError(t, err)
|
|
_ = res.Body.Close()
|
|
require.Equal(t, http.StatusOK, res.StatusCode, route)
|
|
}
|
|
|
|
for _, route := range []struct {
|
|
method string
|
|
path string
|
|
}{
|
|
// Promoted routes no longer answer on /api/experimental.
|
|
{http.MethodGet, "/api/experimental/chats"},
|
|
{http.MethodGet, "/api/experimental/chats/config/system-prompt"},
|
|
{http.MethodGet, fmt.Sprintf("/api/experimental/organizations/%s/chats/models", firstUser.OrganizationID)},
|
|
{http.MethodGet, fmt.Sprintf("/api/experimental/organizations/%s/mcp-servers", firstUser.OrganizationID)},
|
|
{http.MethodDelete, fmt.Sprintf("/api/experimental/mcp/servers/%s/oauth2/disconnect", uuid.New())},
|
|
{http.MethodGet, "/api/experimental/users/me/ai-provider-keys"},
|
|
// Never-promoted routes are absent from /api/v2.
|
|
{http.MethodGet, "/api/v2/chats/model-configs"},
|
|
{http.MethodPost, "/api/v2/chats/model-configs"},
|
|
{http.MethodGet, "/api/v2/chats/config/computer-use-provider"},
|
|
{http.MethodGet, "/api/v2/chats/config/advisor"},
|
|
{http.MethodGet, "/api/v2/chats/projects"},
|
|
{http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/chats/projects", firstUser.OrganizationID)},
|
|
{http.MethodGet, fmt.Sprintf("/api/v2/chats/%s/debug/runs", chat.ID)},
|
|
{http.MethodGet, fmt.Sprintf("/api/v2/chats/%s/stream/desktop", chat.ID)},
|
|
{http.MethodGet, "/api/v2/mcp/servers/not-a-uuid/oauth2/callback"},
|
|
{http.MethodPost, "/api/v2/mcp/http/server"},
|
|
} {
|
|
res, err := client.Request(ctx, route.method, route.path, nil)
|
|
require.NoError(t, err)
|
|
_ = res.Body.Close()
|
|
require.Equal(t, http.StatusNotFound, res.StatusCode, "%s %s", route.method, route.path)
|
|
}
|
|
}
|