package main import ( "encoding/json" "os" "os/exec" "path/filepath" "strings" "testing" ) type NodeResponse struct { ID string `json:"id"` Title string `json:"title"` Content string `json:"content"` Tags []string `json:"tags"` Relations map[string][]string `json:"relations"` } func runAx(t *testing.T, dir string, args ...string) (string, error) { cmd := exec.Command("./ax", args...) cmd.Dir = dir out, err := cmd.CombinedOutput() return string(out), err } func parseJSON(t *testing.T, out string) NodeResponse { var node NodeResponse err := json.Unmarshal([]byte(out), &node) if err != nil { t.Fatalf("failed to parse JSON output: %v\nOutput: %s", err, out) } return node } func TestE2E(t *testing.T) { // Build the binary first cmd := exec.Command("go", "build", "-o", "ax", ".") err := cmd.Run() if err != nil { t.Fatalf("failed to build: %v", err) } defer os.Remove("./ax") // Create a temp directory for the test tmpDir, err := os.MkdirTemp("", "ax-e2e-*") if err != nil { t.Fatalf("failed to create temp dir: %v", err) } defer os.RemoveAll(tmpDir) // Copy binary to temp dir binaryPath := filepath.Join(tmpDir, "ax") cmd = exec.Command("cp", "./ax", binaryPath) err = cmd.Run() if err != nil { t.Fatalf("failed to copy binary: %v", err) } // 1. Initialize DB out, err := runAx(t, tmpDir, "init") if err != nil { t.Fatalf("ax init failed: %v\n%s", err, out) } // Set test user os.Setenv("AX_USER", "testuser") defer os.Unsetenv("AX_USER") // 2. Add an issue out, err = runAx(t, tmpDir, "add", "Test Issue 1", "--namespace", "testproj", "--prio", "high", "--json") if err != nil { t.Fatalf("ax add failed: %v\n%s", err, out) } node1 := parseJSON(t, out) if node1.Title != "Test Issue 1" { t.Errorf("Expected title 'Test Issue 1', got '%s'", node1.Title) } hasHighPrio := false for _, tag := range node1.Tags { if tag == "_prio::high" { hasHighPrio = true } } if !hasHighPrio { t.Errorf("Expected tag '_prio::high', got %v", node1.Tags) } // Note: in_namespace relation points to a node ID, not the string "testproj". // We'll verify it by listing the namespace. // 3. Add a second issue (the blocker) out, err = runAx(t, tmpDir, "add", "Test Issue 2", "--json") if err != nil { t.Fatalf("ax add failed: %v\n%s", err, out) } node2 := parseJSON(t, out) // Make node1 blocked by node2 out, err = runAx(t, tmpDir, "update", node1.ID, "--rel", "blocks:"+node2.ID, "--json") if err != nil { t.Fatalf("ax update failed: %v\n%s", err, out) } // 4. Try to close the blocked issue (should fail) out, err = runAx(t, tmpDir, "update", node1.ID, "--status", "done") if err == nil { t.Errorf("Expected closing blocked issue to fail, but it succeeded") } if !strings.Contains(out, "cannot close: blocked by") { t.Errorf("Expected blocked error message, got: %s", out) } // 5. Close the blocker out, err = runAx(t, tmpDir, "update", node2.ID, "--status", "done", "--json") if err != nil { t.Fatalf("ax update failed: %v\n%s", err, out) } // 6. Close the original issue (should succeed now) out, err = runAx(t, tmpDir, "update", node1.ID, "--status", "done", "--json") if err != nil { t.Fatalf("ax update failed: %v\n%s", err, out) } // 7. Update assignee out, err = runAx(t, tmpDir, "update", node1.ID, "--assignee", "bob", "--json") if err != nil { t.Fatalf("ax update failed: %v\n%s", err, out) } // List by assignee to verify out, err = runAx(t, tmpDir, "list", "--assignee", "bob", "--json") if err != nil { t.Fatalf("ax list assignee failed: %v\n%s", err, out) } var assigneeNodes []NodeResponse err = json.Unmarshal([]byte(out), &assigneeNodes) if err != nil { t.Fatalf("failed to parse list JSON: %v\n%s", err, out) } foundAssigned := false for _, n := range assigneeNodes { if n.ID == node1.ID { foundAssigned = true } } if !foundAssigned { t.Errorf("Expected to find node %s assigned to bob", node1.ID) } // 8. List by namespace out, err = runAx(t, tmpDir, "list", "--namespace", "testproj", "--json") if err != nil { t.Fatalf("ax list failed: %v\n%s", err, out) } // Should be an array of nodes var nodes []NodeResponse err = json.Unmarshal([]byte(out), &nodes) if err != nil { t.Fatalf("failed to parse list JSON: %v\n%s", err, out) } foundNamespace := false for _, n := range nodes { if n.ID == node1.ID { foundNamespace = true } } if !foundNamespace { t.Errorf("Expected node %s in namespace 'testproj', but wasn't found in list output: %v", node1.ID, nodes) } // 9. Mention parsing out, err = runAx(t, tmpDir, "add", "Hello @alice", "--content", "Please see this @alice.", "--json") if err != nil { t.Fatalf("ax add mention failed: %v\n%s", err, out) } // List mention out, err = runAx(t, tmpDir, "list", "--mention", "alice", "--json") if err != nil { t.Fatalf("ax list mention failed: %v\n%s", err, out) } err = json.Unmarshal([]byte(out), &nodes) if err != nil { t.Fatalf("failed to parse mention list JSON: %v\n%s", err, out) } if len(nodes) == 0 { t.Errorf("Expected at least 1 node in alice's inbox") } // Test completed successfully }