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
4 changes: 2 additions & 2 deletions api/ai_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func registerAISessionRoutes(app *fiber.App, group *huma.Group) {
}, func(op *huma.Operation) {
op.OperationID = "ai-session-list-by-project"
op.Summary = "获取项目的AI助手会话列表"
op.Description = "返回指定项目目录下的 Claude Code 和 Codex 会话信息"
op.Description = "返回指定项目目录下的 Claude Code、CodexPi 会话信息"
op.Tags = []string{aiSessionTag}
})

Expand All @@ -96,7 +96,7 @@ func registerAISessionRoutes(app *fiber.App, group *huma.Group) {
}, func(op *huma.Operation) {
op.OperationID = "ai-session-list-by-path"
op.Summary = "通过路径获取AI助手会话列表"
op.Description = "根据目录路径返回 Claude Code 和 Codex 会话信息"
op.Description = "根据目录路径返回 Claude Code、CodexPi 会话信息"
op.Tags = []string{aiSessionTag}
})

Expand Down
3 changes: 2 additions & 1 deletion api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,11 @@ func Init(ctx context.Context, cfg *utils.AppConfig, assets embed.FS, info *AppI
theLogger.Error("failed to initialize web session manager", zap.Error(err))
return err
}
defer webSessionManager.StopAllPiRuntimes()

registerAuthRoutes(app, cfg)
registerHealthRoutes(app, humaAPI)
registerProjectRoutes(v1)
registerProjectRoutes(v1, webSessionManager)
registerWorktreeRoutes(v1, cfg)
registerBranchRoutes(v1)
registerTaskRoutes(v1)
Expand Down
6 changes: 5 additions & 1 deletion api/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"code-kanban/api/h"
"code-kanban/model"
"code-kanban/service/websession"
)

const projectTag = "project-项目管理"
Expand Down Expand Up @@ -43,7 +44,7 @@ type projectAccessInput struct {
ID string `path:"id"`
}

func registerProjectRoutes(group *huma.Group) {
func registerProjectRoutes(group *huma.Group, webSessionManager *websession.Manager) {
service := model.NewProjectService()

huma.Post(group, "/projects/create", func(ctx context.Context, input *createProjectInput) (*h.ItemResponse[model.Project], error) {
Expand Down Expand Up @@ -227,6 +228,9 @@ func registerProjectRoutes(group *huma.Group) {
huma.Post(group, "/projects/{id}/delete", func(ctx context.Context, input *struct {
ID string `path:"id"`
}) (*h.MessageResponse, error) {
if webSessionManager != nil {
webSessionManager.StopProjectPiRuntimes(input.ID)
}
if err := service.DeleteProject(ctx, input.ID); err != nil {
if errors.Is(err, model.ErrDBNotInitialized) {
return nil, huma.Error503ServiceUnavailable("database is not initialized")
Expand Down
97 changes: 97 additions & 0 deletions api/project_agent_trust_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package api

import (
"io"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"

"code-kanban/api/h"
"code-kanban/model"
"code-kanban/model/tables"
"code-kanban/service"
"code-kanban/service/websession"
"code-kanban/utils"

"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
)

func TestProjectPiTrustRoutes(t *testing.T) {
model.DBClose()
if err := model.InitWithDSN(filepath.Join(t.TempDir(), "agent-trust.db"), 0, true); err != nil {
t.Fatalf("InitWithDSN: %v", err)
}
t.Cleanup(model.DBClose)

project := &tables.ProjectTable{Name: "Trust API", Path: t.TempDir()}
project.Init()
if err := model.GetDB().Create(project).Error; err != nil {
t.Fatalf("create project: %v", err)
}
manager, err := websession.NewManager(websession.Config{DataDir: t.TempDir()}, zap.NewNop())
if err != nil {
t.Fatalf("NewManager: %v", err)
}

app := fiber.New(fiber.Config{Immutable: true})
_, group := h.NewAPI(app, &utils.AppConfig{})
registerWebSessionRoutes(app, group, manager, zap.NewNop())
path := "/api/v1/projects/" + project.ID + "/agent-trust/pi"

assertProjectPiTrustResponse(t, app, http.MethodGet, path, "", http.StatusOK, `"trusted":false`)
assertProjectPiTrustResponse(
t,
app,
http.MethodPost,
path,
`{"trusted":true,"path":"D:/forged"}`,
http.StatusOK,
`"trusted":true`,
)
var trust tables.ProjectAgentTrustTable
if err := model.GetDB().Where("project_id = ? AND agent = ?", project.ID, "pi").First(&trust).Error; err != nil {
t.Fatalf("load trust record: %v", err)
}
wantTrustedPath, err := service.CanonicalAgentTrustPath(project.Path)
if err != nil {
t.Fatalf("canonical project path: %v", err)
}
if trust.TrustedPath != wantTrustedPath {
t.Fatalf("trusted path = %q, want server project path %q", trust.TrustedPath, wantTrustedPath)
}
assertProjectPiTrustResponse(t, app, http.MethodDelete, path, "", http.StatusOK, `"trusted":false`)
}

func assertProjectPiTrustResponse(
t *testing.T,
app *fiber.App,
method string,
path string,
body string,
wantStatus int,
wantFragment string,
) {
t.Helper()
request := httptest.NewRequest(method, path, strings.NewReader(body))
if body != "" {
request.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON)
}
response, err := app.Test(request)
if err != nil {
t.Fatalf("%s %s: %v", method, path, err)
}
defer response.Body.Close()
payload, err := io.ReadAll(response.Body)
if err != nil {
t.Fatalf("read response: %v", err)
}
if response.StatusCode != wantStatus {
t.Fatalf("%s %s status = %d, want %d: %s", method, path, response.StatusCode, wantStatus, payload)
}
if !strings.Contains(string(payload), wantFragment) {
t.Fatalf("%s %s response %s does not contain %s", method, path, payload, wantFragment)
}
}
Loading
Loading