Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
kind: internal
pr: 1377
title: the live standing check returns to the conversation that receives the firing
surface: [chat, remote]
invalidates:
- "The live standing acceptance pressed `ctrl+t` while Home still selected the settled reminder exchange and assumed it had opened a chat. That chord acts only on a conversation row there, so the test stayed on Home and reported a delivered `said:` row as missing. It now reopens the existing conversation and proves that transition before waiting for the firing."
---

The standing event already crossed the hosted task-update wire, remained visible
through its reply, and reached the conversation exactly once. Focused tests now
pin those two boundaries directly.

The end-to-end scenario moves from the settled reminder row onto its existing
conversation and waits for that conversation's footer before the five-minute
standing pass. Both the attended firing and the later closed-window drain are
read from the surfaces where a person actually receives them.
12 changes: 8 additions & 4 deletions internal/e2e/tui_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -765,10 +765,14 @@ func testFiringReachesThePerson(t *testing.T) {

// Back into the conversation and wait. The window runs the same pass the
// timer runs, every standing.Interval (five minutes), the first one an
// interval after launch. Answering the card already handed the keyboard
// back to the list; the new-chat chord opens a conversation composer.
r.keys("C-t")
time.Sleep(2500 * time.Millisecond)
// interval after launch. Answering the card hands the keyboard back to its
// settled exchange row, immediately below the conversation we opened. Walk
// onto that conversation and open it: ctrl+t acts only on a conversation
// row, so sending it from the exchange row silently left this test on Home
// while it looked there for a conversation-only firing row (#1344).
r.keys("Up")
r.keys("Enter")
r.waitFor(20*time.Second, say(t, "homeDoorWord"))

// /status, while something stands: the derived `keeping watch` line, and the
// `◦ 1 standing order` line at the foot of the task column. That count was a
Expand Down
58 changes: 58 additions & 0 deletions internal/remote/tasklane_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"time"

"github.com/Agent-Field/codeaf/internal/session"
"github.com/Agent-Field/codeaf/internal/standing"
)

// railAgent is a [fakeAgent] that carries the standing task lane the real
Expand Down Expand Up @@ -157,6 +158,63 @@ func TestAHandStartedTaskReachesTheHostedRail(t *testing.T) {
}
}

// A FIRING USES THE REAL SESSION LANE OVER THE REAL WIRE. The scripted rail
// above proves that a task-shaped event can cross; this is the other producer
// of that lane, whose event has no task payload and is raised outside a turn.
func TestAStandingFiringReachesTheHostedConversation(t *testing.T) {
workspace := t.TempDir()
far, err := session.New(session.Config{
Workspace: workspace,
Model: "stub/standing-wire",
APIKey: "fixture",
BaseURL: "http://127.0.0.1:1/v1",
System: "Test only.",
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = far.Close() })

loop, err := Loopback(Hello{Version: Version}, Options{Boot: func(Hello) (*Engine, error) {
return &Engine{Agent: far, Workspace: workspace}, nil
}})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = loop.Close() })

lane, stop := loop.Client.Agent().WatchTaskUpdates()
t.Cleanup(stop)
// WatchTaskUpdates asks asynchronously so the surface loop never waits on a
// round trip. This synchronous repeat is the test's receipt that the far
// subscription exists before the firing; replacing it is the door's normal
// idempotent behaviour.
if _, err := loop.Client.call(context.Background(), MethodTaskWatch, nil); err != nil {
t.Fatalf("open the hosted standing lane: %v", err)
}

item := standing.Item{
ID: "water",
Words: "remind me in 1 minute to drink water",
Workspace: workspace,
}
runner := session.NewStandingRunner(session.Config{}, t.TempDir())
if _, err := runner.Say(context.Background(), item, "Time to drink water!"); err != nil {
t.Fatalf("fire the standing item: %v", err)
}

event := nextTask(t, lane)
if event.Kind != session.EventStandingUpdate || event.Standing == nil {
t.Fatalf("the lane carried %v, not a standing update", event.Kind)
}
if event.Standing.Item.ID != item.ID || event.Standing.Item.Words != item.Words {
t.Fatalf("the hosted row names %+v, want %+v", event.Standing.Item, item)
}
if event.Standing.Text != "Time to drink water!" {
t.Fatalf("the hosted row says %q", event.Standing.Text)
}
}

// A window that arrives after the work started still learns every row, because
// the engine's subscription replays the roster as it opens.
func TestAHostedRailOpenedLateStillGetsTheRoster(t *testing.T) {
Expand Down
57 changes: 57 additions & 0 deletions internal/tui3/standing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,63 @@ func TestAFiringOffTheStandingLaneIsDrawnInTheConversation(t *testing.T) {
}
}

// firingReplyAgent carries both lanes involved in a live firing: the standing
// event that draws the news and the turn the session wakes to answer it.
type firingReplyAgent struct {
*taskFake
wakes chan (<-chan session.Event)
}

func (f *firingReplyAgent) Wakes() <-chan (<-chan session.Event) { return f.wakes }

// THE REPLY MAY FOLD ITS OWN WORK AND NEVER THE NEWS THAT WOKE IT. The row is
// present before the first reply event and remains exactly once after the turn
// settles, which pins both halves of the ordering contract.
func TestAStandingFiringStaysDrawnAfterItsReply(t *testing.T) {
agent := &firingReplyAgent{
taskFake: &taskFake{
fakeAgent: &fakeAgent{model: "m"},
updates: make(chan session.Event, 8),
},
wakes: make(chan (<-chan session.Event), 1),
}
a := newTestApp(agent)
a.width, a.height = 120, 24
tasks, wakes := a.watchTasks(), a.watchWakes()
if tasks == nil || wakes == nil {
t.Fatal("the surface did not open both standing firing lanes")
}

const words = "remind me in 1 minute to drink water"
row := standName(words) + " · said: Time to drink water!"
agent.updates <- session.Event{Kind: session.EventStandingUpdate, Standing: &session.StandingNotice{
Item: standing.Item{
ID: "water",
Words: words,
},
Update: "fired",
Text: "Time to drink water!",
}}
drive(t, a, runCmd(tasks)...)
if body := standText(a); strings.Count(body, row) != 1 {
t.Fatalf("before the reply the firing row occurs %d times, want once:\n%s", strings.Count(body, row), body)
}

agent.wakes <- woken(
text(session.EventTextDelta, "Drink some water now."),
session.Event{Kind: session.EventTurnDone},
)
drive(t, a, runCmd(wakes)...)
drive(t, a, frameMsg{})
body := standText(a)
if !strings.Contains(body, "Drink some water now.") {
t.Fatalf("the firing's reply was not drawn:\n%s", body)
}
if got := strings.Count(body, row); got != 1 {
t.Fatalf("after the reply the firing row occurs %d times, want once:\n%s", got, body)
}
}

// AND A RUN THAT STOPPED ON SOMEBODY WEARS THE ACCENT, on the same lane and
// with no turn to carry it either.
func TestAFiringThatNeedsSomebodyIsDrawnWithTheAskGlyph(t *testing.T) {
Expand Down
Loading