mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
The tree was a debranded snapshot of upstream taken around 2026-02-20, and upstream has moved about five months since. This replaces it with upstream master, mechanically re-derived: bin/debrand does the rename, and the pipeline and squashed migrations are restored from the shipped tree. Not a merge. The previous import replaced the tree wholesale in a single commit, so there is no shared content history to merge against. Reproducing the rename on the new tree is what actually converges. What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints, compose files, LLM.md, and insights/migrations. The squashed initial migration is load-bearing — the live database records it by name, so upstream's ~918 individual migrations cannot come back. Nine products are dropped as unreferenced. Twenty-one more that we do not run are kept anyway because the core app still imports them; unpicking those is a refactor and does not belong in a rebase. This base does not build yet, and the reasons are not in the rename: upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against our 4.2, and the dependency manifests still name upstream packages that we republish under our own names. Those are decisions, not renames.
400 lines
12 KiB
Go
400 lines
12 KiB
Go
package tui
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
tea "charm.land/bubbletea/v2"
|
|
"github.com/insights/insights/phrocs/internal/config"
|
|
"github.com/insights/insights/phrocs/internal/process"
|
|
)
|
|
|
|
// readyShowAllModel creates a model with a small set of "active" processes and
|
|
// a registry config that has additional processes. The registry is injected
|
|
// directly via standbyRegProcs rather than requiring bin/mprocs.yaml on disk.
|
|
func readyShowAllModel(t *testing.T) Model {
|
|
t.Helper()
|
|
f := false
|
|
procs := map[string]config.ProcConfig{
|
|
"backend": {Shell: "start-backend", Autostart: &f, Groups: map[string]string{"layer": "Application", "tech": "Python"}},
|
|
"frontend": {Shell: "start-frontend", Autostart: &f, Groups: map[string]string{"layer": "Application", "tech": "Frontend"}},
|
|
}
|
|
cfg := &config.Config{
|
|
Procs: procs,
|
|
MouseScrollSpeed: 3,
|
|
Scrollback: 1000,
|
|
GroupOrder: map[string][]string{
|
|
"layer": {"Application", "Processing", "Ingestion", "Infrastructure"},
|
|
"tech": {"Python", "Frontend", "Rust", "Docker"},
|
|
},
|
|
}
|
|
mgr := process.NewManager(cfg)
|
|
m := New(mgr, cfg, "", nil)
|
|
next, _ := m.Update(tea.WindowSizeMsg{Width: 120, Height: 40})
|
|
model := next.(Model)
|
|
|
|
// Inject standby processes as if loaded from registry
|
|
model.standbyRegProcs = []*process.Process{
|
|
process.NewStandbyProcess("capture", config.ProcConfig{
|
|
Shell: "start-rust-service capture",
|
|
Capability: "event_ingestion",
|
|
Groups: map[string]string{"layer": "Ingestion", "tech": "Rust"},
|
|
}),
|
|
process.NewStandbyProcess("celery-worker", config.ProcConfig{
|
|
Shell: "start-celery worker",
|
|
Capability: "celery_workers",
|
|
Groups: map[string]string{"layer": "Processing", "tech": "Python"},
|
|
}),
|
|
process.NewStandbyProcess("docker-compose", config.ProcConfig{
|
|
Shell: "docker compose up",
|
|
Groups: map[string]string{"layer": "Infrastructure", "tech": "Docker"},
|
|
}),
|
|
}
|
|
|
|
return model
|
|
}
|
|
|
|
// ── Toggle ──────────────────────────────────────────────────────────────────────
|
|
|
|
func TestShowAll_toggleAddsStandby(t *testing.T) {
|
|
m := readyShowAllModel(t)
|
|
if len(m.services) != 2 {
|
|
t.Fatalf("initial: want 2 services, got %d", len(m.services))
|
|
}
|
|
|
|
// Toggle on
|
|
m.showAllRegProcs = true
|
|
m.refetchServices()
|
|
m.sortServices()
|
|
|
|
if len(m.services) != 5 {
|
|
t.Fatalf("show all on: want 5 services, got %d", len(m.services))
|
|
}
|
|
|
|
// Verify standby processes are present
|
|
standbyCount := 0
|
|
for _, p := range m.services {
|
|
if p.IsStandby() {
|
|
standbyCount++
|
|
}
|
|
}
|
|
if standbyCount != 3 {
|
|
t.Errorf("want 3 standby procs, got %d", standbyCount)
|
|
}
|
|
}
|
|
|
|
func TestShowAll_toggleOffRemovesStandby(t *testing.T) {
|
|
m := readyShowAllModel(t)
|
|
|
|
// Toggle on then off
|
|
m.showAllRegProcs = true
|
|
m.refetchServices()
|
|
m.sortServices()
|
|
m.showAllRegProcs = false
|
|
m.refetchServices()
|
|
m.sortServices()
|
|
|
|
if len(m.services) != 2 {
|
|
t.Fatalf("show all off: want 2 services, got %d", len(m.services))
|
|
}
|
|
for _, p := range m.services {
|
|
if p.IsStandby() {
|
|
t.Error("no standby procs should remain after toggle off")
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Config preservation ─────────────────────────────────────────────────────────
|
|
|
|
func TestShowAll_restoreConfigPreservesRuntimeSettings(t *testing.T) {
|
|
// When toggling off, only Procs and GroupOrder should be replaced from disk.
|
|
// Other config fields (Scrollback, MouseScrollSpeed, etc.) must be preserved
|
|
// so runtime changes (e.g. from setup mode) survive the toggle.
|
|
dir := t.TempDir()
|
|
configPath := filepath.Join(dir, "mprocs.yaml")
|
|
if err := os.WriteFile(configPath, []byte("procs:\n backend: {shell: echo}\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
f := false
|
|
cfg := &config.Config{
|
|
Procs: map[string]config.ProcConfig{"backend": {Shell: "echo", Autostart: &f}},
|
|
MouseScrollSpeed: 3,
|
|
Scrollback: 1000,
|
|
}
|
|
mgr := process.NewManager(cfg)
|
|
m := New(mgr, cfg, configPath, nil)
|
|
next, _ := m.Update(tea.WindowSizeMsg{Width: 120, Height: 40})
|
|
model := next.(Model)
|
|
|
|
// Simulate runtime mutation of non-Procs, non-GroupOrder fields
|
|
model.cfg.MouseScrollSpeed = 99
|
|
model.cfg.Scrollback = 42
|
|
|
|
model.restoreConfigFromDisk()
|
|
|
|
if model.cfg.MouseScrollSpeed != 99 {
|
|
t.Errorf("MouseScrollSpeed should be preserved, got %d", model.cfg.MouseScrollSpeed)
|
|
}
|
|
if model.cfg.Scrollback != 42 {
|
|
t.Errorf("Scrollback should be preserved, got %d", model.cfg.Scrollback)
|
|
}
|
|
}
|
|
|
|
// ── Standby sort order ──────────────────────────────────────────────────────────
|
|
|
|
func TestShowAll_standbySortOrder(t *testing.T) {
|
|
m := readyShowAllModel(t)
|
|
m.showAllRegProcs = true
|
|
m.refetchServices()
|
|
m.sortMode = SortStatus
|
|
m.sortServices()
|
|
|
|
// Standby processes should sort last
|
|
lastRealIdx := -1
|
|
firstStandbyIdx := -1
|
|
for i, p := range m.services {
|
|
if !p.IsStandby() {
|
|
lastRealIdx = i
|
|
}
|
|
if p.IsStandby() && firstStandbyIdx == -1 {
|
|
firstStandbyIdx = i
|
|
}
|
|
}
|
|
if firstStandbyIdx <= lastRealIdx {
|
|
t.Errorf("standby procs should sort after real procs, got first standby at %d, last real at %d",
|
|
firstStandbyIdx, lastRealIdx)
|
|
}
|
|
}
|
|
|
|
// ── Grouping interaction ────────────────────────────────────────────────────────
|
|
|
|
func TestShowAll_standbyAppearsInGroups(t *testing.T) {
|
|
m := readyShowAllModel(t)
|
|
m.showAllRegProcs = true
|
|
m.refetchServices()
|
|
m.sortServices()
|
|
|
|
// Enable grouping by layer
|
|
m.groupDimIndex = 0
|
|
for i, d := range m.groupDims {
|
|
if d == "layer" {
|
|
m.groupDimIndex = i
|
|
break
|
|
}
|
|
}
|
|
m.rebuildSidebarEntries()
|
|
|
|
// Check that standby processes appear under their correct group headers
|
|
foundCapture := false
|
|
for i, e := range m.sidebarEntries {
|
|
if e.proc != nil && e.proc.Name == "capture" {
|
|
foundCapture = true
|
|
// Walk back to find the group header
|
|
for j := i - 1; j >= 0; j-- {
|
|
if m.sidebarEntries[j].isHeader() {
|
|
if m.sidebarEntries[j].groupHeader != "Ingestion" {
|
|
t.Errorf("capture should be under Ingestion, got %q", m.sidebarEntries[j].groupHeader)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !foundCapture {
|
|
t.Error("capture standby should appear in grouped entries")
|
|
}
|
|
}
|
|
|
|
// ── Navigation ──────────────────────────────────────────────────────────────────
|
|
|
|
func TestShowAll_navigationIncludesStandby(t *testing.T) {
|
|
m := readyShowAllModel(t)
|
|
m.showAllRegProcs = true
|
|
m.refetchServices()
|
|
m.sortServices()
|
|
|
|
// Navigate through all processes
|
|
visited := make(map[string]bool)
|
|
for i := 0; i < len(m.services); i++ {
|
|
if p := m.activeProc(); p != nil {
|
|
visited[p.Name] = true
|
|
}
|
|
m = update(m, keypress('j'))
|
|
}
|
|
|
|
for _, name := range []string{"backend", "frontend", "capture", "celery-worker", "docker-compose"} {
|
|
if !visited[name] {
|
|
t.Errorf("should be able to navigate to %s", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Cursor preservation ─────────────────────────────────────────────────────────
|
|
|
|
func TestShowAll_cursorPreservedOnToggle(t *testing.T) {
|
|
m := readyShowAllModel(t)
|
|
|
|
// Select frontend
|
|
for i, p := range m.services {
|
|
if p.Name == "frontend" {
|
|
m.servicesCursor = i
|
|
break
|
|
}
|
|
}
|
|
|
|
m.showAllRegProcs = true
|
|
m.refetchServices()
|
|
m.sortServices()
|
|
|
|
if p := m.activeProc(); p == nil || p.Name != "frontend" {
|
|
name := ""
|
|
if p := m.activeProc(); p != nil {
|
|
name = p.Name
|
|
}
|
|
t.Errorf("cursor should stay on frontend after toggle on, got %q", name)
|
|
}
|
|
|
|
m.showAllRegProcs = false
|
|
m.refetchServices()
|
|
m.sortServices()
|
|
|
|
if p := m.activeProc(); p == nil || p.Name != "frontend" {
|
|
name := ""
|
|
if p := m.activeProc(); p != nil {
|
|
name = p.Name
|
|
}
|
|
t.Errorf("cursor should stay on frontend after toggle off, got %q", name)
|
|
}
|
|
}
|
|
|
|
// ── Key enablement ──────────────────────────────────────────────────────────────
|
|
|
|
func TestShowAll_keyEnablementByProcState(t *testing.T) {
|
|
// Expected key enablement for each process state. `r` is enabled in every
|
|
// state (it acts as start on standby/stopped procs); Stop stays gated on
|
|
// running.
|
|
cases := []struct {
|
|
name string
|
|
procName string // name of the process to focus on (from readyShowAllModel)
|
|
wantStart bool
|
|
wantStop bool
|
|
wantRestart bool
|
|
}{
|
|
{name: "standby", procName: "capture", wantStart: true, wantRestart: true},
|
|
{name: "stopped", procName: "backend", wantStart: true, wantRestart: true},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
m := readyShowAllModel(t)
|
|
m.showAllRegProcs = true
|
|
m.refetchServices()
|
|
m.sortServices()
|
|
|
|
for i, p := range m.services {
|
|
if p.Name == tc.procName {
|
|
m.servicesCursor = i
|
|
break
|
|
}
|
|
}
|
|
m.updateProcKeys()
|
|
|
|
if m.keys.Start.Enabled() != tc.wantStart {
|
|
t.Errorf("Start: got %v, want %v", m.keys.Start.Enabled(), tc.wantStart)
|
|
}
|
|
if m.keys.Stop.Enabled() != tc.wantStop {
|
|
t.Errorf("Stop: got %v, want %v", m.keys.Stop.Enabled(), tc.wantStop)
|
|
}
|
|
if m.keys.Restart.Enabled() != tc.wantRestart {
|
|
t.Errorf("Restart: got %v, want %v", m.keys.Restart.Enabled(), tc.wantRestart)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ── Promote standby ─────────────────────────────────────────────────────────────
|
|
|
|
func TestShowAll_promoteStandbyToReal(t *testing.T) {
|
|
m := readyShowAllModel(t)
|
|
m.showAllRegProcs = true
|
|
m.refetchServices()
|
|
m.sortServices()
|
|
|
|
// Navigate to capture (standby)
|
|
for i, p := range m.services {
|
|
if p.Name == "capture" {
|
|
m.servicesCursor = i
|
|
break
|
|
}
|
|
}
|
|
|
|
if p := m.activeProc(); p == nil || !p.IsStandby() {
|
|
t.Fatal("active proc should be standby capture")
|
|
}
|
|
|
|
real, ok := m.promoteStandby()
|
|
if !ok {
|
|
t.Fatal("promoteStandby should succeed")
|
|
}
|
|
if real.Name != "capture" {
|
|
t.Errorf("promoted proc name: got %q, want capture", real.Name)
|
|
}
|
|
if real.IsStandby() {
|
|
t.Error("promoted proc should not be standby")
|
|
}
|
|
|
|
// capture should no longer be in standbyRegProcs
|
|
for _, p := range m.standbyRegProcs {
|
|
if p.Name == "capture" {
|
|
t.Error("capture should be removed from standbyRegProcs after promotion")
|
|
}
|
|
}
|
|
|
|
// It should still appear in services (as real)
|
|
found := false
|
|
for _, p := range m.services {
|
|
if p.Name == "capture" {
|
|
found = true
|
|
if p.IsStandby() {
|
|
t.Error("capture in services should be real, not standby")
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("capture should still appear in services after promotion")
|
|
}
|
|
}
|
|
|
|
func TestShowAll_promotedProcessSurvivesToggleOff(t *testing.T) {
|
|
m := readyShowAllModel(t)
|
|
m.showAllRegProcs = true
|
|
m.refetchServices()
|
|
m.sortServices()
|
|
|
|
// Promote capture
|
|
for i, p := range m.services {
|
|
if p.Name == "capture" {
|
|
m.servicesCursor = i
|
|
break
|
|
}
|
|
}
|
|
m.promoteStandby()
|
|
|
|
// Toggle off — promoted process should remain (it's real now)
|
|
m.showAllRegProcs = false
|
|
m.refetchServices()
|
|
m.sortServices()
|
|
|
|
found := false
|
|
for _, p := range m.services {
|
|
if p.Name == "capture" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("promoted process should survive toggle-off")
|
|
}
|
|
}
|