From c9a28df3dc2c1d15d717eee7784818b7b0056176 Mon Sep 17 00:00:00 2001 From: Pierre Martin Date: Mon, 23 Mar 2026 17:42:44 +0100 Subject: [PATCH] test(02-02): add failing tests for PPID chain walk + workspace resolution - TestReadPPID, TestReadPPIDMissing - TestResolveWorkspace, TestResolveWorkspaceNotFound, TestResolveWorkspaceMaxDepth - TestBuildTerminalWorkspaceMapUnit - Add go.i3wm.org/i3/v4 dependency --- go.mod | 6 ++ go.sum | 6 ++ workspace.go | 32 +++++++++ workspace_test.go | 169 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 213 insertions(+) create mode 100644 go.sum create mode 100644 workspace.go create mode 100644 workspace_test.go diff --git a/go.mod b/go.mod index 27ebc40..1d6f8c1 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,9 @@ module github.com/pieMusic/vmux go 1.25 + +require ( + github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc // indirect + github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 // indirect + go.i3wm.org/i3/v4 v4.24.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f295699 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc h1:7D+Bh06CRPCJO3gr2F7h1sriovOZ8BMhca2Rg85c2nk= +github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 h1:O/r2Sj+8QcMF7V5IcmiE2sMFV2q3J47BEirxbXJAdzA= +github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= +go.i3wm.org/i3/v4 v4.24.0 h1:sBVc+EwxO1UMG7SqYGdGmS4XkMagyHA2y2tcs548fTw= +go.i3wm.org/i3/v4 v4.24.0/go.mod h1:Sdg8TVasZI6E7pc6aV7jxwzN4sl/8MUUs5W2+iyvXyo= diff --git a/workspace.go b/workspace.go new file mode 100644 index 0000000..0d8598f --- /dev/null +++ b/workspace.go @@ -0,0 +1,32 @@ +package main + +import ( + i3 "go.i3wm.org/i3/v4" +) + +// X11PIDResolver abstrait la lecture de _NET_WM_PID pour testabilite. +type X11PIDResolver interface { + GetPID(windowID int64) (int, error) +} + +// I3TreeProvider abstrait i3.GetTree() pour testabilite. +type I3TreeProvider interface { + GetTree() (i3.Tree, error) +} + +// ReadPPID lit le PPid depuis /proc/PID/status. procDir injectable pour les tests. +func ReadPPID(procDir string, pid int) (int, error) { + return 0, nil +} + +// ResolveWorkspace remonte la chaine PPID depuis claudePID jusqu'a trouver +// un PID connu dans terminalWorkspaces. Max 20 niveaux. +func ResolveWorkspace(procDir string, claudePID int, terminalWorkspaces map[int]string) string { + return "" +} + +// BuildTerminalWorkspaceMap construit la map terminalPID -> workspaceName. +// Utilise i3 GetTree + X11 _NET_WM_PID. +func BuildTerminalWorkspaceMap(tree I3TreeProvider, x11 X11PIDResolver) (map[int]string, error) { + return nil, nil +} diff --git a/workspace_test.go b/workspace_test.go new file mode 100644 index 0000000..1deda87 --- /dev/null +++ b/workspace_test.go @@ -0,0 +1,169 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + i3 "go.i3wm.org/i3/v4" +) + +// --- Helpers --- + +func writeFakeStatus(t *testing.T, procDir string, pid, ppid int) { + t.Helper() + pidDir := filepath.Join(procDir, fmt.Sprintf("%d", pid)) + if err := os.MkdirAll(pidDir, 0o755); err != nil { + t.Fatal(err) + } + content := fmt.Sprintf("Name:\tprocess\nPPid:\t%d\nUid:\t1000\n", ppid) + if err := os.WriteFile(filepath.Join(pidDir, "status"), []byte(content), 0o644); err != nil { + t.Fatal(err) + } +} + +// --- Mock types --- + +type mockI3TreeProvider struct { + tree i3.Tree + err error +} + +func (m *mockI3TreeProvider) GetTree() (i3.Tree, error) { + return m.tree, m.err +} + +type mockX11PIDResolver struct { + pids map[int64]int // windowID -> PID +} + +func (m *mockX11PIDResolver) GetPID(windowID int64) (int, error) { + pid, ok := m.pids[windowID] + if !ok { + return 0, fmt.Errorf("no PID for window %d", windowID) + } + return pid, nil +} + +// --- ReadPPID tests --- + +func TestReadPPID(t *testing.T) { + procDir := t.TempDir() + writeFakeStatus(t, procDir, 100, 50) + + ppid, err := ReadPPID(procDir, 100) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if ppid != 50 { + t.Errorf("ReadPPID = %d, want 50", ppid) + } +} + +func TestReadPPIDMissing(t *testing.T) { + procDir := t.TempDir() + + _, err := ReadPPID(procDir, 999) + if err == nil { + t.Fatal("expected error for missing PID, got nil") + } +} + +// --- ResolveWorkspace tests --- + +func TestResolveWorkspace(t *testing.T) { + procDir := t.TempDir() + // Chain: 100 -> 50 -> 10 + writeFakeStatus(t, procDir, 100, 50) + writeFakeStatus(t, procDir, 50, 10) + writeFakeStatus(t, procDir, 10, 1) + + terminalMap := map[int]string{10: "workspace 3"} + + ws := ResolveWorkspace(procDir, 100, terminalMap) + if ws != "workspace 3" { + t.Errorf("ResolveWorkspace = %q, want %q", ws, "workspace 3") + } +} + +func TestResolveWorkspaceNotFound(t *testing.T) { + procDir := t.TempDir() + writeFakeStatus(t, procDir, 100, 50) + writeFakeStatus(t, procDir, 50, 1) + writeFakeStatus(t, procDir, 1, 0) + + terminalMap := map[int]string{999: "workspace 5"} + + ws := ResolveWorkspace(procDir, 100, terminalMap) + if ws != "" { + t.Errorf("ResolveWorkspace = %q, want empty", ws) + } +} + +func TestResolveWorkspaceMaxDepth(t *testing.T) { + procDir := t.TempDir() + // Chain of 25 PIDs: 100 -> 99 -> 98 -> ... -> 76 + for pid := 100; pid > 75; pid-- { + writeFakeStatus(t, procDir, pid, pid-1) + } + // PID 75 is in the terminal map but depth > 20 + terminalMap := map[int]string{75: "workspace 1"} + + ws := ResolveWorkspace(procDir, 100, terminalMap) + if ws != "" { + t.Errorf("ResolveWorkspace should return empty after max depth, got %q", ws) + } +} + +// --- BuildTerminalWorkspaceMap tests --- + +func TestBuildTerminalWorkspaceMapUnit(t *testing.T) { + // Mock i3 tree with 2 workspaces, each containing a window + tree := i3.Tree{ + Root: &i3.Node{ + Type: i3.Root, + Nodes: []*i3.Node{ + { + Type: i3.OutputNode, + Name: "eDP-1", + Nodes: []*i3.Node{ + { + Type: i3.WorkspaceNode, + Name: "1", + Nodes: []*i3.Node{ + {Type: i3.Con, Window: 12345}, + }, + }, + { + Type: i3.WorkspaceNode, + Name: "3", + Nodes: []*i3.Node{ + {Type: i3.Con, Window: 67890}, + }, + }, + }, + }, + }, + }, + } + + x11 := &mockX11PIDResolver{ + pids: map[int64]int{ + 12345: 1000, + 67890: 2000, + }, + } + + result, err := BuildTerminalWorkspaceMap(&mockI3TreeProvider{tree: tree}, x11) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if result[1000] != "1" { + t.Errorf("PID 1000 workspace = %q, want %q", result[1000], "1") + } + if result[2000] != "3" { + t.Errorf("PID 2000 workspace = %q, want %q", result[2000], "3") + } +}