version_bump_test.go

  1package chat
  2
  3import (
  4	"testing"
  5	"time"
  6
  7	"github.com/charmbracelet/crush/internal/config"
  8	"github.com/charmbracelet/crush/internal/message"
  9	"github.com/charmbracelet/crush/internal/ui/anim"
 10	"github.com/charmbracelet/crush/internal/ui/attachments"
 11	"github.com/charmbracelet/crush/internal/ui/list"
 12	"github.com/charmbracelet/crush/internal/ui/styles"
 13	"github.com/stretchr/testify/require"
 14)
 15
 16// versionedItem is the cross-cutting interface every chat item type
 17// must satisfy under F6: every documented mutator must bump the
 18// shared version counter so the list-level memo invalidates.
 19type versionedItem interface {
 20	list.Item
 21	Version() uint64
 22}
 23
 24// requireBump asserts that the supplied mutator advances the item's
 25// Version(). The mutator runs once; an absent bump is a regression
 26// (a finished item would keep serving stale frozen output to the
 27// list cache).
 28func requireBump(t *testing.T, name string, item versionedItem, mutate func()) {
 29	t.Helper()
 30	before := item.Version()
 31	mutate()
 32	after := item.Version()
 33	require.Greaterf(t, after, before, "%s must bump Version() (before=%d, after=%d)", name, before, after)
 34}
 35
 36// TestAssistantMessageItem_MutatorsBumpVersion enumerates every
 37// documented mutator on AssistantMessageItem and asserts each one
 38// advances Version().
 39func TestAssistantMessageItem_MutatorsBumpVersion(t *testing.T) {
 40	t.Parallel()
 41
 42	sty := styles.CharmtonePantera()
 43	build := func(thinking, content string) *message.Message {
 44		parts := []message.ContentPart{
 45			message.ReasoningContent{
 46				Thinking:   thinking,
 47				StartedAt:  testStartedAt,
 48				FinishedAt: testFinishedAt,
 49			},
 50		}
 51		if content != "" {
 52			parts = append(parts, message.TextContent{Text: content})
 53		}
 54		return &message.Message{ID: "a-mut", Role: message.Assistant, Parts: parts}
 55	}
 56
 57	item := NewAssistantMessageItem(&sty, build("thinking", "content")).(*AssistantMessageItem)
 58
 59	requireBump(t, "SetMessage", item, func() {
 60		item.SetMessage(build("thinking", "more content"))
 61	})
 62	requireBump(t, "SetFocused", item, func() {
 63		item.SetFocused(true)
 64	})
 65	requireBump(t, "SetHighlight", item, func() {
 66		item.SetHighlight(0, 0, 0, 5)
 67	})
 68	// ToggleExpanded only mutates state when there is non-empty
 69	// thinking text โ€” which the build helper provides.
 70	requireBump(t, "ToggleExpanded", item, func() {
 71		item.ToggleExpanded()
 72	})
 73}
 74
 75// TestUserMessageItem_MutatorsBumpVersion enumerates UserMessageItem
 76// mutators.
 77func TestUserMessageItem_MutatorsBumpVersion(t *testing.T) {
 78	t.Parallel()
 79
 80	sty := styles.CharmtonePantera()
 81	r := attachments.NewRenderer(
 82		sty.Attachments.Normal,
 83		sty.Attachments.Deleting,
 84		sty.Attachments.Image,
 85		sty.Attachments.Text,
 86		sty.Attachments.Skill,
 87	)
 88	msg := &message.Message{
 89		ID:   "u-mut",
 90		Role: message.User,
 91		Parts: []message.ContentPart{
 92			message.TextContent{Text: "Hello"},
 93		},
 94	}
 95	item := NewUserMessageItem(&sty, msg, r).(*UserMessageItem)
 96
 97	requireBump(t, "SetFocused", item, func() {
 98		item.SetFocused(true)
 99	})
100	requireBump(t, "SetHighlight", item, func() {
101		item.SetHighlight(0, 0, 0, 3)
102	})
103}
104
105// TestAssistantInfoItem_VersionedAndFinished sanity-checks the
106// AssistantInfoItem wiring. The item carries only immutable data
107// after construction; we still assert Version() is callable and
108// Finished() returns true.
109func TestAssistantInfoItem_VersionedAndFinished(t *testing.T) {
110	t.Parallel()
111
112	sty := styles.CharmtonePantera()
113	cfg := &config.Config{}
114	msg := &message.Message{
115		ID:    "info",
116		Role:  message.Assistant,
117		Parts: []message.ContentPart{message.Finish{Reason: message.FinishReasonEndTurn, Time: time.Now().Unix()}},
118	}
119	item := NewAssistantInfoItem(&sty, msg, cfg, time.Unix(0, 0)).(*AssistantInfoItem)
120
121	require.True(t, item.Finished(), "AssistantInfoItem must be Finished()")
122	// Version() is callable and starts at zero.
123	require.Equal(t, uint64(0), item.Version())
124}
125
126// TestBaseToolMessageItem_MutatorsBumpVersion enumerates the base
127// tool item mutators. Specific tool types layer on top of this
128// base; the base bumps cover the shared mutator surface.
129func TestBaseToolMessageItem_MutatorsBumpVersion(t *testing.T) {
130	t.Parallel()
131
132	sty := styles.CharmtonePantera()
133	tc := message.ToolCall{ID: "tc1", Name: "bash", Input: "{}", Finished: false}
134	item := NewToolMessageItem(&sty, "msg", tc, nil, false)
135
136	v := item.(versionedItem)
137
138	requireBump(t, "SetFocused", v, func() {
139		if f, ok := item.(list.Focusable); ok {
140			f.SetFocused(true)
141		}
142	})
143	requireBump(t, "SetHighlight", v, func() {
144		if h, ok := item.(list.Highlightable); ok {
145			h.SetHighlight(0, 0, 0, 3)
146		}
147	})
148	requireBump(t, "SetToolCall", v, func() {
149		tc2 := tc
150		tc2.Input = `{"command":"echo"}`
151		item.SetToolCall(tc2)
152	})
153	requireBump(t, "SetResult", v, func() {
154		item.SetResult(&message.ToolResult{ToolCallID: "tc1", Content: "ok"})
155	})
156	requireBump(t, "SetStatus", v, func() {
157		item.SetStatus(ToolStatusSuccess)
158	})
159	requireBump(t, "ToggleExpanded", v, func() {
160		if e, ok := item.(Expandable); ok {
161			e.ToggleExpanded()
162		}
163	})
164	requireBump(t, "SetCompact", v, func() {
165		if c, ok := item.(Compactable); ok {
166			c.SetCompact(true)
167		}
168	})
169}
170
171// TestAssistantMessageItem_AnimateBumpsVersion covers the spinner
172// regression: while the assistant message is spinning, every
173// anim.StepMsg fed through Animate must bump Version() so the
174// list-level cache invalidates and the next draw re-renders the
175// advanced spinner frame. Without this bump the cached entry's
176// version stays put and the spinner appears frozen.
177func TestAssistantMessageItem_AnimateBumpsVersion(t *testing.T) {
178	t.Parallel()
179
180	sty := styles.CharmtonePantera()
181	streaming := &message.Message{
182		ID:   "spin",
183		Role: message.Assistant,
184		Parts: []message.ContentPart{
185			message.ReasoningContent{Thinking: "thinking..."},
186		},
187	}
188	item := NewAssistantMessageItem(&sty, streaming).(*AssistantMessageItem)
189
190	requireBump(t, "Animate", item, func() {
191		item.Animate(anim.StepMsg{})
192	})
193
194	// A non-spinning item must not bump on Animate: the bump only
195	// makes sense while the spinner is live, and a stray bump on a
196	// finished item would needlessly invalidate frozen entries.
197	finished := &message.Message{
198		ID:   "spin",
199		Role: message.Assistant,
200		Parts: []message.ContentPart{
201			message.TextContent{Text: "done"},
202			message.Finish{Reason: message.FinishReasonEndTurn, Time: testFinishTime},
203		},
204	}
205	item.SetMessage(finished)
206	require.True(t, item.Finished(), "item must report Finished() once the message finishes")
207	before := item.Version()
208	item.Animate(anim.StepMsg{})
209	require.Equal(t, before, item.Version(), "Animate must not bump Version() on a non-spinning item")
210}
211
212// TestAssistantMessageItem_FinishedTransition covers ยง4.5.1: a
213// streaming assistant message reports Finished() == false; once the
214// message reports IsFinished() and stops spinning, Finished() must
215// return true.
216func TestAssistantMessageItem_FinishedTransition(t *testing.T) {
217	t.Parallel()
218
219	sty := styles.CharmtonePantera()
220
221	// Streaming: no finish part, no content yet โ€” isSpinning == true.
222	streaming := &message.Message{
223		ID:   "stream",
224		Role: message.Assistant,
225		Parts: []message.ContentPart{
226			message.ReasoningContent{Thinking: "thinking..."},
227		},
228	}
229	item := NewAssistantMessageItem(&sty, streaming).(*AssistantMessageItem)
230	require.False(t, item.Finished(), "streaming assistant message must not be Finished()")
231
232	// Finished with content.
233	finished := &message.Message{
234		ID:   "stream",
235		Role: message.Assistant,
236		Parts: []message.ContentPart{
237			message.ReasoningContent{Thinking: "thinking", StartedAt: testStartedAt, FinishedAt: testFinishedAt},
238			message.TextContent{Text: "the answer"},
239			message.Finish{Reason: message.FinishReasonEndTurn, Time: testFinishTime},
240		},
241	}
242	item.SetMessage(finished)
243	require.True(t, item.Finished(), "finished assistant message must be Finished()")
244}
245
246// TestUserMessageItem_FinishedAlwaysTrue locks in the freezable
247// contract: user messages are never spinning.
248func TestUserMessageItem_FinishedAlwaysTrue(t *testing.T) {
249	t.Parallel()
250
251	sty := styles.CharmtonePantera()
252	r := attachments.NewRenderer(
253		sty.Attachments.Normal,
254		sty.Attachments.Deleting,
255		sty.Attachments.Image,
256		sty.Attachments.Text,
257		sty.Attachments.Skill,
258	)
259	msg := &message.Message{
260		ID:    "u-fin",
261		Role:  message.User,
262		Parts: []message.ContentPart{message.TextContent{Text: "hi"}},
263	}
264	item := NewUserMessageItem(&sty, msg, r).(*UserMessageItem)
265	require.True(t, item.Finished())
266}
267
268// TestAgentToolMessageItem_NestedToolMutatorsBumpVersion covers B1:
269// the nested-tool mutators on AgentToolMessageItem must bump
270// Version() so the list cache invalidates frozen entries when a
271// nested tool is added or the slice changes. SetNestedTools always
272// bumps unconditionally โ€” the live update path in
273// internal/ui/model/ui.go mutates existing children in place and
274// then re-passes the same slice, so a pointer-equality dedupe would
275// hide observable child-render changes. AddNestedTool also always
276// observably mutates state and always bumps.
277func TestAgentToolMessageItem_NestedToolMutatorsBumpVersion(t *testing.T) {
278	t.Parallel()
279
280	sty := styles.CharmtonePantera()
281	parent := message.ToolCall{ID: "agent-parent", Name: "agent", Input: `{}`, Finished: false}
282	item := NewAgentToolMessageItem(&sty, parent, nil, false)
283
284	mkChild := func(id string) ToolMessageItem {
285		tc := message.ToolCall{ID: id, Name: "bash", Input: `{}`, Finished: false}
286		return NewToolMessageItem(&sty, "msg", tc, nil, false)
287	}
288
289	// AddNestedTool always bumps.
290	requireBump(t, "AddNestedTool", item, func() {
291		item.AddNestedTool(mkChild("c1"))
292	})
293
294	// SetNestedTools always bumps, even with a pointer-equal slice.
295	current := append([]ToolMessageItem(nil), item.NestedTools()...)
296	requireBump(t, "SetNestedTools[pointer-equal]", item, func() {
297		item.SetNestedTools(current)
298	})
299
300	// SetNestedTools with a different slice (extra element) bumps.
301	requireBump(t, "SetNestedTools[different]", item, func() {
302		item.SetNestedTools(append(current, mkChild("c2")))
303	})
304
305	// SetNestedTools to an empty slice from a non-empty state bumps.
306	requireBump(t, "SetNestedTools[empty]", item, func() {
307		item.SetNestedTools(nil)
308	})
309}
310
311// TestAgenticFetchToolMessageItem_NestedToolMutatorsBumpVersion is
312// the agentic-fetch counterpart to the agent-tool nested mutator
313// bump test above.
314func TestAgenticFetchToolMessageItem_NestedToolMutatorsBumpVersion(t *testing.T) {
315	t.Parallel()
316
317	sty := styles.CharmtonePantera()
318	parent := message.ToolCall{ID: "fetch-parent", Name: "agentic_fetch", Input: `{}`, Finished: false}
319	item := NewAgenticFetchToolMessageItem(&sty, parent, nil, false)
320
321	mkChild := func(id string) ToolMessageItem {
322		tc := message.ToolCall{ID: id, Name: "fetch", Input: `{}`, Finished: false}
323		return NewToolMessageItem(&sty, "msg", tc, nil, false)
324	}
325
326	requireBump(t, "AddNestedTool", item, func() {
327		item.AddNestedTool(mkChild("c1"))
328	})
329
330	current := append([]ToolMessageItem(nil), item.NestedTools()...)
331	requireBump(t, "SetNestedTools[pointer-equal]", item, func() {
332		item.SetNestedTools(current)
333	})
334
335	requireBump(t, "SetNestedTools[different]", item, func() {
336		item.SetNestedTools(append(current, mkChild("c2")))
337	})
338
339	requireBump(t, "SetNestedTools[empty]", item, func() {
340		item.SetNestedTools(nil)
341	})
342}
343
344// TestAgentToolMessageItem_NestedChildInPlaceMutationBumpsParent is
345// the T5 regression test: it mirrors the live update flow at
346// internal/ui/model/ui.go:1242-1281 where nested tool calls are
347// updated in place (SetToolCall / SetResult on the same child
348// pointers) and then the same slice is handed back to the parent
349// via SetNestedTools. The parent must still bump its version so
350// the list cache invalidates the parent's pre-rendered string and
351// the freshly-rendered child output becomes visible.
352func TestAgentToolMessageItem_NestedChildInPlaceMutationBumpsParent(t *testing.T) {
353	t.Parallel()
354
355	sty := styles.CharmtonePantera()
356	parent := message.ToolCall{ID: "agent-parent", Name: "agent", Input: `{}`, Finished: false}
357	item := NewAgentToolMessageItem(&sty, parent, nil, false)
358
359	childTC := message.ToolCall{ID: "c1", Name: "bash", Input: `{}`, Finished: false}
360	child := NewToolMessageItem(&sty, "msg", childTC, nil, false)
361	item.AddNestedTool(child)
362
363	v0 := item.Version()
364	childVersionBefore := child.(versionedItem).Version()
365
366	// In-place mutate the existing child, exactly like the live
367	// flow in ui.go:1271-1278 does.
368	child.SetResult(&message.ToolResult{ToolCallID: "c1", Content: "ok"})
369	require.Greaterf(t, child.(versionedItem).Version(), childVersionBefore,
370		"child SetResult must bump child version")
371
372	// Hand the same slice back to the parent (pointers unchanged).
373	same := item.NestedTools()
374	item.SetNestedTools(same)
375	require.Greaterf(t, item.Version(), v0,
376		"parent SetNestedTools must bump even when child pointers are unchanged (in-place child mutation invalidates parent's pre-rendered output)")
377}
378
379// TestAgenticFetchToolMessageItem_NestedChildInPlaceMutationBumpsParent
380// is the agentic-fetch counterpart of the T5 regression test.
381func TestAgenticFetchToolMessageItem_NestedChildInPlaceMutationBumpsParent(t *testing.T) {
382	t.Parallel()
383
384	sty := styles.CharmtonePantera()
385	parent := message.ToolCall{ID: "fetch-parent", Name: "agentic_fetch", Input: `{}`, Finished: false}
386	item := NewAgenticFetchToolMessageItem(&sty, parent, nil, false)
387
388	childTC := message.ToolCall{ID: "c1", Name: "fetch", Input: `{}`, Finished: false}
389	child := NewToolMessageItem(&sty, "msg", childTC, nil, false)
390	item.AddNestedTool(child)
391
392	v0 := item.Version()
393	childVersionBefore := child.(versionedItem).Version()
394
395	child.SetResult(&message.ToolResult{ToolCallID: "c1", Content: "ok"})
396	require.Greaterf(t, child.(versionedItem).Version(), childVersionBefore,
397		"child SetResult must bump child version")
398
399	same := item.NestedTools()
400	item.SetNestedTools(same)
401	require.Greaterf(t, item.Version(), v0,
402		"parent SetNestedTools must bump even when child pointers are unchanged")
403}
404
405// requireNoBump asserts the supplied mutator leaves the item's
406// Version() unchanged. The mutator runs once; an unexpected bump
407// would force the F6 list memo to re-render an item whose output
408// did not change, churning the cache.
409func requireNoBump(t *testing.T, name string, item versionedItem, mutate func()) {
410	t.Helper()
411	before := item.Version()
412	mutate()
413	after := item.Version()
414	require.Equalf(t, before, after,
415		"%s must not bump Version() (before=%d, after=%d)", name, before, after)
416}
417
418// TestBaseToolMessageItem_AnimateBumpsVersion is the spinner
419// regression test for non-agent tools: while the tool is spinning,
420// every anim.StepMsg whose ID matches the tool must bump Version()
421// so the list-level cache invalidates and the next draw re-renders
422// the advanced spinner frame. Foreign IDs must not bump (they would
423// churn the cache on every frame), and a finished tool must not
424// bump on any ID (the entry is frozen and stays frozen).
425func TestBaseToolMessageItem_AnimateBumpsVersion(t *testing.T) {
426	t.Parallel()
427
428	sty := styles.CharmtonePantera()
429	tc := message.ToolCall{ID: "tc-spin", Name: "bash", Input: "{}", Finished: false}
430	item := NewToolMessageItem(&sty, "msg", tc, nil, false)
431	v := item.(versionedItem)
432	a, ok := item.(Animatable)
433	require.True(t, ok, "base tool message item must implement Animatable")
434
435	// Spinning + matching ID โ†’ bump.
436	requireBump(t, "Animate[spinning,own ID]", v, func() {
437		a.Animate(anim.StepMsg{ID: tc.ID})
438	})
439
440	// Spinning + foreign ID โ†’ no bump. Routing this StepMsg here at
441	// all would mean a future chat.Animate refactor; the item must
442	// be defensive against it so we don't churn the list cache.
443	requireNoBump(t, "Animate[spinning,foreign ID]", v, func() {
444		a.Animate(anim.StepMsg{ID: "some-other-tool"})
445	})
446
447	// Finished โ†’ no bump on any ID. The entry is frozen; a stray
448	// bump would needlessly invalidate frozen entries.
449	tcFinished := tc
450	tcFinished.Finished = true
451	item.SetToolCall(tcFinished)
452	item.SetResult(&message.ToolResult{ToolCallID: tc.ID, Content: "ok"})
453	require.True(t, item.Finished(), "tool must report Finished() once the result lands")
454
455	requireNoBump(t, "Animate[finished,own ID]", v, func() {
456		a.Animate(anim.StepMsg{ID: tc.ID})
457	})
458	requireNoBump(t, "Animate[finished,foreign ID]", v, func() {
459		a.Animate(anim.StepMsg{ID: "some-other-tool"})
460	})
461}
462
463// TestAgentToolMessageItem_AnimateBumpsVersion is the spinner
464// regression test for agent tools. The parent must bump on both
465// the parent-tick branch (msg.ID == parent.ID()) and the
466// nested-tick branch (msg.ID == nested.ID()) because the list
467// only checks the parent's version โ€” nested tools are not list
468// entries of their own. Unrelated IDs must not bump, and a parent
469// with a result must not bump on any ID.
470func TestAgentToolMessageItem_AnimateBumpsVersion(t *testing.T) {
471	t.Parallel()
472
473	sty := styles.CharmtonePantera()
474	parentTC := message.ToolCall{ID: "agent-parent", Name: "agent", Input: `{}`, Finished: false}
475	parent := NewAgentToolMessageItem(&sty, parentTC, nil, false)
476
477	childTC := message.ToolCall{ID: "agent-child", Name: "bash", Input: `{}`, Finished: false}
478	child := NewToolMessageItem(&sty, "msg", childTC, nil, false)
479	parent.AddNestedTool(child)
480
481	// Spinning + parent's own ID โ†’ parent bumps.
482	requireBump(t, "Animate[spinning,parent ID]", parent, func() {
483		parent.Animate(anim.StepMsg{ID: parentTC.ID})
484	})
485
486	// Spinning + nested child ID โ†’ parent bumps. The list only
487	// invalidates on the parent; without this the nested
488	// spinner's frame would never reach the screen even though
489	// the nested anim's step has advanced.
490	requireBump(t, "Animate[spinning,nested ID]", parent, func() {
491		parent.Animate(anim.StepMsg{ID: childTC.ID})
492	})
493
494	// Spinning + unrelated ID โ†’ no bump.
495	requireNoBump(t, "Animate[spinning,foreign ID]", parent, func() {
496		parent.Animate(anim.StepMsg{ID: "unrelated"})
497	})
498
499	// Once the parent has a result, neither branch bumps.
500	parent.SetResult(&message.ToolResult{ToolCallID: parentTC.ID, Content: "done"})
501	requireNoBump(t, "Animate[finished,parent ID]", parent, func() {
502		parent.Animate(anim.StepMsg{ID: parentTC.ID})
503	})
504	requireNoBump(t, "Animate[finished,nested ID]", parent, func() {
505		parent.Animate(anim.StepMsg{ID: childTC.ID})
506	})
507}
508
509// TestAgenticFetchToolMessageItem_AnimateBumpsVersion is the
510// agentic-fetch counterpart of the agent-tool Animate bump test.
511// Without an explicit override the embedded base Animate would
512// drop nested-child StepMsgs at anim.Animate's ID check and never
513// bump the parent on its own ticks; this test locks in the
514// override.
515func TestAgenticFetchToolMessageItem_AnimateBumpsVersion(t *testing.T) {
516	t.Parallel()
517
518	sty := styles.CharmtonePantera()
519	parentTC := message.ToolCall{ID: "fetch-parent", Name: "agentic_fetch", Input: `{}`, Finished: false}
520	parent := NewAgenticFetchToolMessageItem(&sty, parentTC, nil, false)
521
522	childTC := message.ToolCall{ID: "fetch-child", Name: "fetch", Input: `{}`, Finished: false}
523	child := NewToolMessageItem(&sty, "msg", childTC, nil, false)
524	parent.AddNestedTool(child)
525
526	requireBump(t, "Animate[spinning,parent ID]", parent, func() {
527		parent.Animate(anim.StepMsg{ID: parentTC.ID})
528	})
529	requireBump(t, "Animate[spinning,nested ID]", parent, func() {
530		parent.Animate(anim.StepMsg{ID: childTC.ID})
531	})
532	requireNoBump(t, "Animate[spinning,foreign ID]", parent, func() {
533		parent.Animate(anim.StepMsg{ID: "unrelated"})
534	})
535
536	parent.SetResult(&message.ToolResult{ToolCallID: parentTC.ID, Content: "done"})
537	requireNoBump(t, "Animate[finished,parent ID]", parent, func() {
538		parent.Animate(anim.StepMsg{ID: parentTC.ID})
539	})
540	requireNoBump(t, "Animate[finished,nested ID]", parent, func() {
541		parent.Animate(anim.StepMsg{ID: childTC.ID})
542	})
543}
544
545// TestBaseToolMessageItem_FinishedTransition covers ยง4.5.1 for
546// tools: a still-running tool reports Finished() == false; once the
547// tool call is marked finished and a result lands, Finished()
548// returns true. Cancelled tools also become Finished.
549func TestBaseToolMessageItem_FinishedTransition(t *testing.T) {
550	t.Parallel()
551
552	sty := styles.CharmtonePantera()
553	tc := message.ToolCall{ID: "tc-fin", Name: "bash", Input: "{}", Finished: false}
554	item := NewToolMessageItem(&sty, "msg", tc, nil, false)
555	require.False(t, item.Finished(), "running tool must not be Finished()")
556
557	tcFinished := tc
558	tcFinished.Finished = true
559	item.SetToolCall(tcFinished)
560	item.SetResult(&message.ToolResult{ToolCallID: "tc-fin", Content: "ok"})
561	require.True(t, item.Finished(), "finished tool with result must be Finished()")
562
563	// Canceled tool with no result is also Finished.
564	tcCanceled := message.ToolCall{ID: "tc-cancel", Name: "bash", Input: "{}", Finished: false}
565	canceled := NewToolMessageItem(&sty, "msg", tcCanceled, nil, true)
566	require.True(t, canceled.Finished(), "canceled tool must be Finished()")
567}