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
This commit is contained in:
Pierre Martin
2026-03-23 17:42:44 +01:00
parent a49f7d1c57
commit c9a28df3dc
4 changed files with 213 additions and 0 deletions

6
go.mod
View File

@@ -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
)

6
go.sum Normal file
View File

@@ -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=

32
workspace.go Normal file
View File

@@ -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
}

169
workspace_test.go Normal file
View File

@@ -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")
}
}