Why: F10 - any handler error became a no-side-effect rejected receipt and an audit-store failure dropped the generated operation ID, losing the reconciliation handle and inviting unsafe retries. Evidence: cmdbus tests pass (verify-u7b.json); post-execution-unknown, audit-failure-keeps-ID and confirmation-idempotency cases added. Impact: unknown receipts now carry the trusted operation ID via the U7A constructor; rejected stays ID-free; no auto-replay anywhere. Refs: feature/agent-stack-audit-remediation-b1 U7B
97 lines
3.4 KiB
Go
97 lines
3.4 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|