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
2 changes: 1 addition & 1 deletion docs/engram-cloud/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Cloud auth token is runtime config:
export ENGRAM_CLOUD_TOKEN="your-token"
```

The local `~/.engram/cloud.json` stores the server URL. The token is intentionally read from the environment.
The local `~/.engram/cloud.json` stores the server URL and may also store a `token` fallback. `ENGRAM_CLOUD_TOKEN` takes precedence over any token in `cloud.json`; if the env var is unset, Engram falls back to `cloud.json.token`. This fallback is intentional (issue #343) for use cases such as background autosync where exporting the env var on every shell is not practical.

---

Expand Down
1 change: 1 addition & 0 deletions internal/tui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const (
ScreenSessions
ScreenSessionDetail
ScreenSetup
ScreenCloudSettings
)

type SessionDeleteState int
Expand Down
25 changes: 25 additions & 0 deletions internal/tui/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,31 @@ func TestNewInitializesModelDefaults(t *testing.T) {
}
}

func TestScreenCloudSettingsConstant(t *testing.T) {
if ScreenCloudSettings != ScreenSetup+1 {
t.Fatalf("ScreenCloudSettings = %d, want %d (ScreenSetup+1)", ScreenCloudSettings, ScreenSetup+1)
}

seen := map[Screen]bool{}
for _, s := range []Screen{
ScreenDashboard,
ScreenSearch,
ScreenSearchResults,
ScreenRecent,
ScreenObservationDetail,
ScreenTimeline,
ScreenSessions,
ScreenSessionDetail,
ScreenSetup,
ScreenCloudSettings,
} {
if seen[s] {
t.Fatalf("screen constant %d is duplicated", s)
}
seen[s] = true
}
}

func TestInitReturnsCommand(t *testing.T) {
m := New(newTestFixture(t).store, "")
if cmd := m.Init(); cmd == nil {
Expand Down
43 changes: 42 additions & 1 deletion internal/tui/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ func (m Model) handleKeyPress(key string) (tea.Model, tea.Cmd) {
return m.handleSessionDetailKeys(key)
case ScreenSetup:
return m.handleSetupKeys(key)
case ScreenCloudSettings:
return m.handleCloudSettingsKeys(key)
}
return m, nil
}
Expand All @@ -204,9 +206,17 @@ var dashboardMenuItems = []string{
"Recent observations",
"Browse sessions",
"Setup agent plugin",
"Cloud sync settings",
"Quit",
}

var cloudSettingsMenuItems = []string{
"Configure server",
"View status",
"Enroll projects",
"Back",
}

func (m Model) handleDashboardKeys(key string) (tea.Model, tea.Cmd) {
switch key {
case "up", "k":
Expand Down Expand Up @@ -264,7 +274,12 @@ func (m Model) handleDashboardSelection() (tea.Model, tea.Cmd) {
m.SetupInstalling = false
m.SetupInstallingName = ""
return m, nil
case 4: // Quit
case 4: // Cloud sync settings
m.PrevScreen = ScreenDashboard
m.Screen = ScreenCloudSettings
m.Cursor = 0
return m, nil
case 5: // Quit
return m, tea.Quit
}
return m, nil
Expand Down Expand Up @@ -643,6 +658,32 @@ func (m Model) handleSetupKeys(key string) (tea.Model, tea.Cmd) {
return m, nil
}

// ─── Cloud Settings ──────────────────────────────────────────────────────────

func (m Model) handleCloudSettingsKeys(key string) (tea.Model, tea.Cmd) {
switch key {
case "up", "k":
if m.Cursor > 0 {
m.Cursor--
}
case "down", "j":
if m.Cursor < len(cloudSettingsMenuItems)-1 {
m.Cursor++
}
case "enter", " ":
if m.Cursor == len(cloudSettingsMenuItems)-1 { // Back
m.Screen = ScreenDashboard
m.Cursor = 0
return m, loadStats(m.store)
}
case "esc", "q":
m.Screen = ScreenDashboard
m.Cursor = 0
return m, loadStats(m.store)
}
return m, nil
}

// ─── Helpers ─────────────────────────────────────────────────────────────────

func (m Model) resetSessionDeleteState() Model {
Expand Down
117 changes: 114 additions & 3 deletions internal/tui/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,116 @@ func TestHandleDashboardAndSearchKeyPaths(t *testing.T) {
}
}

func TestDashboardHasCloudSettingsMenuItem(t *testing.T) {
cloudIdx, quitIdx := -1, -1
for i, item := range dashboardMenuItems {
if item == "Cloud sync settings" {
cloudIdx = i
}
if item == "Quit" {
quitIdx = i
}
}
if cloudIdx < 0 {
t.Fatal("dashboard menu is missing Cloud sync settings item")
}
if quitIdx < 0 {
t.Fatal("dashboard menu is missing Quit item")
}
if cloudIdx >= quitIdx {
t.Fatalf("Cloud sync settings (%d) must appear before Quit (%d)", cloudIdx, quitIdx)
}
}

func TestCloudSettingsNavigation(t *testing.T) {
m := New(nil, "")
m.Cursor = 4 // Cloud sync settings

updatedModel, _ := m.handleDashboardSelection()
updated := updatedModel.(Model)
if updated.Screen != ScreenCloudSettings {
t.Fatalf("enter on Cloud sync settings should open ScreenCloudSettings, got %v", updated.Screen)
}
if updated.Cursor != 0 {
t.Fatalf("cursor should reset to 0 on entering cloud settings, got %d", updated.Cursor)
}

updatedModel, _ = updated.handleCloudSettingsKeys("esc")
updated = updatedModel.(Model)
if updated.Screen != ScreenDashboard {
t.Fatalf("esc from cloud settings should return to dashboard, got %v", updated.Screen)
}

m = New(nil, "")
m.Screen = ScreenCloudSettings
updatedModel, _ = m.handleCloudSettingsKeys("q")
updated = updatedModel.(Model)
if updated.Screen != ScreenDashboard {
t.Fatalf("q from cloud settings should return to dashboard, got %v", updated.Screen)
}
}

func TestCloudSettingsMenuNavigation(t *testing.T) {
m := New(nil, "")
m.Screen = ScreenCloudSettings

updatedModel, _ := m.handleCloudSettingsKeys("down")
updated := updatedModel.(Model)
if updated.Cursor != 1 {
t.Fatalf("down should move cursor to 1, got %d", updated.Cursor)
}

updatedModel, _ = updated.handleCloudSettingsKeys("j")
updated = updatedModel.(Model)
if updated.Cursor != 2 {
t.Fatalf("j should move cursor to 2, got %d", updated.Cursor)
}

updatedModel, _ = updated.handleCloudSettingsKeys("down")
updated = updatedModel.(Model)
if updated.Cursor != 3 {
t.Fatalf("down should move cursor to 3, got %d", updated.Cursor)
}

updatedModel, _ = updated.handleCloudSettingsKeys("down")
updated = updatedModel.(Model)
if updated.Cursor != 3 {
t.Fatalf("down at bottom should stay at 3, got %d", updated.Cursor)
}

updatedModel, _ = updated.handleCloudSettingsKeys("up")
updated = updatedModel.(Model)
if updated.Cursor != 2 {
t.Fatalf("up should move cursor to 2, got %d", updated.Cursor)
}

updatedModel, _ = updated.handleCloudSettingsKeys("k")
updated = updatedModel.(Model)
if updated.Cursor != 1 {
t.Fatalf("k should move cursor to 1, got %d", updated.Cursor)
}

m = New(nil, "")
m.Screen = ScreenCloudSettings
updatedModel, _ = m.handleCloudSettingsKeys("up")
updated = updatedModel.(Model)
if updated.Cursor != 0 {
t.Fatalf("up at top should stay at 0, got %d", updated.Cursor)
}

m = New(nil, "")
m.Screen = ScreenCloudSettings
m.Cursor = 3 // Back
updatedModel, cmd := m.handleCloudSettingsKeys("enter")
updated = updatedModel.(Model)
if updated.Screen != ScreenDashboard {
t.Fatalf("enter on Back should return to dashboard, got %v", updated.Screen)
}
if cmd == nil {
t.Fatal("enter on Back should refresh stats")
}
}
Comment on lines +311 to +322

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add coverage for selecting unimplemented menu items.

As per path instructions for **/*_test.go, please verify coverage of edge cases. The test covers pressing "enter" on the "Back" item (m.Cursor = 3), but lacks an assertion for pressing "enter" or "space" on the currently unimplemented items (e.g., m.Cursor = 0). Verify that selecting these items safely acts as a no-op (returning the model unchanged with a nil command) to prevent regressions in future PRs.

🧪 Proposed test addition
 	if cmd == nil {
 		t.Fatal("enter on Back should refresh stats")
 	}
+
+	m = New(nil, "")
+	m.Screen = ScreenCloudSettings
+	m.Cursor = 0 // Configure server
+	updatedModel, cmd = m.handleCloudSettingsKeys("enter")
+	updated = updatedModel.(Model)
+	if updated.Screen != ScreenCloudSettings {
+		t.Fatalf("enter on unimplemented item should not change screen, got %v", updated.Screen)
+	}
+	if cmd != nil {
+		t.Fatal("enter on unimplemented item should return nil command")
+	}
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
m = New(nil, "")
m.Screen = ScreenCloudSettings
m.Cursor = 3 // Back
updatedModel, cmd := m.handleCloudSettingsKeys("enter")
updated = updatedModel.(Model)
if updated.Screen != ScreenDashboard {
t.Fatalf("enter on Back should return to dashboard, got %v", updated.Screen)
}
if cmd == nil {
t.Fatal("enter on Back should refresh stats")
}
}
m = New(nil, "")
m.Screen = ScreenCloudSettings
m.Cursor = 3 // Back
updatedModel, cmd := m.handleCloudSettingsKeys("enter")
updated = updatedModel.(Model)
if updated.Screen != ScreenDashboard {
t.Fatalf("enter on Back should return to dashboard, got %v", updated.Screen)
}
if cmd == nil {
t.Fatal("enter on Back should refresh stats")
}
m = New(nil, "")
m.Screen = ScreenCloudSettings
m.Cursor = 0 // Configure server
updatedModel, cmd = m.handleCloudSettingsKeys("enter")
updated = updatedModel.(Model)
if updated.Screen != ScreenCloudSettings {
t.Fatalf("enter on unimplemented item should not change screen, got %v", updated.Screen)
}
if cmd != nil {
t.Fatal("enter on unimplemented item should return nil command")
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/update_test.go` around lines 310 - 321, Add test coverage
alongside the existing cloud-settings key handling test for an unimplemented
item such as m.Cursor = 0, verifying both “enter” and “space” return the
unchanged Model and a nil command. Keep the existing ScreenDashboard and
refresh-command assertions for the Back item unchanged.

Source: Path instructions


func TestHandleRecentTimelineSessionsAndDetailKeyPaths(t *testing.T) {
fx := newTestFixture(t)
m := New(fx.store, "")
Expand Down Expand Up @@ -617,6 +727,7 @@ func TestHandleKeyPressRouterAndClearsError(t *testing.T) {
ScreenSessions,
ScreenSessionDetail,
ScreenSetup,
ScreenCloudSettings,
} {
m.Screen = screen
m.ErrorMsg = "old error"
Expand All @@ -643,7 +754,7 @@ func TestHandleDashboardKeysAndSelectionRemainingBranches(t *testing.T) {
t.Fatal("cursor should stay at bottom boundary")
}

m.Cursor = 4
m.Cursor = 5
_, cmd := m.handleDashboardKeys(" ")
if cmd == nil {
t.Fatal("space on quit item should return quit command")
Expand All @@ -661,10 +772,10 @@ func TestHandleDashboardKeysAndSelectionRemainingBranches(t *testing.T) {
t.Fatal("cursor 0 selection should open search")
}

m.Cursor = 4
m.Cursor = 5
_, cmd = m.handleDashboardSelection()
if cmd == nil {
t.Fatal("cursor 4 selection should quit")
t.Fatal("cursor 5 selection should quit")
}

m.Cursor = 99
Expand Down
31 changes: 25 additions & 6 deletions internal/tui/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ func (m Model) View() string {
content = m.viewSessionDetail()
case ScreenSetup:
content = m.viewSetup()
case ScreenCloudSettings:
content = m.viewCloudSettings()
default:
content = "Unknown screen"
}
Expand Down Expand Up @@ -159,19 +161,36 @@ func (m Model) viewDashboard() string {
// Menu
b.WriteString(titleStyle.Render(" Actions"))
b.WriteString("\n")
b.WriteString(renderMenu(dashboardMenuItems, m.Cursor))

for i, item := range dashboardMenuItems {
if i == m.Cursor {
// Help
b.WriteString(helpStyle.Render("\n j/k navigate • enter select • s search • q quit"))

return b.String()
}

func (m Model) viewCloudSettings() string {
var b strings.Builder

b.WriteString(headerStyle.Render(" Cloud sync settings"))
b.WriteString("\n\n")
b.WriteString(renderMenu(cloudSettingsMenuItems, m.Cursor))
b.WriteString(helpStyle.Render("\n j/k navigate • enter select • esc/q back"))

return b.String()
}

// renderMenu renders a vertical list of selectable menu items with a cursor.
func renderMenu(items []string, cursor int) string {
var b strings.Builder
for i, item := range items {
if i == cursor {
b.WriteString(menuSelectedStyle.Render("▸ " + item))
} else {
b.WriteString(menuItemStyle.Render(" " + item))
}
b.WriteString("\n")
}

// Help
b.WriteString(helpStyle.Render("\n j/k navigate • enter select • s search • q quit"))

return b.String()
}

Expand Down
1 change: 1 addition & 0 deletions internal/tui/view_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ func TestViewRouterCoversAllScreens(t *testing.T) {
{screen: ScreenSessions, want: "Sessions"},
{screen: ScreenSessionDetail, want: "Session:"},
{screen: ScreenSetup, want: "Setup"},
{screen: ScreenCloudSettings, want: "Cloud sync settings"},
}

for _, tt := range tests {
Expand Down