package server import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "testing" "time" agentcommand "agent-command" "agent-system-backend/internal/cmdbus" ) // TestCommandOperationLookupIsOwnerProjectScoped 覆盖 U7b/R7 的对账查询: // unknown 回执给出的 operationID 能按认证 owner + project 查到落库记录; // 跨 owner、错 project、缺 projectId 一律查不到或拒(fail-closed),任何 // "查不到"都不得被读成"未执行"。 func TestCommandOperationLookupIsOwnerProjectScoped(t *testing.T) { srv, st := newAuthTestServer(t) srv.Bus = cmdbus.New(st, nil, time.Now) srv.WireBuiltinCommands() // 生产装配点:总线拿到宿主侧对账查询端口 h := srv.Handler() aliceKey, err := createLegacyAPIKeyFixture(t, st, "alice", "e2e") if err != nil { t.Fatal(err) } bobKey, err := createLegacyAPIKeyFixture(t, st, "bob", "e2e") if err != nil { t.Fatal(err) } project, err := st.CreateProject("alice", "对账项目") if err != nil { t.Fatal(err) } conversation, err := st.CreateConversation("alice", project.ID, "对账会话", "") if err != nil { t.Fatal(err) } // 写库之后才失败的命令:unknown + operationID(审计信封按会话快照落项目)。 if err := srv.Bus.RegisterPluginCommand(agentcommand.CommandSpec{ID: "demo.write", Title: "写入"}, func(context.Context, cmdbus.Request) (any, error) { return nil, errors.New("ack lost") }); err != nil { t.Fatal(err) } resp := srv.Bus.Dispatch(context.Background(), cmdbus.Request{ ID: "demo.write", Owner: "alice", Actor: "alice", ConversationID: conversation.ID, }) if resp.Receipt.State != agentcommand.StateUnknown || resp.Receipt.OperationID == "" { t.Fatalf("post-execution failure must be unknown with an operation ID: %+v", resp.Receipt) } operationID := resp.Receipt.OperationID get := func(key, url string) *httptest.ResponseRecorder { t.Helper() req := httptest.NewRequest(http.MethodGet, url, nil) req.Header.Set("Authorization", "Bearer "+key) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) return rec } rec := get(aliceKey, "/api/commands/operations/"+operationID+"?projectId="+project.ID) if rec.Code != http.StatusOK { t.Fatalf("owner lookup: %d %s", rec.Code, rec.Body.String()) } var out struct { Operation struct { OperationID string `json:"OperationID"` CommandID string `json:"CommandID"` State string `json:"State"` } `json:"operation"` } if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil { t.Fatal(err) } if out.Operation.OperationID != operationID || out.Operation.CommandID != "demo.write" || out.Operation.State != string(agentcommand.StateUnknown) { t.Fatalf("lookup projection wrong: %+v", out.Operation) } for _, tc := range []struct { name, key, url string want int }{ {"cross owner", bobKey, "/api/commands/operations/" + operationID + "?projectId=" + project.ID, http.StatusNotFound}, {"wrong project", aliceKey, "/api/commands/operations/" + operationID + "?projectId=prj_other", http.StatusNotFound}, {"missing project", aliceKey, "/api/commands/operations/" + operationID, http.StatusBadRequest}, {"missing operation", aliceKey, "/api/commands/operations/op_absent?projectId=" + project.ID, http.StatusNotFound}, } { if rec := get(tc.key, tc.url); rec.Code != tc.want { t.Fatalf("%s: %d %s (want %d)", tc.name, rec.Code, rec.Body.String(), tc.want) } } }