enforce lifecycle evidence contracts
This commit is contained in:
+9
-15
@@ -221,27 +221,21 @@ func main() {
|
||||
}
|
||||
types := map[string]string{"release": "TaskReleased", "complete": "TaskCompleted", "block": "TaskBlocked"}
|
||||
var p map[string]any
|
||||
if r.Body != nil {
|
||||
if json.NewDecoder(r.Body).Decode(&p) != nil {
|
||||
p = map[string]any{}
|
||||
}
|
||||
}
|
||||
if p == nil {
|
||||
p = map[string]any{}
|
||||
if r.Body == nil || json.NewDecoder(r.Body).Decode(&p) != nil || p == nil {
|
||||
http.Error(w, "invalid lifecycle payload", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if action == "release" && p["reason"] == nil && p["handoff_ref"] == nil {
|
||||
p["reason"] = "released_by_api"
|
||||
http.Error(w, "reason or handoff_ref required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if action == "block" && p["blocker"] == nil {
|
||||
p["blocker"] = "blocked_by_api"
|
||||
http.Error(w, "blocker required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if action == "complete" && p["report_ref"] == nil {
|
||||
ref, putErr := s.PutArtifact([]byte("completed without an attached report\n"))
|
||||
if putErr != nil {
|
||||
http.Error(w, putErr.Error(), 500)
|
||||
return
|
||||
}
|
||||
p["report_ref"] = ref
|
||||
http.Error(w, "report_ref required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
ePayload, _ := json.Marshal(p)
|
||||
e = domain.Event{ID: id(), Type: types[action], TaskID: taskID, Version: t.Version + 1, Payload: ePayload}
|
||||
|
||||
@@ -72,3 +72,23 @@ func TestArtifactIsContentAddressed(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLifecycleEventsRequireEvidence(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
typ string
|
||||
body string
|
||||
}{
|
||||
{"release", "TaskReleased", `{}`},
|
||||
{"complete", "TaskCompleted", `{}`},
|
||||
{"block", "TaskBlocked", `{}`},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := domain.ValidateEvent(domain.Event{Type: tc.typ, TaskID: "task-1", Version: 1, Payload: json.RawMessage(tc.body)})
|
||||
if err == nil {
|
||||
t.Fatal("expected lifecycle evidence validation error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,8 @@ The first pass closed the store/API defects (lifecycle defaults, CAS content ver
|
||||
|
||||
The latest pass now applies `AuthorizeEvent` to lifecycle and approval writes and adds `/readyz` with router/provider configuration checks. Readiness is configuration-level only; it does not yet probe herdr/provider health.
|
||||
|
||||
The lifecycle API contract pass now requires callers to provide explicit release evidence (`reason` or `handoff_ref`), a block `blocker`, and a completion `report_ref`. The server no longer creates generated placeholder completion artifacts or converts malformed/empty lifecycle bodies into defaults. Regression coverage validates that release, completion, and block events reject missing evidence.
|
||||
|
||||
Recommended order:
|
||||
|
||||
1. Add the orchestration coordinator: lease → worktree → harness session → bootstrap → lifecycle events.
|
||||
|
||||
Reference in New Issue
Block a user