feat: Add on_complete/for-each runtime expansion types (gt-8tmz.8)

Implements the schema and validation for runtime dynamic expansion:

- Add OnCompleteSpec type for step completion triggers
- Add on_complete field to Step struct
- Validate for_each path format (must start with "output.")
- Validate bond is required with for_each (and vice versa)
- Validate parallel and sequential are mutually exclusive
- Add cloneOnComplete for proper step cloning
- Add comprehensive tests for parsing and validation

The runtime executor (in gastown) will interpret these fields to:
- Bond N molecules when step completes based on output.collection
- Run bonded molecules in parallel or sequential order
- Pass item/index context via vars substitution

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-25 16:07:57 -08:00
parent 8b5168f1a3
commit a42fac8cb9
3 changed files with 335 additions and 0 deletions

View File

@@ -777,3 +777,244 @@ func TestParse_NeedsAndWaitsFor(t *testing.T) {
t.Errorf("Steps[2].WaitsFor = %q, want 'all-children'", formula.Steps[2].WaitsFor)
}
}
// gt-8tmz.8: Tests for on_complete/for-each runtime expansion
func TestParse_OnComplete(t *testing.T) {
jsonData := `{
"formula": "mol-patrol",
"version": 1,
"type": "workflow",
"steps": [
{
"id": "survey-workers",
"title": "Survey workers",
"on_complete": {
"for_each": "output.polecats",
"bond": "mol-polecat-arm",
"vars": {
"polecat_name": "{item.name}",
"rig": "{item.rig}"
},
"parallel": true
}
},
{
"id": "aggregate",
"title": "Aggregate results",
"needs": ["survey-workers"],
"waits_for": "all-children"
}
]
}`
p := NewParser()
formula, err := p.Parse([]byte(jsonData))
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
// Validate parsed formula
if err := formula.Validate(); err != nil {
t.Errorf("Validate failed: %v", err)
}
// Check on_complete field
oc := formula.Steps[0].OnComplete
if oc == nil {
t.Fatal("Steps[0].OnComplete is nil")
}
if oc.ForEach != "output.polecats" {
t.Errorf("ForEach = %q, want 'output.polecats'", oc.ForEach)
}
if oc.Bond != "mol-polecat-arm" {
t.Errorf("Bond = %q, want 'mol-polecat-arm'", oc.Bond)
}
if len(oc.Vars) != 2 {
t.Errorf("len(Vars) = %d, want 2", len(oc.Vars))
}
if oc.Vars["polecat_name"] != "{item.name}" {
t.Errorf("Vars[polecat_name] = %q, want '{item.name}'", oc.Vars["polecat_name"])
}
if !oc.Parallel {
t.Error("Parallel should be true")
}
}
func TestValidate_OnComplete_Valid(t *testing.T) {
formula := &Formula{
Formula: "mol-valid",
Version: 1,
Type: TypeWorkflow,
Steps: []*Step{
{
ID: "survey",
Title: "Survey",
OnComplete: &OnCompleteSpec{
ForEach: "output.items",
Bond: "mol-item",
},
},
},
}
if err := formula.Validate(); err != nil {
t.Errorf("Validate failed for valid on_complete: %v", err)
}
}
func TestValidate_OnComplete_MissingBond(t *testing.T) {
formula := &Formula{
Formula: "mol-invalid",
Version: 1,
Type: TypeWorkflow,
Steps: []*Step{
{
ID: "survey",
Title: "Survey",
OnComplete: &OnCompleteSpec{
ForEach: "output.items",
// Bond is missing
},
},
},
}
err := formula.Validate()
if err == nil {
t.Error("expected validation error for missing bond")
}
if !strings.Contains(err.Error(), "bond is required") {
t.Errorf("expected 'bond is required' error, got: %v", err)
}
}
func TestValidate_OnComplete_MissingForEach(t *testing.T) {
formula := &Formula{
Formula: "mol-invalid",
Version: 1,
Type: TypeWorkflow,
Steps: []*Step{
{
ID: "survey",
Title: "Survey",
OnComplete: &OnCompleteSpec{
Bond: "mol-item",
// ForEach is missing
},
},
},
}
err := formula.Validate()
if err == nil {
t.Error("expected validation error for missing for_each")
}
if !strings.Contains(err.Error(), "for_each is required") {
t.Errorf("expected 'for_each is required' error, got: %v", err)
}
}
func TestValidate_OnComplete_InvalidForEachPath(t *testing.T) {
formula := &Formula{
Formula: "mol-invalid",
Version: 1,
Type: TypeWorkflow,
Steps: []*Step{
{
ID: "survey",
Title: "Survey",
OnComplete: &OnCompleteSpec{
ForEach: "items", // Should start with "output."
Bond: "mol-item",
},
},
},
}
err := formula.Validate()
if err == nil {
t.Error("expected validation error for invalid for_each path")
}
if !strings.Contains(err.Error(), "must start with 'output.'") {
t.Errorf("expected 'must start with output.' error, got: %v", err)
}
}
func TestValidate_OnComplete_ParallelAndSequential(t *testing.T) {
formula := &Formula{
Formula: "mol-invalid",
Version: 1,
Type: TypeWorkflow,
Steps: []*Step{
{
ID: "survey",
Title: "Survey",
OnComplete: &OnCompleteSpec{
ForEach: "output.items",
Bond: "mol-item",
Parallel: true,
Sequential: true, // Can't have both
},
},
},
}
err := formula.Validate()
if err == nil {
t.Error("expected validation error for parallel + sequential")
}
if !strings.Contains(err.Error(), "cannot set both parallel and sequential") {
t.Errorf("expected 'cannot set both' error, got: %v", err)
}
}
func TestValidate_OnComplete_Sequential(t *testing.T) {
formula := &Formula{
Formula: "mol-valid",
Version: 1,
Type: TypeWorkflow,
Steps: []*Step{
{
ID: "process-queue",
Title: "Process queue",
OnComplete: &OnCompleteSpec{
ForEach: "output.branches",
Bond: "mol-merge",
Sequential: true,
},
},
},
}
if err := formula.Validate(); err != nil {
t.Errorf("Validate failed for sequential on_complete: %v", err)
}
}
func TestValidate_OnComplete_InChildren(t *testing.T) {
formula := &Formula{
Formula: "mol-valid",
Version: 1,
Type: TypeWorkflow,
Steps: []*Step{
{
ID: "parent",
Title: "Parent",
Children: []*Step{
{
ID: "child",
Title: "Child",
OnComplete: &OnCompleteSpec{
ForEach: "output.items",
Bond: "mol-item",
},
},
},
},
},
}
if err := formula.Validate(); err != nil {
t.Errorf("Validate failed for on_complete in child: %v", err)
}
}