feat: show Creator action history
CI / test (pull_request) Successful in 10s
CI / test (push) Successful in 10s

This commit was merged in pull request #5.
This commit is contained in:
m
2026-08-16 18:40:58 +03:00
parent 2d2615b734
commit 0e56fe7662
11 changed files with 495 additions and 11 deletions
+100
View File
@@ -53,6 +53,43 @@ func (f fakeCreatorAuthority) LoadGrant(context.Context, authority.GrantID) (aut
return f.grant, f.scope, nil
}
func (f *fakeExecutionRepository) ListAuthorizedByOwner(context.Context, identity.ActorID, identity.PrincipalID, int) ([]commitcontract.AuthorizedAction, error) {
if f.action.ID == "" {
return []commitcontract.AuthorizedAction{}, nil
}
return []commitcontract.AuthorizedAction{f.action}, nil
}
func (f *fakeExecutionRepository) FindLatestAttempt(_ context.Context, id commitcontract.AuthorizedActionID) (commitcontract.ActionAttempt, bool, error) {
for index := len(f.attempts) - 1; index >= 0; index-- {
if f.attempts[index].AuthorizedID == id {
return f.attempts[index], true, nil
}
}
return commitcontract.ActionAttempt{}, false, nil
}
type fakeCreatorHistoryRepository struct {
fakeExecutionRepository
actions []commitcontract.AuthorizedAction
latest map[commitcontract.AuthorizedActionID]commitcontract.ActionAttempt
actorID identity.ActorID
principalID identity.PrincipalID
limit int
listCalls int
}
func (f *fakeCreatorHistoryRepository) ListAuthorizedByOwner(_ context.Context, actorID identity.ActorID, principalID identity.PrincipalID, limit int) ([]commitcontract.AuthorizedAction, error) {
f.actorID, f.principalID, f.limit = actorID, principalID, limit
f.listCalls++
return f.actions, nil
}
func (f *fakeCreatorHistoryRepository) FindLatestAttempt(_ context.Context, id commitcontract.AuthorizedActionID) (commitcontract.ActionAttempt, bool, error) {
attempt, ok := f.latest[id]
return attempt, ok, nil
}
func TestCreatorCommandDerivesAuthorityAndActionFieldsServerSide(t *testing.T) {
handler, repository, executor := validCreatorCommandHandler()
mux := http.NewServeMux()
@@ -127,6 +164,69 @@ func TestCreatorCommandRejectsSessionForeignOwnerStaleStateAndRevokedGrant(t *te
}
}
func TestCreatorActionHistoryUsesSessionOwnerAndShowsMissingAttempt(t *testing.T) {
session := identity.CreatorSession{ActorID: "creator-actor", PrincipalID: "creator-principal", ExpiresAtUnix: 1000}
repository := &fakeCreatorHistoryRepository{
actions: []commitcontract.AuthorizedAction{
{ID: "authorized-new", PrincipalID: session.PrincipalID, AuthorizedAtUnixMillis: 20, State: commitcontract.ActionAuthorized, Proposal: commitcontract.ActionProposal{ActorID: session.ActorID, IntentID: "intent-new", Operation: commitcontract.OperationSatisfyDelivered, ContinuitySubject: continuity.SatisfyDeliveredSubject{ProspectiveItemID: "prospective-new"}}},
{ID: "authorized-pending", PrincipalID: session.PrincipalID, AuthorizedAtUnixMillis: 10, State: commitcontract.ActionAuthorized, Proposal: commitcontract.ActionProposal{ActorID: session.ActorID, IntentID: "intent-pending", Operation: commitcontract.OperationSatisfyDelivered, ContinuitySubject: continuity.SatisfyDeliveredSubject{ProspectiveItemID: "prospective-pending"}}},
},
latest: map[commitcontract.AuthorizedActionID]commitcontract.ActionAttempt{
"authorized-new": {ID: "attempt-new", AuthorizedID: "authorized-new", IntentID: "intent-new", State: commitcontract.ActionSucceeded},
},
}
handler := creatorCommandHandler{core: fakeCreatorCore{session: session}, repository: repository}
mux := http.NewServeMux()
registerCreatorCommandRoutes(mux, handler)
request := httptest.NewRequest(http.MethodGet, "/v1/creator/authorized-actions?limit=2", nil)
request.Header.Set("Authorization", "Bearer creator-session")
response := httptest.NewRecorder()
mux.ServeHTTP(response, request)
if response.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
}
var result creatorActionHistoryResponse
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
t.Fatal(err)
}
if repository.actorID != session.ActorID || repository.principalID != session.PrincipalID || repository.limit != 2 || repository.listCalls != 1 {
t.Fatalf("owner query actor=%q principal=%q limit=%d calls=%d", repository.actorID, repository.principalID, repository.limit, repository.listCalls)
}
if len(result.Items) != 2 || result.Items[0].State != commitcontract.ActionSucceeded || result.Items[0].ActionAttemptID != "attempt-new" ||
result.Items[1].State != commitcontract.ActionAuthorized || result.Items[1].ActionAttemptID != "" {
t.Fatalf("history=%#v", result.Items)
}
if strings.Contains(response.Body.String(), "EvidenceRef") || strings.Contains(response.Body.String(), "GrantID") {
t.Fatalf("history leaked restricted envelope fields: %s", response.Body.String())
}
}
func TestCreatorActionHistoryRejectsSessionAndInvalidLimitBeforeQuery(t *testing.T) {
for _, test := range []struct {
name string
sessionErr error
path string
status int
}{
{name: "session", sessionErr: errCreatorSessionDenied, path: "/v1/creator/authorized-actions", status: http.StatusUnauthorized},
{name: "zero", path: "/v1/creator/authorized-actions?limit=0", status: http.StatusBadRequest},
{name: "too large", path: "/v1/creator/authorized-actions?limit=51", status: http.StatusBadRequest},
{name: "invalid", path: "/v1/creator/authorized-actions?limit=many", status: http.StatusBadRequest},
} {
t.Run(test.name, func(t *testing.T) {
repository := &fakeCreatorHistoryRepository{}
handler := creatorCommandHandler{core: fakeCreatorCore{session: identity.CreatorSession{ActorID: "actor", PrincipalID: "principal"}, sessionErr: test.sessionErr}, repository: repository}
mux := http.NewServeMux()
registerCreatorCommandRoutes(mux, handler)
response := httptest.NewRecorder()
mux.ServeHTTP(response, httptest.NewRequest(http.MethodGet, test.path, nil))
if response.Code != test.status || repository.listCalls != 0 {
t.Fatalf("status=%d calls=%d", response.Code, repository.listCalls)
}
})
}
}
func validCreatorCommandHandler() (creatorCommandHandler, *fakeExecutionRepository, *knownExecutor) {
session := identity.CreatorSession{ActorID: "creator-actor", PrincipalID: "creator-principal", ExpiresAtUnix: 1000}
target := work.TaskTarget{TaskID: "task-1", GoalID: "goal-1", ScopeID: "scope-1", Revision: 3}