package mcp import ( "context" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/yan1h/agent-coding-workflow/internal/infra/errs" "github.com/yan1h/agent-coding-workflow/internal/run" "github.com/yan1h/agent-coding-workflow/internal/workspace" ) // ===== fakes ===== // fakeRunSvc implements RunService. type fakeRunSvc struct { profiles []run.RunProfileResp lastOp string logs []run.LogLineResp } func (f *fakeRunSvc) List(_ context.Context, _ workspace.Caller, wsID uuid.UUID) ([]run.RunProfileResp, error) { out := make([]run.RunProfileResp, 0, len(f.profiles)) for _, p := range f.profiles { if p.WorkspaceID == wsID.String() { out = append(out, p) } } return out, nil } func (f *fakeRunSvc) Create(_ context.Context, _ workspace.Caller, wsID uuid.UUID, req run.CreateRunProfileReq) (run.RunProfileResp, error) { p := run.RunProfileResp{ ID: uuid.NewString(), WorkspaceID: wsID.String(), Slug: req.Slug, Name: req.Name, Command: req.Command, Status: string(run.StatusStopped), } f.profiles = append(f.profiles, p) return p, nil } func (f *fakeRunSvc) Update(_ context.Context, _ workspace.Caller, profileID uuid.UUID, req run.UpdateRunProfileReq) (run.RunProfileResp, error) { for i := range f.profiles { if f.profiles[i].ID == profileID.String() { if req.Name != nil { f.profiles[i].Name = *req.Name } return f.profiles[i], nil } } return run.RunProfileResp{}, errs.New(errs.CodeNotFound, "profile") } func (f *fakeRunSvc) Delete(_ context.Context, _ workspace.Caller, profileID uuid.UUID) error { kept := f.profiles[:0] for _, p := range f.profiles { if p.ID != profileID.String() { kept = append(kept, p) } } f.profiles = kept return nil } func (f *fakeRunSvc) Start(_ context.Context, _ workspace.Caller, _ uuid.UUID) (run.RunStatusResp, error) { f.lastOp = "start" return run.RunStatusResp{Status: string(run.StatusRunning)}, nil } func (f *fakeRunSvc) Stop(_ context.Context, _ workspace.Caller, _ uuid.UUID) (run.RunStatusResp, error) { f.lastOp = "stop" return run.RunStatusResp{Status: string(run.StatusStopped)}, nil } func (f *fakeRunSvc) Restart(_ context.Context, _ workspace.Caller, _ uuid.UUID) (run.RunStatusResp, error) { f.lastOp = "restart" return run.RunStatusResp{Status: string(run.StatusRunning)}, nil } func (f *fakeRunSvc) Status(_ context.Context, _ workspace.Caller, _ uuid.UUID) (run.RunStatusResp, error) { f.lastOp = "status" return run.RunStatusResp{Status: string(run.StatusRunning)}, nil } func (f *fakeRunSvc) Logs(_ context.Context, _ workspace.Caller, _ uuid.UUID, _ int64, limit int) ([]run.LogLineResp, error) { if len(f.logs) > limit { return f.logs[:limit], nil } return f.logs, nil } func runTestDeps() (ServerDeps, *fakeRunSvc) { deps := testDeps() deps.Workspaces = &fakeWorkspaceSvc{items: []*workspace.Workspace{{ ID: testWSID, ProjectID: testPID, Slug: "ws-1", Name: "Workspace 1", DefaultBranch: "main", SyncStatus: workspace.SyncStatusIdle, CreatedAt: time.Now(), }}} runSvc := &fakeRunSvc{} deps.Runs = runSvc return deps, runSvc } // ===== tests ===== func TestRunProfileCRUD(t *testing.T) { deps, runSvc := runTestDeps() result, err := callTool(ctxWithAuth(Scope{}), deps, "create_run_profile", map[string]any{ "workspace_id": testWSID.String(), "slug": "dev", "name": "Dev", "command": "pnpm dev", }) require.NoError(t, err) require.False(t, result.IsError) var createOut runProfileOut unmarshalStructured(t, result, &createOut) assert.True(t, createOut.OK) assert.Equal(t, "dev", createOut.Profile.Slug) pid := createOut.Profile.ID result, err = callTool(ctxWithAuth(Scope{}), deps, "list_run_profiles", map[string]any{ "workspace_id": testWSID.String(), }) require.NoError(t, err) var listOut listRunProfilesOut unmarshalStructured(t, result, &listOut) require.Len(t, listOut.Profiles, 1) result, err = callTool(ctxWithAuth(Scope{}), deps, "update_run_profile", map[string]any{ "workspace_id": testWSID.String(), "profile_id": pid, "name": "Dev2", }) require.NoError(t, err) unmarshalStructured(t, result, &createOut) assert.Equal(t, "Dev2", createOut.Profile.Name) result, err = callTool(ctxWithAuth(Scope{}), deps, "delete_run_profile", map[string]any{ "workspace_id": testWSID.String(), "profile_id": pid, }) require.NoError(t, err) require.False(t, result.IsError) assert.Empty(t, runSvc.profiles) } func TestRunProfileLifecycleOps(t *testing.T) { deps, runSvc := runTestDeps() p, err := runSvc.Create(context.Background(), workspace.Caller{}, testWSID, run.CreateRunProfileReq{ Slug: "dev", Name: "Dev", Command: "pnpm dev", }) require.NoError(t, err) for _, tc := range []struct{ tool, op, wantStatus string }{ {"start_run_profile", "start", string(run.StatusRunning)}, {"stop_run_profile", "stop", string(run.StatusStopped)}, {"restart_run_profile", "restart", string(run.StatusRunning)}, {"get_run_status", "status", string(run.StatusRunning)}, } { result, err := callTool(ctxWithAuth(Scope{}), deps, tc.tool, map[string]any{ "workspace_id": testWSID.String(), "profile_id": p.ID, }) require.NoError(t, err, tc.tool) require.False(t, result.IsError, tc.tool) var out runStatusOut unmarshalStructured(t, result, &out) assert.Equal(t, tc.op, runSvc.lastOp, tc.tool) assert.Equal(t, tc.wantStatus, out.Status.Status, tc.tool) } } func TestRunProfileMembershipEnforced(t *testing.T) { deps, runSvc := runTestDeps() // profile 属于其他 workspace → 不可经本 workspace 操作 runSvc.profiles = append(runSvc.profiles, run.RunProfileResp{ ID: uuid.NewString(), WorkspaceID: uuid.NewString(), Slug: "alien", Name: "Alien", }) result, err := callTool(ctxWithAuth(Scope{}), deps, "start_run_profile", map[string]any{ "workspace_id": testWSID.String(), "profile_id": runSvc.profiles[0].ID, }) require.NoError(t, err) assert.True(t, result.IsError) assert.Empty(t, runSvc.lastOp) } func TestGetRunLogs(t *testing.T) { deps, runSvc := runTestDeps() p, err := runSvc.Create(context.Background(), workspace.Caller{}, testWSID, run.CreateRunProfileReq{ Slug: "dev", Name: "Dev", Command: "pnpm dev", }) require.NoError(t, err) runSvc.logs = []run.LogLineResp{ {ID: 1, Level: "out", Text: "listening on :3000", TS: "2026-06-10T00:00:00Z"}, {ID: 2, Level: "err", Text: "warn: deprecated", TS: "2026-06-10T00:00:01Z"}, } result, err := callTool(ctxWithAuth(Scope{}), deps, "get_run_logs", map[string]any{ "workspace_id": testWSID.String(), "profile_id": p.ID, }) require.NoError(t, err) require.False(t, result.IsError) var out getRunLogsOut unmarshalStructured(t, result, &out) require.Len(t, out.Lines, 2) assert.Equal(t, "listening on :3000", out.Lines[0].Text) }