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"}
|
types := map[string]string{"release": "TaskReleased", "complete": "TaskCompleted", "block": "TaskBlocked"}
|
||||||
var p map[string]any
|
var p map[string]any
|
||||||
if r.Body != nil {
|
if r.Body == nil || json.NewDecoder(r.Body).Decode(&p) != nil || p == nil {
|
||||||
if json.NewDecoder(r.Body).Decode(&p) != nil {
|
http.Error(w, "invalid lifecycle payload", http.StatusBadRequest)
|
||||||
p = map[string]any{}
|
return
|
||||||
}
|
|
||||||
}
|
|
||||||
if p == nil {
|
|
||||||
p = map[string]any{}
|
|
||||||
}
|
}
|
||||||
if action == "release" && p["reason"] == nil && p["handoff_ref"] == nil {
|
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 {
|
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 {
|
if action == "complete" && p["report_ref"] == nil {
|
||||||
ref, putErr := s.PutArtifact([]byte("completed without an attached report\n"))
|
http.Error(w, "report_ref required", http.StatusBadRequest)
|
||||||
if putErr != nil {
|
return
|
||||||
http.Error(w, putErr.Error(), 500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
p["report_ref"] = ref
|
|
||||||
}
|
}
|
||||||
ePayload, _ := json.Marshal(p)
|
ePayload, _ := json.Marshal(p)
|
||||||
e = domain.Event{ID: id(), Type: types[action], TaskID: taskID, Version: t.Version + 1, Payload: ePayload}
|
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)
|
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 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:
|
Recommended order:
|
||||||
|
|
||||||
1. Add the orchestration coordinator: lease → worktree → harness session → bootstrap → lifecycle events.
|
1. Add the orchestration coordinator: lease → worktree → harness session → bootstrap → lifecycle events.
|
||||||
|
|||||||
Reference in New Issue
Block a user