okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

dawntui: webhooks on listView: filter, pane, confirm only for destructive actions, add on ctrl+o82b9457

main
21.2 KiB598 linesraw
1//go:build unix
2
3// Acknowledgement loss must not duplicate effects in the real daemon. The
4// proxy waits for admission before cutting the client response, so these
5// scenarios cannot accidentally test a failure that preceded execution.
6package e2e
7
8import (
9	"context"
10	"encoding/json"
11	"errors"
12	"net/http"
13	"slices"
14	"strings"
15	"sync"
16	"sync/atomic"
17
18	tea "charm.land/bubbletea/v2"
19	"testing"
20	"time"
21
22	"albedo/cli/internal/app"
23	"albedo/cli/internal/daemon"
24	"albedo/cli/internal/localdaemon"
25	"albedo/cli/internal/tui"
26)
27
28func TestLostCreationAcknowledgementCreatesOneSession(t *testing.T) {
29	providerRoute(t, echoReply)
30	// Default-changing tests finish before parallel bodies. These creations
31	// send no turns and do not assert which provider the daemon selects.
32	t.Parallel()
33	for _, fault := range []acknowledgementFault{dropAcknowledgement, truncateAcknowledgement, rejectAcknowledgement, emptyAcknowledgement, malformedAcknowledgement, missingAcknowledgement} {
34		t.Run(string(fault), func(t *testing.T) {
35			workspace := t.TempDir()
36			proxy := cutAcknowledgement(t, "/sessions", fault)
37			service := app.Service{Connect: func(context.Context) (*daemon.Connection, error) { return proxy.connection, nil }}
38			opened, err := service.PrepareOpen(context.Background(), app.OpenOptions{Workspace: workspace, Fresh: true})
39			if err != nil {
40				t.Fatal(err)
41			}
42			accepted := proxy.acceptedResponse(t)
43			var created struct {
44				ID string `json:"id"`
45			}
46			if decodeErr := json.Unmarshal(accepted, &created); decodeErr != nil || created.ID == "" {
47				t.Fatalf("accepted session: %s, %v", accepted, decodeErr)
48			}
49			if opened.Selected == nil || opened.Selected.ID != created.ID {
50				t.Fatal("lost creation receipt was not recovered")
51			}
52			sessions, err := daemon.ListSessions(context.Background(), conn(t))
53			if err != nil {
54				t.Fatal(err)
55			}
56			count := 0
57			for _, session := range sessions {
58				if session.Workspace == workspace {
59					count++
60				}
61			}
62			if count != 1 {
63				t.Fatalf("workspace contains %d sessions, want one", count)
64			}
65		})
66	}
67}
68
69func TestRejectedCreationReceiptRecoversWithoutAllocatingSession(t *testing.T) {
70	t.Parallel()
71	workspace := t.TempDir()
72	handle, err := daemon.NewCreation(daemon.CreateSessionRequest{Workspace: workspace, Provider: "missing-creation-profile"})
73	if err != nil {
74		t.Fatal(err)
75	}
76	_, err = daemon.CreateSessionOperation(t.Context(), conn(t), handle)
77	initial, ok := errors.AsType[*daemon.APIError](err)
78	if !ok || initial.StatusCode != http.StatusBadRequest {
79		t.Fatalf("unknown provider did not reject creation: %v", err)
80	}
81	// The creation resource retains its rejection without allocating a session.
82	created, err := daemon.ResolveCreation(t.Context(), conn(t), handle)
83	recovered, ok := errors.AsType[*daemon.APIError](err)
84	if !ok || recovered.StatusCode != initial.StatusCode || created.ID != "" {
85		t.Fatalf("resolve rejected creation: %+v, %v", created, err)
86	}
87	sessions, err := daemon.ListSessions(t.Context(), conn(t))
88	if err != nil {
89		t.Fatal(err)
90	}
91	for _, session := range sessions {
92		if session.Workspace == workspace {
93			t.Fatal("rejected creation allocated a session")
94		}
95	}
96}
97
98func TestInvalidTUICreationAcknowledgementRecoversCreatedSession(t *testing.T) {
99	profile := providerRoute(t, echoReply)
100	// Default-changing tests finish before parallel bodies. These creations
101	// send no turns and do not assert which provider the daemon selects.
102	t.Parallel()
103	for _, fault := range []acknowledgementFault{dropAcknowledgement, emptyAcknowledgement, malformedAcknowledgement, missingAcknowledgement} {
104		t.Run(string(fault), func(t *testing.T) {
105			initial := daemonSession(t, newFaultSession(t, profile))
106			workspace := t.TempDir()
107			proxy := cutAcknowledgement(t, "/sessions", fault)
108			driver := driveTUIWithConnection(t, &initial, proxy.connection)
109			defer driver.App.Chat.Close()
110			result := driver.Send(tui.FolderNewSessionMsg{Workspace: workspace})
111			driver.Update(result)
112			accepted := proxy.acceptedResponse(t)
113			var created struct {
114				ID string `json:"id"`
115			}
116			if decodeErr := json.Unmarshal(accepted, &created); decodeErr != nil || created.ID == "" {
117				t.Fatalf("accepted session: %s, %v", accepted, decodeErr)
118			}
119			if driver.App.ActiveSession == nil || driver.App.ActiveSession.ID != created.ID || driver.App.Chat.Notices.HasError() {
120				t.Fatalf("lost creation response did not recover its created session: %+v", driver.App.Chat.Notices)
121			}
122			count := 0
123			for _, session := range daemonSessions(t) {
124				if session.Workspace == workspace {
125					count++
126				}
127			}
128			if count != 1 {
129				t.Fatalf("TUI creation admitted %d sessions, want one", count)
130			}
131		})
132	}
133}
134
135func TestInvalidSubmissionAcknowledgementSendsOneTurn(t *testing.T) {
136	for _, fault := range []acknowledgementFault{dropAcknowledgement, emptyAcknowledgement, malformedAcknowledgement, missingAcknowledgement, negativeAcknowledgement, wrongStatusAcknowledgement} {
137		t.Run(string(fault), func(t *testing.T) {
138			profile := "invalid-submit-" + string(fault)
139			if err := suite.provider.addProfile(profile, echoReply); err != nil {
140				t.Fatal(err)
141			}
142			id := newFaultSession(t, profile)
143			proxy := cutAcknowledgement(t, "/sessions/"+id+"/inputs/", fault)
144			client := daemon.NewChatClient(proxy.connection, id)
145			result, err := client.Send(context.Background(), "exactly one uncertain turn", nil)
146			if err != nil || result == nil || result.OperationID == "" {
147				t.Fatalf("recover submission: %+v %v", result, err)
148			}
149			proxy.acceptedResponse(t)
150			waitIdle(t, id, profile, 1)
151			if count := len(suite.provider.requests(profile)); count != 1 {
152				t.Fatalf("provider received %d turns, want one", count)
153			}
154			history, err := daemon.NewChatClient(conn(t), id).History(context.Background(), 0, 120)
155			if err != nil {
156				t.Fatal(err)
157			}
158			count := 0
159			for _, event := range history.Events {
160				if event.Type == daemon.EventUser {
161					count++
162				}
163			}
164			if count != 1 {
165				t.Fatalf("transcript contains %d user submissions, want one", count)
166			}
167		})
168	}
169}
170
171func TestInvalidCommandAcknowledgementAddsOneWorkItem(t *testing.T) {
172	profile := providerRoute(t, echoReply)
173	t.Parallel()
174	for _, fault := range []acknowledgementFault{truncateAcknowledgement, emptyAcknowledgement, malformedAcknowledgement, missingAcknowledgement, negativeAcknowledgement, wrongStatusAcknowledgement} {
175		t.Run(string(fault), func(t *testing.T) {
176			id := newFaultSession(t, profile)
177			path := "/extensions/work/items"
178			proxy := cutAcknowledgement(t, path, fault)
179			page, err := daemon.LoadPage(t.Context(), conn(t), id, "/work")
180			if err != nil {
181				t.Fatal(err)
182			}
183			var action *daemon.PageAction
184			for index := range page.Actions {
185				candidate := &page.Actions[index]
186				if candidate.Operation.Method == http.MethodPost && candidate.Operation.PathTemplate == path {
187					action = candidate
188					break
189				}
190			}
191			if action == nil {
192				t.Fatal("work creation action missing")
193			}
194			_, err = daemon.ExecutePageAction(t.Context(), proxy.connection, daemon.PageActionRequest{Action: *action, Session: page.Session, Form: map[string]json.RawMessage{"title": json.RawMessage(`"one uncertain work item"`), "notes": json.RawMessage(`""`), "parent_id": json.RawMessage(`null`)}})
195			proxy.assertSingleEffect(t, err)
196			listed, err := daemon.LoadPage(context.Background(), conn(t), id, "/work")
197			if err != nil {
198				t.Fatal(err)
199			}
200			if len(listed.Rows) != 1 || listed.Rows[0].Text != "one uncertain work item" {
201				t.Fatalf("ledger after lost acknowledgement: %+v", listed)
202			}
203		})
204	}
205}
206
207func TestCancellationAfterAdmissionPreservesUncertainCause(t *testing.T) {
208	profile := providerRoute(t, echoReply)
209	t.Parallel()
210	proxy := cutAcknowledgement(t, "/sessions", cancelAcknowledgement)
211	ctx, cancel := context.WithCancel(context.Background())
212	defer cancel()
213	result := make(chan error, 1)
214	workspace := t.TempDir()
215	go func() {
216		_, err := daemon.CreateSession(ctx, proxy.connection, daemon.CreateSessionRequest{Workspace: workspace, Provider: profile})
217		result <- err
218	}()
219	select {
220	case <-proxy.admitted:
221		cancel()
222	case <-time.After(20 * time.Second):
223		t.Fatal("mutation was not admitted")
224	}
225	select {
226	case err := <-result:
227		proxy.assertSingleEffect(t, err)
228		if !errors.Is(err, context.Canceled) {
229			t.Fatalf("uncertain error lost cancellation cause: %v", err)
230		}
231		uncertain, ok := errors.AsType[*daemon.UncertainOutcomeError](err)
232		if !ok || uncertain.Handle == nil {
233			t.Fatal("cancellation lost the operation handle")
234		}
235		original, resolveErr := daemon.ResolveCreation(t.Context(), proxy.connection, uncertain.Handle)
236		if resolveErr != nil || original.ID == "" {
237			t.Fatalf("resolve original creation: %+v %v", original, resolveErr)
238		}
239		proxy.acceptedResponse(t)
240	case <-time.After(5 * time.Second):
241		t.Fatal("canceled operation did not return")
242	}
243}
244
245func TestLostSignInAcknowledgementStartsOneLocalFlow(t *testing.T) {
246	proxy := cutAcknowledgement(t, "/auth/logins/", dropAcknowledgement)
247	recovered, err := daemon.StartProviderLogin(context.Background(), proxy.connection, "antigravity", "", nil)
248	if err != nil {
249		t.Fatal(err)
250	}
251	accepted := proxy.acceptedResponse(t)
252	var started daemon.StartedSignIn
253	if decodeErr := json.Unmarshal(accepted, &started); decodeErr != nil || started.ID == "" {
254		t.Fatalf("accepted sign-in: %s, %v", accepted, decodeErr)
255	}
256	if recovered.ID != started.ID {
257		t.Fatal("sign-in recovery changed the chosen ID")
258	}
259	t.Cleanup(func() {
260		if cancelErr := daemon.CancelSignIn(context.Background(), conn(t), started.ID); cancelErr != nil {
261			t.Errorf("cancel sign-in: %v", cancelErr)
262		}
263	})
264	status, err := daemon.PollSignIn(context.Background(), conn(t), started.ID)
265	if err != nil || status.State != "waiting" {
266		t.Fatalf("the one admitted local flow: %+v, %v", status, err)
267	}
268	// Starting only allocates a loopback listener and builds an authorization
269	// URL. No callback or pasted code is supplied, so no exchange reaches a provider.
270}
271
272func TestCoreAuthenticationRefusalRecoversBeforeCreation(t *testing.T) {
273	profile := providerRoute(t, echoReply)
274	t.Parallel()
275	snapshot := conn(t).Snapshot()
276	snapshot.Token = "expired-client-token"
277	stale := daemon.NewConnection(snapshot, func(ctx context.Context) (daemon.ConnectionSnapshot, error) {
278		return localdaemon.Rediscover(ctx, suite.home)
279	})
280	t.Cleanup(stale.HTTPClient().CloseIdleConnections)
281	workspace := t.TempDir()
282	created, err := daemon.CreateSession(context.Background(), stale, daemon.CreateSessionRequest{Workspace: workspace, Provider: profile})
283	if err != nil || created.ID == "" {
284		t.Fatalf("creation after pre-admission refusal: %+v, %v", created, err)
285	}
286	if stale.Snapshot().Token != conn(t).Snapshot().Token {
287		t.Fatal("recovery did not update credentials")
288	}
289	sessions, err := daemon.ListSessions(context.Background(), conn(t))
290	if err != nil {
291		t.Fatal(err)
292	}
293	count := 0
294	for _, session := range sessions {
295		if session.Workspace == workspace {
296			count++
297		}
298	}
299	if count != 1 {
300		t.Fatalf("authentication recovery created %d sessions, want one", count)
301	}
302}
303
304func newFaultSession(t *testing.T, profile string) string {
305	t.Helper()
306	created, err := daemon.CreateSession(t.Context(), conn(t), daemon.CreateSessionRequest{Workspace: t.TempDir(), Provider: profile})
307	if err != nil {
308		t.Fatal(err)
309	}
310	return created.ID
311}
312
313func TestWebhookCreationMissingGeneratedSecretStopsAfterOneEffect(t *testing.T) {
314	profile := providerRoute(t, echoReply)
315	id := newFaultSession(t, profile)
316	enableExtension(t, conn(t), id, "webhooks")
317	path := "/extensions/webhooks/hooks"
318	proxy := mutateAcknowledgement(t, path, "", nil, func(body []byte) ([]byte, error) {
319		var envelope map[string]json.RawMessage
320		if err := json.Unmarshal(body, &envelope); err != nil {
321			return nil, err
322		}
323		if _, present := envelope["secret"]; !present {
324			return nil, errors.New("daemon did not generate a secret")
325		}
326		delete(envelope, "secret")
327		return json.Marshal(envelope)
328	})
329	session := daemonSession(t, id)
330	driver := driveTUIWithConnection(t, &session, proxy.connection)
331	defer driver.App.Chat.Close()
332	driver.Dispatch(tui.ChatOpenWebhooksPageMsg{})
333	if !driver.App.WebhooksPage.Loaded || driver.App.WebhooksPage.Error != "" {
334		t.Fatalf("open creation screen: %s", driver.App.WebhooksPage.Error)
335	}
336	driver.Dispatch(tea.KeyPressMsg{Code: 'o', Mod: tea.ModCtrl})
337	driver.Type("missing-secret")
338	// Creation installs the signature atomically even when its secret acknowledgment is lost.
339	driver.App.WebhooksPage.Form.Inputs["header"].SetValue("x-custom-signature")
340	proxy.mu.Lock()
341	before := len(proxy.requests)
342	proxy.mu.Unlock()
343	driver.Dispatch(tea.KeyPressMsg{Code: 's', Mod: tea.ModCtrl})
344	accepted := proxy.acceptedResponse(t)
345	page := driver.App.WebhooksPage
346	if page.Saving || page.Loading || page.Form == nil || page.Error == "" || page.Notice != "" || page.Reveal != nil {
347		t.Fatalf("uncertain creation state: %+v", page)
348	}
349	proxy.mu.Lock()
350	requests := slices.Clone(proxy.requests[before:])
351	proxy.mu.Unlock()
352	if !slices.Equal(requests, []string{"POST " + path}) {
353		t.Fatalf("creation replayed, configured a signature, or refreshed: %v", requests)
354	}
355	var original struct {
356		Resource struct {
357			ETag  string `json:"etag"`
358			Value struct {
359				ID string `json:"id"`
360			} `json:"value"`
361		} `json:"resource"`
362	}
363	if err := json.Unmarshal(accepted, &original); err != nil {
364		t.Fatal(err)
365	}
366	t.Cleanup(func() {
367		if _, err := daemon.DeleteWebhook(context.Background(), conn(t), original.Resource.Value.ID, original.Resource.ETag); err != nil {
368			t.Error(err)
369		}
370	})
371	listed, err := daemon.ListWebhooks(t.Context(), conn(t), id)
372	if err != nil {
373		t.Fatal(err)
374	}
375	count := 0
376	for _, entry := range listed {
377		if entry.Hook.Session == id && entry.Hook.Name == "missing-secret" {
378			count++
379			if entry.Hook.Header != "x-custom-signature" {
380				t.Fatal("atomic signature configuration was lost")
381			}
382		}
383	}
384	if count != 1 {
385		t.Fatalf("persisted %d hooks, want one", count)
386	}
387}
388
389func TestUnknownReceiptReplaysOriginalCreationOnce(t *testing.T) {
390	providerRoute(t, echoReply)
391	// Default-changing tests finish before parallel bodies. These creations
392	// send no turns and do not assert which provider the daemon selects.
393	t.Parallel()
394	workspace := t.TempDir()
395	proxy := cutAcknowledgement(t, "/sessions", hideReceiptAcknowledgement)
396	handle, err := daemon.NewCreation(daemon.CreateSessionRequest{Workspace: workspace})
397	if err != nil {
398		t.Fatal(err)
399	}
400	created, err := daemon.CreateSessionOperation(t.Context(), proxy.connection, handle)
401	if err != nil {
402		t.Fatal(err)
403	}
404	proxy.mu.Lock()
405	responses := slices.Clone(proxy.responses)
406	proxy.mu.Unlock()
407	if len(responses) != 2 {
408		t.Fatalf("expected the original PUT and one conditional retry, got %d", len(responses))
409	}
410	requests := operationRequests(proxy)
411	if len(requests) != 4 || requests[0] != requests[2] || requests[1] != requests[3] {
412		t.Fatalf("retry changed the original resource: %v", requests)
413	}
414	count := 0
415	for _, session := range daemonSessions(t) {
416		if session.Workspace == workspace {
417			count++
418		}
419	}
420	if count != 1 {
421		t.Fatalf("replay created %d sessions", count)
422	}
423	resolved, err := daemon.ResolveCreation(t.Context(), proxy.connection, handle)
424	if err != nil || resolved.ID != created.ID {
425		t.Fatalf("original creation: %+v %v", resolved, err)
426	}
427}
428
429func TestQueuedPromptDeadlineCancelsOnlyItsOperation(t *testing.T) {
430	entered, release := make(chan struct{}), make(chan struct{})
431	released := false
432	defer func() {
433		if !released {
434			close(release)
435		}
436	}()
437	profile := providerRoute(t, func(request map[string]any) string {
438		if lastUserText(request) == "active" {
439			close(entered)
440			<-release
441		}
442		return echoReply(request)
443	})
444	t.Parallel()
445	id := newSession(t, t.TempDir())
446	if _, err := daemon.NewChatClient(conn(t), id).Send(t.Context(), "active", nil); err != nil {
447		t.Fatal(err)
448	}
449	select {
450	case <-entered:
451	case <-time.After(10 * time.Second):
452		t.Fatal("active turn never started")
453	}
454	connection, attachErr := daemon.Attach(t.Context(), conn(t).Snapshot(), nil)
455	if attachErr != nil {
456		t.Fatal(attachErr)
457	}
458	counter := &submissionCounterTransport{next: connection.HTTPClient().Transport, accepted: make(chan struct{})}
459	connection.HTTPClient().Transport = counter
460	t.Cleanup(connection.HTTPClient().CloseIdleConnections)
461	service := app.Service{Connect: func(context.Context) (*daemon.Connection, error) { return connection, nil }}
462	finished := make(chan error, 1)
463	go func() {
464		_, err := service.RunPrompt(t.Context(), app.PromptOptions{SessionID: id, Prompt: "waiting", Timeout: 2 * time.Second})
465		finished <- err
466	}()
467	select {
468	case <-counter.accepted:
469	case err := <-finished:
470		t.Fatalf("prompt ended before durable acceptance: %v", err)
471	case <-time.After(5 * time.Second):
472		t.Fatal("queued prompt was not admitted")
473	}
474	err := <-finished
475	uncertain, ok := errors.AsType[*daemon.UncertainOutcomeError](err)
476	if !ok || uncertain.Handle == nil || !errors.Is(err, context.DeadlineExceeded) {
477		t.Fatalf("deadline lost operation identity: %v", err)
478	}
479	receipt, err := daemon.ResolveOperation(t.Context(), connection, uncertain.Handle)
480	if err != nil || receipt.Delivery == nil || *receipt.Delivery != "cancelled" {
481		t.Fatalf("queued receipt after deadline: %+v %v", receipt, err)
482	}
483	if counter.posts.Load() != 1 {
484		t.Fatal("query recovery submitted another intent")
485	}
486	close(release)
487	released = true
488	waitIdle(t, id, profile, 1)
489	receipt, err = daemon.ResolveOperation(t.Context(), connection, uncertain.Handle)
490	if err != nil || receipt.Delivery == nil || *receipt.Delivery != "cancelled" {
491		t.Fatalf("cancelled receipt after unrelated turn: %+v %v", receipt, err)
492	}
493	if counter.posts.Load() != 1 {
494		t.Fatal("query recovery submitted another intent")
495	}
496	history, err := daemon.NewChatClient(conn(t), id).History(t.Context(), 0, 120)
497	if err != nil {
498		t.Fatal(err)
499	}
500	matched := 0
501	for _, event := range history.Events {
502		if event.Type == daemon.EventUser && event.OperationID == uncertain.Handle.ID() {
503			matched++
504		}
505	}
506	if matched != 0 {
507		t.Fatalf("cancelled deadline operation delivered %d times", matched)
508	}
509}
510
511// Count the real client's submissions while leaving its SSE stream unbuffered.
512type submissionCounterTransport struct {
513	next       http.RoundTripper
514	posts      atomic.Int32
515	accepted   chan struct{}
516	acceptOnce sync.Once
517}
518
519func (transport *submissionCounterTransport) RoundTrip(request *http.Request) (*http.Response, error) {
520	if request.Method == http.MethodPut && strings.Contains(request.URL.Path, "/inputs/") {
521		transport.posts.Add(1)
522	}
523	response, err := transport.next.RoundTrip(request)
524	if err == nil && request.Method == http.MethodPut && strings.Contains(request.URL.Path, "/inputs/") && response.StatusCode == http.StatusAccepted {
525		transport.acceptOnce.Do(func() { close(transport.accepted) })
526	}
527	return response, err
528}
529func (transport *submissionCounterTransport) CloseIdleConnections() {
530	if closer, ok := transport.next.(interface{ CloseIdleConnections() }); ok {
531		closer.CloseIdleConnections()
532	}
533}
534
535func TestTUICreationRecoveryRetainsSelectedSession(t *testing.T) {
536	profile := providerRoute(t, echoReply)
537	// Default-changing tests finish before parallel bodies. These creations
538	// send no turns and do not assert which provider the daemon selects.
539	t.Parallel()
540	initial := daemonSession(t, newFaultSession(t, profile))
541	other := daemonSession(t, newFaultSession(t, profile))
542	workspace := t.TempDir()
543	proxy := cutAcknowledgement(t, "/sessions", unresolvedAcknowledgement)
544	driver := driveTUIWithConnection(t, &initial, proxy.connection)
545	t.Cleanup(func() { driver.App.Chat.Close() })
546	uncertain := driver.Send(tui.FolderNewSessionMsg{Workspace: workspace})
547	driver.Update(tui.FolderOpenSessionMsg{Session: other})
548	recovery := driver.Update(uncertain)
549	if recovery == nil {
550		t.Fatal("navigation discarded uncertain creation recovery")
551	}
552	proxy.allowReceipt.Store(true)
553	for _, recoveryRequest := range driver.results(recovery) {
554		driver.Update(driver.Send(recoveryRequest))
555	}
556	if driver.App.ActiveSession == nil || driver.App.ActiveSession.ID != other.ID {
557		t.Fatal("recovering older creation replaced the user's selected session")
558	}
559	count := 0
560	for _, session := range driver.App.Sessions {
561		if session.Workspace == workspace {
562			count++
563		}
564	}
565	if count != 1 {
566		t.Fatalf("navigation recovered %d created sessions", count)
567	}
568	proxy.mu.Lock()
569	responses := slices.Clone(proxy.responses)
570	proxy.mu.Unlock()
571	var first struct {
572		ID string `json:"id"`
573	}
574	var replay struct {
575		Decision struct {
576			SessionID  string `json:"session_id"`
577			Admission  string `json:"admission"`
578			HTTPStatus int    `json:"http_status"`
579		} `json:"decision"`
580	}
581	if len(responses) != 2 || json.Unmarshal(responses[0], &first) != nil || first.ID == "" || json.Unmarshal(responses[1], &replay) != nil || replay.Decision.SessionID != first.ID || replay.Decision.Admission != "accepted" || replay.Decision.HTTPStatus != 201 {
582		t.Fatalf("creation retry lost its original accepted decision: %d responses, identity=%q replay=%+v", len(responses), first.ID, replay.Decision)
583	}
584	if recovered, err := daemon.GetSession(t.Context(), conn(t), first.ID); err != nil || recovered.Workspace != workspace {
585		t.Fatalf("recovery lost created session: %v", err)
586	}
587	count = 0
588	for _, session := range daemonSessions(t) {
589		if session.Workspace == workspace {
590			count++
591		}
592	}
593	if count != 1 {
594		t.Fatalf("creation uncertainty produced %d durable sessions", count)
595	}
596}
597
598// Receipt fault tests also navigate sessions, whose event streams must remain live.