mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
This PR makes the agent-pushed pinned snapshot (`chat_context_resources`) the sole source of workspace context for chats, completing the "Release 5" cleanup. It removes legacy mechanisms now superseded by the snapshot that agents push over dRPC (`PushContextState`) and refresh via `chat-context/refresh`. Removed: - **Live-read at turn time.** MCP tool discovery, skill live-body reads, and the instruction/skill history fallback that dialed the workspace on every turn. - **Context injected as message history.** The `persist_workspace_context` generation action and its decision-loop guard. - **The legacy write path.** `POST`/`DELETE /api/v2/workspaceagents/me/experimental/chat-context`, the agentsdk `AddChatContext`/`ClearChatContext` methods, and the CLI one-shot writer. - **The `chats.last_injected_context` column** and all of its plumbing (migration `000529`, queries, `db2sdk`, `dbauthz`, audit table, and the frontend `ContextUsageIndicator` fallback). Subagent context inheritance no longer copies parent context messages; children now hydrate the parent's pinned `chat_context_resources` on create, which yields an identical pin for the same workspace and agent. What stays (still served by the live agent connection, not the snapshot): `read_skill_file` supporting-file reads, `read_skill` supporting-file listing, and MCP tool execution. > [!NOTE] > Migration `000529` drops `chats.last_injected_context` and recreates the `chats_expanded` view without it. The down migration restores both. <details> <summary>Decision log (D1-D5)</summary> - **D1 (subagent inheritance):** Re-point inheritance from the legacy message copy to a pinned hydrate. Children call `hydrateChatContextOnCreate` instead of copying parent context messages. - **D2 (`persist_workspace_context`):** Remove the generation action entirely along with the decision-loop guard it existed to satisfy, since context is never injected into history anymore. - **D3 (legacy HTTP + CLI):** Remove the experimental `chat-context` POST/DELETE endpoints, the agentsdk methods, and the CLI one-shot. The dRPC push + `chat-context/refresh` replace them. - **D4 (frontend fallback):** Remove the `last_injected_context` fallback in `ContextUsageIndicator`; pinned `resources` are the sole source. - **D5 (sequencing):** Ship as a single PR rather than a stacked pair. </details> --- Coder Agents generated on behalf of @kylecarbs.
128 lines
3.8 KiB
Go
128 lines
3.8 KiB
Go
package coderd
|
|
|
|
import (
|
|
"database/sql"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
|
"github.com/coder/coder/v2/coderd/database/dbfake"
|
|
"github.com/coder/coder/v2/coderd/database/dbgen"
|
|
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
|
"github.com/coder/coder/v2/coderd/rbac/policy"
|
|
"github.com/coder/coder/v2/testutil"
|
|
)
|
|
|
|
func TestActiveAgentChatsIncludeInheritedACLs(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitMedium))
|
|
db, _, sqlDB := dbtestutil.NewDBWithSQLDB(t)
|
|
|
|
org, err := db.GetDefaultOrganization(ctx)
|
|
require.NoError(t, err)
|
|
|
|
owner := dbgen.User(t, db, database.User{})
|
|
workspace := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
|
OrganizationID: org.ID,
|
|
OwnerID: owner.ID,
|
|
}).WithAgent().Do()
|
|
modelConfig := insertAgentChatTestModelConfig(t, db, owner.ID)
|
|
|
|
root, err := db.InsertChat(ctx, database.InsertChatParams{
|
|
OrganizationID: org.ID,
|
|
Status: database.ChatStatusWaiting,
|
|
ClientType: database.ChatClientTypeUi,
|
|
OwnerID: owner.ID,
|
|
LastModelConfigID: modelConfig.ID,
|
|
Title: "root-active-chat",
|
|
AgentID: uuid.NullUUID{UUID: workspace.Agents[0].ID, Valid: true},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
child, err := db.InsertChat(ctx, database.InsertChatParams{
|
|
OrganizationID: org.ID,
|
|
Status: database.ChatStatusRunning,
|
|
ClientType: database.ChatClientTypeUi,
|
|
OwnerID: owner.ID,
|
|
LastModelConfigID: modelConfig.ID,
|
|
Title: "child-active-chat",
|
|
AgentID: uuid.NullUUID{UUID: workspace.Agents[0].ID, Valid: true},
|
|
ParentChatID: uuid.NullUUID{UUID: root.ID, Valid: true},
|
|
RootChatID: uuid.NullUUID{UUID: root.ID, Valid: true},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
rootUserACL := database.ChatACL{
|
|
owner.ID.String(): {Permissions: []policy.Action{policy.ActionRead, policy.ActionSSH}},
|
|
}
|
|
rootGroupACL := database.ChatACL{
|
|
org.ID.String(): {Permissions: []policy.Action{policy.ActionRead}},
|
|
}
|
|
|
|
userACLValue, err := rootUserACL.Value()
|
|
require.NoError(t, err)
|
|
groupACLValue, err := rootGroupACL.Value()
|
|
require.NoError(t, err)
|
|
|
|
_, err = sqlDB.ExecContext(
|
|
ctx,
|
|
`UPDATE chats SET user_acl = $1::jsonb, group_acl = $2::jsonb WHERE id = $3`,
|
|
userACLValue,
|
|
groupACLValue,
|
|
root.ID,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
activeChats, err := db.GetActiveChatsByAgentID(ctx, workspace.Agents[0].ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, activeChats, 2)
|
|
|
|
activeByID := make(map[uuid.UUID]database.Chat, len(activeChats))
|
|
for _, chat := range activeChats {
|
|
activeByID[chat.ID] = chat
|
|
}
|
|
|
|
fetchedRoot, ok := activeByID[root.ID]
|
|
require.True(t, ok)
|
|
require.Equal(t, rootUserACL, fetchedRoot.UserACL)
|
|
require.Equal(t, rootGroupACL, fetchedRoot.GroupACL)
|
|
|
|
fetchedChild, ok := activeByID[child.ID]
|
|
require.True(t, ok)
|
|
require.True(t, fetchedChild.ParentChatID.Valid)
|
|
require.Equal(t, rootUserACL, fetchedChild.UserACL)
|
|
require.Equal(t, rootGroupACL, fetchedChild.GroupACL)
|
|
}
|
|
|
|
func insertAgentChatTestModelConfig(
|
|
t testing.TB,
|
|
db database.Store,
|
|
userID uuid.UUID,
|
|
) database.ChatModelConfig {
|
|
t.Helper()
|
|
|
|
createdBy := uuid.NullUUID{UUID: userID, Valid: true}
|
|
|
|
provider := dbgen.AIProvider(t, db, database.AIProvider{
|
|
Type: database.AIProviderTypeOpenai,
|
|
Name: "test-openai",
|
|
DisplayName: sql.NullString{String: "OpenAI", Valid: true},
|
|
})
|
|
dbgen.AIProviderKey(t, db, database.AIProviderKey{
|
|
ProviderID: provider.ID,
|
|
APIKey: "test-api-key",
|
|
})
|
|
|
|
return dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
|
|
Provider: "openai",
|
|
AIProviderID: uuid.NullUUID{UUID: provider.ID, Valid: true},
|
|
CreatedBy: createdBy,
|
|
UpdatedBy: createdBy,
|
|
IsDefault: true,
|
|
})
|
|
}
|