Skip to content

Commit f0ee71b

Browse files
cpcloudclaude
andcommitted
test(llm): rewrite insights tests as user-flow tests
Replace field-setting tests with proper user-interaction tests that drive behavior through keypresses: D to open dashboard, J/k to navigate sections, e to expand, j to enter rows, enter to jump, r to refresh. Deliver LLM results via insightsResultMsg through Update() instead of setting model fields directly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 02c50bc commit f0ee71b

1 file changed

Lines changed: 122 additions & 59 deletions

File tree

internal/app/dashboard_test.go

Lines changed: 122 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1291,108 +1291,171 @@ func TestInsightsRows_WithItems(t *testing.T) {
12911291
assert.True(t, rows[1].Target.InfoOnly)
12921292
}
12931293

1294-
func TestInsightsSection_AppearsInDashboardView(t *testing.T) {
1294+
func TestInsightsSection_AppearsAfterResultMsg(t *testing.T) {
12951295
t.Parallel()
12961296
m := newTestModel(t)
12971297
m.insightsEnabled = true
1298-
m.showDashboard = true
1299-
m.dash.data = nonEmptyDashboard()
1300-
m.dash.insights.items = []insightItem{
1301-
{Text: "Test insight", Tab: "appliances", EntityID: 1},
1298+
m.width = 120
1299+
m.height = 40
1300+
1301+
// Open dashboard with D.
1302+
sendKey(m, "D")
1303+
require.True(t, m.showDashboard)
1304+
1305+
// Simulate LLM response arriving via insightsResultMsg.
1306+
m.Update(insightsResultMsg{
1307+
Items: []insightItem{
1308+
{Text: "Water heater is 12y old", Tab: "appliances", EntityID: 1},
1309+
},
1310+
})
1311+
m.prepareDashboardView()
1312+
1313+
// Expand insights section (last section).
1314+
sendKey(m, "G") // jump to bottom
1315+
// Find and expand the insights header.
1316+
for m.dash.cursor > 0 {
1317+
entry := m.dash.nav[m.dash.cursor]
1318+
if entry.IsHeader && entry.Section == dashSectionInsights {
1319+
break
1320+
}
1321+
sendKey(m, "k")
13021322
}
1303-
m.dash.insights.generatedAt = time.Now()
1304-
m.dash.expanded = map[string]bool{dashSectionInsights: true}
1305-
m.buildDashNav()
1323+
sendKey(m, "e") // expand
1324+
m.prepareDashboardView()
13061325

1307-
view := m.dashboardView(50, 80)
1308-
assert.Contains(t, view, dashSectionInsights)
1309-
assert.Contains(t, view, "Test insight")
1326+
overlay := m.buildDashboardOverlay()
1327+
assert.Contains(t, overlay, dashSectionInsights)
1328+
assert.Contains(t, overlay, "Water heater is 12y old")
13101329
}
13111330

1312-
func TestInsightsSection_LoadingState(t *testing.T) {
1331+
func TestInsightsSection_LoadingShowsSpinner(t *testing.T) {
13131332
t.Parallel()
13141333
m := newTestModel(t)
13151334
m.insightsEnabled = true
1316-
// Need llmClient to be non-nil for insightsWanted.
1317-
// We can just set the flag directly for rendering tests.
1318-
m.showDashboard = true
1319-
m.dash.data = nonEmptyDashboard()
1335+
m.width = 120
1336+
m.height = 40
1337+
1338+
// Open dashboard.
1339+
sendKey(m, "D")
1340+
require.True(t, m.showDashboard)
1341+
1342+
// Simulate loading state (insights fetch in progress).
13201343
m.dash.insights.loading = true
1321-
m.buildDashNav()
1344+
m.prepareDashboardView()
13221345

1323-
view := m.dashboardView(50, 80)
1324-
assert.Contains(t, view, "analyzing...")
1346+
overlay := m.buildDashboardOverlay()
1347+
assert.Contains(t, overlay, "analyzing...")
13251348
}
13261349

1327-
func TestInsightsSection_ErrorState(t *testing.T) {
1350+
func TestInsightsSection_ErrorShowsMutedMessage(t *testing.T) {
13281351
t.Parallel()
13291352
m := newTestModel(t)
13301353
m.insightsEnabled = true
1331-
m.showDashboard = true
1332-
m.dash.data = nonEmptyDashboard()
1333-
m.dash.insights.err = fmt.Errorf("network timeout")
1334-
m.buildDashNav()
1354+
m.width = 120
1355+
m.height = 40
1356+
1357+
// Open dashboard.
1358+
sendKey(m, "D")
1359+
require.True(t, m.showDashboard)
1360+
1361+
// Simulate error result arriving.
1362+
m.Update(insightsResultMsg{Err: fmt.Errorf("network timeout")})
1363+
m.prepareDashboardView()
13351364

1336-
view := m.dashboardView(50, 80)
1337-
assert.Contains(t, view, "unavailable")
1338-
assert.Contains(t, view, "network timeout")
1339-
assert.NotContains(t, view, "error:")
1365+
overlay := m.buildDashboardOverlay()
1366+
assert.Contains(t, overlay, "unavailable")
1367+
assert.Contains(t, overlay, "network timeout")
1368+
assert.NotContains(t, overlay, "error:")
13401369
}
13411370

1342-
func TestInsightsNav_JumpsToEntity(t *testing.T) {
1371+
func TestInsightsNav_JumpsToEntityViaKeyboard(t *testing.T) {
13431372
t.Parallel()
13441373
m := newTestModel(t)
1345-
m.showDashboard = true
1346-
m.dash.data = nonEmptyDashboard()
1347-
m.dash.insights.items = []insightItem{
1348-
{Text: "Test insight", Tab: "appliances", EntityID: 42},
1349-
}
1350-
m.dash.expanded = map[string]bool{dashSectionInsights: true}
1351-
m.buildDashNav()
1374+
m.insightsEnabled = true
1375+
m.width = 120
1376+
m.height = 40
13521377

1353-
// Find the insights data entry in nav.
1354-
found := false
1355-
for i, entry := range m.dash.nav {
1356-
if entry.Section == dashSectionInsights && !entry.IsHeader {
1357-
found = true
1358-
m.dash.cursor = i
1378+
// Open dashboard.
1379+
sendKey(m, "D")
1380+
require.True(t, m.showDashboard)
1381+
1382+
// Deliver insights.
1383+
m.Update(insightsResultMsg{
1384+
Items: []insightItem{
1385+
{Text: "Test insight", Tab: "appliances", EntityID: 42},
1386+
},
1387+
})
1388+
m.prepareDashboardView()
1389+
1390+
// Navigate to the Insights section header with J (jump between sections).
1391+
for range 20 {
1392+
entry := m.dash.nav[m.dash.cursor]
1393+
if entry.IsHeader && entry.Section == dashSectionInsights {
13591394
break
13601395
}
1396+
sendKey(m, "J")
13611397
}
1362-
require.True(t, found, "should have an insights nav entry")
1398+
require.Equal(t, dashSectionInsights, m.dash.nav[m.dash.cursor].Section)
1399+
1400+
// Expand it.
1401+
sendKey(m, "e")
1402+
m.prepareDashboardView()
13631403

1364-
// Press enter to jump.
1404+
// Move down to the data row.
1405+
sendKey(m, "j")
1406+
require.False(t, m.dash.nav[m.dash.cursor].IsHeader, "should be on data row")
1407+
1408+
// Press enter to jump to the entity.
13651409
sendKey(m, "enter")
1366-
assert.False(t, m.showDashboard, "enter should close dashboard")
1410+
assert.False(t, m.showDashboard, "dashboard should close")
13671411
assert.Equal(t, tabIndex(tabAppliances), m.active)
13681412
}
13691413

1370-
func TestInsightsRefreshKey(t *testing.T) {
1414+
func TestInsightsRefreshKey_MarksStale(t *testing.T) {
13711415
t.Parallel()
13721416
m := newTestModel(t)
1373-
m.showDashboard = true
13741417
m.insightsEnabled = true
1375-
m.dash.data = nonEmptyDashboard()
1376-
m.dash.insights.items = []insightItem{
1377-
{Text: "Old insight", Tab: "appliances", EntityID: 1},
1378-
}
1379-
m.dash.insights.stale = false
1380-
m.buildDashNav()
1418+
m.width = 120
1419+
m.height = 40
1420+
1421+
// Open dashboard.
1422+
sendKey(m, "D")
1423+
require.True(t, m.showDashboard)
1424+
1425+
// Deliver insights so they're cached.
1426+
m.Update(insightsResultMsg{
1427+
Items: []insightItem{
1428+
{Text: "Old insight", Tab: "appliances", EntityID: 1},
1429+
},
1430+
})
1431+
m.prepareDashboardView()
1432+
require.False(t, m.dash.insights.stale)
13811433

1382-
// r key should mark insights stale (even without llmClient).
1434+
// Press r to refresh.
13831435
sendKey(m, "r")
13841436
assert.True(t, m.dash.insights.stale)
13851437
}
13861438

1387-
func TestMarkInsightsStale_OnMutation(t *testing.T) {
1439+
func TestInsightsStale_AfterMutation(t *testing.T) {
13881440
t.Parallel()
13891441
m := newTestModelWithStore(t)
13901442
m.insightsEnabled = true
1391-
m.dash.insights.items = []insightItem{
1392-
{Text: "Old insight", Tab: "appliances", EntityID: 1},
1393-
}
1394-
m.dash.insights.stale = false
1443+
m.width = 120
1444+
m.height = 40
1445+
1446+
// Open dashboard.
1447+
sendKey(m, "D")
1448+
require.True(t, m.showDashboard)
1449+
1450+
// Deliver insights.
1451+
m.Update(insightsResultMsg{
1452+
Items: []insightItem{
1453+
{Text: "Old insight", Tab: "appliances", EntityID: 1},
1454+
},
1455+
})
1456+
require.False(t, m.dash.insights.stale)
13951457

1458+
// Simulate a data mutation (e.g. user saved an edit).
13961459
m.reloadAfterMutation()
13971460
assert.True(t, m.dash.insights.stale, "mutation should mark insights stale")
13981461
}

0 commit comments

Comments
 (0)