Skip to content

Commit dfb7336

Browse files
Add launch closure automation dispatch
1 parent 98d1da1 commit dfb7336

5 files changed

Lines changed: 556 additions & 0 deletions

app/secure_cells_api.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1350,6 +1350,10 @@ func (app *AethelredApp) SecureCellsGetHandler() http.Handler {
13501350
return
13511351
}
13521352

1353+
if app.handleSecureCellGovernmentAgentExecutionLaunchClosureAutomationDispatchGet(w, r) {
1354+
return
1355+
}
1356+
13531357
if app.handleSecureCellGovernmentAgentExecutionLaunchReceiptValidationGet(w, r) {
13541358
return
13551359
}
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
package app
2+
3+
import (
4+
"encoding/csv"
5+
"fmt"
6+
"net/http"
7+
"strconv"
8+
"strings"
9+
"time"
10+
11+
securecellsintegration "github.com/aethelred/aethelred/pkg/integrations/securecells"
12+
)
13+
14+
type secureCellGovernmentAgentExecutionLaunchClosureAutomationDispatchResponse struct {
15+
Dispatch *securecellsintegration.SecureCellGovernmentAgentExecutionLaunchClosureAutomationDispatch `json:"dispatch,omitempty"`
16+
}
17+
18+
func (app *AethelredApp) handleSecureCellGovernmentAgentExecutionLaunchClosureAutomationDispatchGet(w http.ResponseWriter, r *http.Request) bool {
19+
if r.URL.Path == secureCellsCollectionRoute+"/government-agent-execution-launch-closure-automation-dispatch" {
20+
filter, err := parseSecureCellGovernmentAgentExecutionLaunchClosureOverdueActionFilter(r)
21+
if err != nil {
22+
writeSecureCellAPIError(w, http.StatusBadRequest, err.Error())
23+
return true
24+
}
25+
dispatch, err := app.secureCellService.GetGovernmentAgentExecutionLaunchClosureAutomationDispatch(r.Context(), filter)
26+
if err != nil {
27+
writeSecureCellAPIError(w, http.StatusInternalServerError, err.Error())
28+
return true
29+
}
30+
writeSecureCellJSON(w, http.StatusOK, secureCellGovernmentAgentExecutionLaunchClosureAutomationDispatchResponse{Dispatch: dispatch})
31+
return true
32+
}
33+
34+
if r.URL.Path == secureCellsCollectionRoute+"/government-agent-execution-launch-closure-automation-dispatch/export" {
35+
filter, err := parseSecureCellGovernmentAgentExecutionLaunchClosureOverdueActionFilter(r)
36+
if err != nil {
37+
writeSecureCellAPIError(w, http.StatusBadRequest, err.Error())
38+
return true
39+
}
40+
dispatch, err := app.secureCellService.GetGovernmentAgentExecutionLaunchClosureAutomationDispatch(r.Context(), filter)
41+
if err != nil {
42+
writeSecureCellAPIError(w, http.StatusInternalServerError, err.Error())
43+
return true
44+
}
45+
if err := writeSecureCellGovernmentAgentExecutionLaunchClosureAutomationDispatchExport(w, r, dispatch); err != nil {
46+
writeSecureCellAPIError(w, http.StatusBadRequest, err.Error())
47+
}
48+
return true
49+
}
50+
51+
return false
52+
}
53+
54+
func writeSecureCellGovernmentAgentExecutionLaunchClosureAutomationDispatchExport(w http.ResponseWriter, r *http.Request, dispatch *securecellsintegration.SecureCellGovernmentAgentExecutionLaunchClosureAutomationDispatch) error {
55+
format := secureCellExportFormat(r)
56+
switch format {
57+
case "json":
58+
writeSecureCellJSON(w, http.StatusOK, secureCellGovernmentAgentExecutionLaunchClosureAutomationDispatchResponse{Dispatch: dispatch})
59+
return nil
60+
case "csv":
61+
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
62+
w.Header().Set("Content-Disposition", `attachment; filename="secure-cell-government-agent-execution-launch-closure-automation-dispatch.csv"`)
63+
writer := csv.NewWriter(w)
64+
for _, row := range secureCellGovernmentAgentExecutionLaunchClosureAutomationDispatchCSVRows(dispatch) {
65+
if err := writer.Write(row); err != nil {
66+
return fmt.Errorf("write government-agent-execution-launch-closure-automation-dispatch csv row: %w", err)
67+
}
68+
}
69+
writer.Flush()
70+
return writer.Error()
71+
default:
72+
return fmt.Errorf("unsupported export format %q", format)
73+
}
74+
}
75+
76+
func secureCellGovernmentAgentExecutionLaunchClosureAutomationDispatchCSVRows(dispatch *securecellsintegration.SecureCellGovernmentAgentExecutionLaunchClosureAutomationDispatch) [][]string {
77+
rows := [][]string{{
78+
"dispatch_id",
79+
"brief_id",
80+
"runbook_id",
81+
"packet_id",
82+
"board_id",
83+
"summary_id",
84+
"jurisdiction",
85+
"service_code_filter",
86+
"service_tier_filter",
87+
"evaluated_at",
88+
"focus_lane",
89+
"focus_action",
90+
"severity",
91+
"command",
92+
"lead_role",
93+
"lead_pending_action",
94+
"escalation_required",
95+
"assignment_count",
96+
"assignment_sequence",
97+
"assignment_role",
98+
"assignment_pending_action",
99+
"assignment_automation_action",
100+
"assignment_instruction",
101+
"assignment_cell_ids",
102+
"item_cell_id",
103+
"item_name",
104+
"item_lane",
105+
"item_pending_action",
106+
"item_automation_action",
107+
"item_action_priority",
108+
"item_due_at",
109+
"item_overdue_seconds",
110+
"item_escalation_needed",
111+
"item_action_digest",
112+
"brief_digest",
113+
"dispatch_digest",
114+
"generated_at",
115+
}}
116+
if dispatch == nil {
117+
return rows
118+
}
119+
if len(dispatch.Items) == 0 && len(dispatch.Assignments) == 0 {
120+
rows = append(rows, []string{
121+
dispatch.DispatchID,
122+
dispatch.BriefID,
123+
dispatch.RunbookID,
124+
dispatch.PacketID,
125+
dispatch.BoardID,
126+
dispatch.SummaryID,
127+
dispatch.Jurisdiction,
128+
dispatch.ServiceCode,
129+
dispatch.ServiceTier,
130+
dispatch.EvaluatedAt.UTC().Format(time.RFC3339Nano),
131+
string(dispatch.FocusLane),
132+
dispatch.FocusAction,
133+
string(dispatch.Severity),
134+
dispatch.Command,
135+
dispatch.LeadRole,
136+
dispatch.LeadPendingAction,
137+
strconv.FormatBool(dispatch.EscalationRequired),
138+
strconv.Itoa(dispatch.AssignmentCount),
139+
"", "", "", "", "", "", "", "", "", "", "", "", "", "", "",
140+
dispatch.BriefDigest,
141+
dispatch.DispatchDigest,
142+
dispatch.GeneratedAt.UTC().Format(time.RFC3339Nano),
143+
})
144+
return rows
145+
}
146+
assignments := dispatch.Assignments
147+
if len(assignments) == 0 {
148+
assignments = []securecellsintegration.SecureCellGovernmentAgentExecutionLaunchClosureAutomationDispatchAssignment{{}}
149+
}
150+
items := dispatch.Items
151+
if len(items) == 0 {
152+
items = []securecellsintegration.SecureCellGovernmentAgentExecutionLaunchClosureAutomationBoardItem{{}}
153+
}
154+
maxLen := len(assignments)
155+
if len(items) > maxLen {
156+
maxLen = len(items)
157+
}
158+
for i := 0; i < maxLen; i++ {
159+
var assignment securecellsintegration.SecureCellGovernmentAgentExecutionLaunchClosureAutomationDispatchAssignment
160+
var item securecellsintegration.SecureCellGovernmentAgentExecutionLaunchClosureAutomationBoardItem
161+
if i < len(assignments) {
162+
assignment = assignments[i]
163+
}
164+
if i < len(items) {
165+
item = items[i]
166+
}
167+
dueAt := ""
168+
if item.DueAt != nil {
169+
dueAt = item.DueAt.UTC().Format(time.RFC3339Nano)
170+
}
171+
assignmentSequence := ""
172+
if assignment.Sequence > 0 {
173+
assignmentSequence = strconv.Itoa(assignment.Sequence)
174+
}
175+
rows = append(rows, []string{
176+
dispatch.DispatchID,
177+
dispatch.BriefID,
178+
dispatch.RunbookID,
179+
dispatch.PacketID,
180+
dispatch.BoardID,
181+
dispatch.SummaryID,
182+
dispatch.Jurisdiction,
183+
dispatch.ServiceCode,
184+
dispatch.ServiceTier,
185+
dispatch.EvaluatedAt.UTC().Format(time.RFC3339Nano),
186+
string(dispatch.FocusLane),
187+
dispatch.FocusAction,
188+
string(dispatch.Severity),
189+
dispatch.Command,
190+
dispatch.LeadRole,
191+
dispatch.LeadPendingAction,
192+
strconv.FormatBool(dispatch.EscalationRequired),
193+
strconv.Itoa(dispatch.AssignmentCount),
194+
assignmentSequence,
195+
assignment.Role,
196+
assignment.PendingAction,
197+
assignment.AutomationAction,
198+
assignment.Instruction,
199+
strings.Join(assignment.CellIDs, "|"),
200+
item.CellID,
201+
item.Name,
202+
string(item.Lane),
203+
item.PendingAction,
204+
item.AutomationAction,
205+
string(item.ActionPriority),
206+
dueAt,
207+
strconv.FormatInt(item.OverdueSeconds, 10),
208+
strconv.FormatBool(item.EscalationNeeded),
209+
item.ActionDigest,
210+
dispatch.BriefDigest,
211+
dispatch.DispatchDigest,
212+
dispatch.GeneratedAt.UTC().Format(time.RFC3339Nano),
213+
})
214+
}
215+
return rows
216+
}

app/secure_cells_api_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,35 @@ func TestSecureCellGovernmentAgentExecutionLaunchClosureAutomationBriefCSVRows_E
168168
}
169169
}
170170

171+
func TestSecureCellGovernmentAgentExecutionLaunchClosureAutomationDispatchCSVRows_EmptyDispatchColumnCount(t *testing.T) {
172+
dispatch := &securecellsintegration.SecureCellGovernmentAgentExecutionLaunchClosureAutomationDispatch{
173+
DispatchID: "government-agent-execution-launch-closure-automation-dispatch:UAE:abcdef123456",
174+
BriefID: "government-agent-execution-launch-closure-automation-brief:UAE:abcdef123456",
175+
RunbookID: "government-agent-execution-launch-closure-automation-runbook:UAE:abcdef123456",
176+
PacketID: "government-agent-execution-launch-closure-automation-packet:UAE:abcdef123456",
177+
BoardID: "government-agent-execution-launch-closure-automation-board:UAE:abcdef123456",
178+
SummaryID: "government-agent-execution-launch-closure-automation-summary:UAE:abcdef123456",
179+
Jurisdiction: "UAE",
180+
EvaluatedAt: time.Unix(1, 0).UTC(),
181+
FocusLane: securecellsintegration.SecureCellGovernmentAgentExecutionLaunchClosureAutomationBoardLaneDue,
182+
FocusAction: "work_next_due_closure_actions",
183+
Severity: securecellsintegration.SecureCellGovernmentAgentExecutionLaunchClosureAutomationBriefSeverityMedium,
184+
Command: "Dispatch preventive work to clear the next due closure actions.",
185+
LeadRole: "workflow_coordinator",
186+
BriefDigest: strings.Repeat("a", 64),
187+
DispatchDigest: strings.Repeat("b", 64),
188+
GeneratedAt: time.Unix(2, 0).UTC(),
189+
}
190+
191+
rows := secureCellGovernmentAgentExecutionLaunchClosureAutomationDispatchCSVRows(dispatch)
192+
if len(rows) != 2 {
193+
t.Fatalf("expected header plus empty-dispatch row, got %d rows", len(rows))
194+
}
195+
if got, want := len(rows[1]), len(rows[0]); got != want {
196+
t.Fatalf("expected empty-dispatch csv row to have %d columns, got %d: %#v", want, got, rows[1])
197+
}
198+
}
199+
171200
func TestSecureCellsHandlers_BearerCreateGetArtifactsFlow(t *testing.T) {
172201
app := newAuditEnabledTestApp(t, sims.AppOptionsMap{
173202
"aethelred.pqc.mode": "simulated",
@@ -1655,6 +1684,36 @@ func TestSecureCellsHandlers_GovernmentAgentReadinessSurfaces(t *testing.T) {
16551684
if !strings.Contains(launchClosureAutomationBriefExportRec.Body.String(), "brief_digest") || !strings.Contains(launchClosureAutomationBriefExportRec.Body.String(), createResp.Result.CellID) {
16561685
t.Fatalf("expected government-agent execution launch closure automation brief csv export, got %s", launchClosureAutomationBriefExportRec.Body.String())
16571686
}
1687+
1688+
launchClosureAutomationDispatchReq := httptest.NewRequest(http.MethodGet, secureCellsCollectionRoute+"/government-agent-execution-launch-closure-automation-dispatch?jurisdiction=UAE&before="+url.QueryEscape(overdueBefore), nil)
1689+
launchClosureAutomationDispatchRec := httptest.NewRecorder()
1690+
app.SecureCellsGetHandler().ServeHTTP(launchClosureAutomationDispatchRec, launchClosureAutomationDispatchReq)
1691+
if launchClosureAutomationDispatchRec.Code != http.StatusOK {
1692+
t.Fatalf("expected status %d, got %d body=%s", http.StatusOK, launchClosureAutomationDispatchRec.Code, launchClosureAutomationDispatchRec.Body.String())
1693+
}
1694+
var launchClosureAutomationDispatchResp secureCellGovernmentAgentExecutionLaunchClosureAutomationDispatchResponse
1695+
if err := json.Unmarshal(launchClosureAutomationDispatchRec.Body.Bytes(), &launchClosureAutomationDispatchResp); err != nil {
1696+
t.Fatalf("unmarshal government-agent execution launch closure automation dispatch response: %v", err)
1697+
}
1698+
if launchClosureAutomationDispatchResp.Dispatch == nil || launchClosureAutomationDispatchResp.Dispatch.AssignmentCount != 1 {
1699+
t.Fatalf("unexpected government-agent execution launch closure automation dispatch response: %+v", launchClosureAutomationDispatchResp.Dispatch)
1700+
}
1701+
if launchClosureAutomationDispatchResp.Dispatch.DispatchDigest == "" || launchClosureAutomationDispatchResp.Dispatch.Command == "" || launchClosureAutomationDispatchResp.Dispatch.LeadRole == "" {
1702+
t.Fatalf("expected digest-bound execution launch closure automation dispatch, got %+v", launchClosureAutomationDispatchResp.Dispatch)
1703+
}
1704+
if launchClosureAutomationDispatchResp.Dispatch.BriefID == "" || len(launchClosureAutomationDispatchResp.Dispatch.Assignments) == 0 || len(launchClosureAutomationDispatchResp.Dispatch.Assignments[0].CellIDs) == 0 {
1705+
t.Fatalf("expected populated execution launch closure automation dispatch, got %+v", launchClosureAutomationDispatchResp.Dispatch)
1706+
}
1707+
1708+
launchClosureAutomationDispatchExportReq := httptest.NewRequest(http.MethodGet, secureCellsCollectionRoute+"/government-agent-execution-launch-closure-automation-dispatch/export?format=csv&jurisdiction=UAE&before="+url.QueryEscape(overdueBefore), nil)
1709+
launchClosureAutomationDispatchExportRec := httptest.NewRecorder()
1710+
app.SecureCellsGetHandler().ServeHTTP(launchClosureAutomationDispatchExportRec, launchClosureAutomationDispatchExportReq)
1711+
if launchClosureAutomationDispatchExportRec.Code != http.StatusOK {
1712+
t.Fatalf("expected status %d, got %d body=%s", http.StatusOK, launchClosureAutomationDispatchExportRec.Code, launchClosureAutomationDispatchExportRec.Body.String())
1713+
}
1714+
if !strings.Contains(launchClosureAutomationDispatchExportRec.Body.String(), "dispatch_digest") || !strings.Contains(launchClosureAutomationDispatchExportRec.Body.String(), createResp.Result.CellID) {
1715+
t.Fatalf("expected government-agent execution launch closure automation dispatch csv export, got %s", launchClosureAutomationDispatchExportRec.Body.String())
1716+
}
16581717
}
16591718

16601719
func TestSecureCellsHandlers_BearerFederationLifecycleFlow(t *testing.T) {

0 commit comments

Comments
 (0)