package run import ( "context" "testing" ) // ProgressSinkFrom on a bare context returns nil (nothing wired). func TestProgressSinkFrom_Empty(t *testing.T) { if got := ProgressSinkFrom(context.Background()); got != nil { t.Fatalf("expected nil sink on bare context, got non-nil") } } // WithProgressSink round-trips a sink through the context. func TestWithProgressSink_RoundTrip(t *testing.T) { var got string ctx := WithProgressSink(context.Background(), func(n string) { got = n }) sink := ProgressSinkFrom(ctx) if sink == nil { t.Fatal("expected non-nil sink") } sink("hello") if got != "hello" { t.Fatalf("sink did not deliver note; got %q", got) } } // InstallProgressBridge with no parent and a report only feeds the report, // and notifyAncestors is nil (there are no ancestors to notify). func TestInstallProgressBridge_NoParent(t *testing.T) { var reported []string childCtx, notify := InstallProgressBridge(context.Background(), func(n string) { reported = append(reported, n) }) if notify != nil { t.Fatal("expected nil notifyAncestors when there is no parent") } // A child installed under childCtx should reach our report. child := ProgressSinkFrom(childCtx) if child == nil { t.Fatal("expected a child sink installed") } child("from-child") if len(reported) != 1 || reported[0] != "from-child" { t.Fatalf("report did not receive child note; got %v", reported) } } // InstallProgressBridge with a nil report and an existing parent must pass the // parent through unchanged (no needless wrapper layer) — this is the skill // case: a skill run has no recorder of its own but must forward its progress // to the ancestor agent's critic. func TestInstallProgressBridge_NilReportPassesParentThrough(t *testing.T) { var ancestor []string base := WithProgressSink(context.Background(), func(n string) { ancestor = append(ancestor, n) }) childCtx, notify := InstallProgressBridge(base, nil) // This run's own steps must notify the ancestor. if notify == nil { t.Fatal("expected non-nil notifyAncestors when a parent exists") } notify("my-step") // And a descendant under childCtx must also reach the ancestor. ProgressSinkFrom(childCtx)("grandchild-step") if len(ancestor) != 2 || ancestor[0] != "my-step" || ancestor[1] != "grandchild-step" { t.Fatalf("ancestor did not receive both notes; got %v", ancestor) } } // The full three-level chain: grandchild progress must bump BOTH the child's // own report and the root ancestor — this is the depth>=2 case (agent -> // sub-agent -> sub-sub-agent) where every blocked ancestor must stay alive. func TestInstallProgressBridge_ThreeLevelChain(t *testing.T) { var root, mid []string // Level 0 (root agent): has a recorder (report), no parent. rootCtx, rootNotify := InstallProgressBridge(context.Background(), func(n string) { root = append(root, n) }) if rootNotify != nil { t.Fatal("root should have no ancestors") } // Level 1 (child agent): has its own recorder, parent = root. midCtx, midNotify := InstallProgressBridge(rootCtx, func(n string) { mid = append(mid, n) }) if midNotify == nil { t.Fatal("mid should notify root") } // Level 1's own step notifies root only (its own recorder is fed by its // own step observer, not via notifyAncestors). midNotify("mid-step") if len(root) != 1 || root[0] != "mid-step" { t.Fatalf("root missed mid-step; root=%v", root) } // Level 2 (grandchild): no recorder, parent = mid. gcCtx, gcNotify := InstallProgressBridge(midCtx, nil) if gcNotify == nil { t.Fatal("grandchild should notify its ancestors") } // Grandchild's own step must bump BOTH mid (its parent's recorder) and // root (mid forwards upward). gcNotify("gc-step") if len(mid) != 1 || mid[0] != "gc-step" { t.Fatalf("mid missed gc-step; mid=%v", mid) } if len(root) != 2 || root[1] != "gc-step" { t.Fatalf("root missed forwarded gc-step; root=%v", root) } // A descendant installed under gcCtx still reaches mid + root. ProgressSinkFrom(gcCtx)("ggc-step") if len(mid) != 2 || mid[1] != "ggc-step" { t.Fatalf("mid missed ggc-step; mid=%v", mid) } if len(root) != 3 || root[2] != "ggc-step" { t.Fatalf("root missed ggc-step; root=%v", root) } }