test: add e2e test suite and fix namespace/mention/assignee flags
This commit is contained in:
@@ -157,6 +157,7 @@ Relations connect nodes together:
|
|||||||
| `subtask` | issue → issue | Shows as tree under parent |
|
| `subtask` | issue → issue | Shows as tree under parent |
|
||||||
| `related` | any ↔ any | Shown in related section |
|
| `related` | any ↔ any | Shown in related section |
|
||||||
| `assignee` | issue → user | Adds to user's inbox |
|
| `assignee` | issue → user | Adds to user's inbox |
|
||||||
|
| `in_namespace`| any → namespace | Groups nodes by project or team |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create subtask
|
# Create subtask
|
||||||
@@ -189,7 +190,6 @@ ax add "Task" --type issue --status open --prio high
|
|||||||
| `_type` | `issue`, `note`, `user`, `namespace` | Yes (default: `issue`) |
|
| `_type` | `issue`, `note`, `user`, `namespace` | Yes (default: `issue`) |
|
||||||
| `_status` | `open`, `done` | No |
|
| `_status` | `open`, `done` | No |
|
||||||
| `_prio` | `high`, `medium`, `low` | No |
|
| `_prio` | `high`, `medium`, `low` | No |
|
||||||
| `_namespace` | any string | Yes (default: current user) |
|
|
||||||
|
|
||||||
## Mentions and Inbox
|
## Mentions and Inbox
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ var addCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// default relations
|
// default relations
|
||||||
if !slices.ContainsFunc(cRels, func(e string) bool { return strings.HasPrefix(e, "_namespace::") }) {
|
if !slices.ContainsFunc(cRels, func(e string) bool { return strings.HasPrefix(e, "in_namespace:") }) {
|
||||||
cRels = append(cRels, "_namespace::"+cfg.GetUser())
|
cRels = append(cRels, "in_namespace:"+cfg.GetUser())
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse relations
|
// parse relations
|
||||||
|
|||||||
42
cmd/root.go
42
cmd/root.go
@@ -36,6 +36,8 @@ func addPropertyFlags(cmd *cobra.Command) {
|
|||||||
cmd.Flags().String("status", "", "node status")
|
cmd.Flags().String("status", "", "node status")
|
||||||
cmd.Flags().String("prio", "", "node priority")
|
cmd.Flags().String("prio", "", "node priority")
|
||||||
cmd.Flags().String("namespace", "", "node namespace")
|
cmd.Flags().String("namespace", "", "node namespace")
|
||||||
|
cmd.Flags().String("assignee", "", "node assignee")
|
||||||
|
cmd.Flags().String("mention", "", "node mention")
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerAliasCommands() {
|
func registerAliasCommands() {
|
||||||
@@ -78,24 +80,44 @@ func registerAliasCommands() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func transformArgs(args []string) []string {
|
func transformArgs(args []string) []string {
|
||||||
aliases := map[string]string{
|
tagAliases := map[string]string{
|
||||||
"--status": "_status",
|
"--status": "_status",
|
||||||
"--prio": "_prio",
|
"--prio": "_prio",
|
||||||
"--type": "_type",
|
"--type": "_type",
|
||||||
"--namespace": "_namespace",
|
}
|
||||||
|
relAliases := map[string]string{
|
||||||
|
"--namespace": "in_namespace",
|
||||||
|
"--assignee": "assignee",
|
||||||
|
"--mention": "mentions",
|
||||||
}
|
}
|
||||||
result := []string{}
|
result := []string{}
|
||||||
|
|
||||||
for i := 0; i < len(args); i++ {
|
for i := 0; i < len(args); i++ {
|
||||||
if idx := strings.Index(args[i], "="); idx != -1 {
|
if idx := strings.Index(args[i], "="); idx != -1 {
|
||||||
if prop, ok := aliases[args[i][:idx]]; ok {
|
flag := args[i][:idx]
|
||||||
result = append(result, "--tag", prop+"::"+args[i][idx+1:])
|
val := args[i][idx+1:]
|
||||||
|
if prop, ok := tagAliases[flag]; ok {
|
||||||
|
result = append(result, "--tag", prop+"::"+val)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if prop, ok := relAliases[flag]; ok {
|
||||||
|
result = append(result, "--rel", prop+":"+val)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if prop, ok := aliases[args[i]]; ok && i+1 < len(args) {
|
|
||||||
result, i = append(result, "--tag", prop+"::"+args[i+1]), i+1
|
flag := args[i]
|
||||||
continue
|
if i+1 < len(args) {
|
||||||
|
if prop, ok := tagAliases[flag]; ok {
|
||||||
|
result = append(result, "--tag", prop+"::"+args[i+1])
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if prop, ok := relAliases[flag]; ok {
|
||||||
|
result = append(result, "--rel", prop+":"+args[i+1])
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result = append(result, args[i])
|
result = append(result, args[i])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,11 +55,11 @@ var updateCmd = &cobra.Command{
|
|||||||
ok, blockers, err := svc.CanClose(args[0])
|
ok, blockers, err := svc.CanClose(args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "failed to check blockers:", err)
|
fmt.Fprintln(os.Stderr, "failed to check blockers:", err)
|
||||||
return
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
fmt.Fprintf(os.Stderr, "cannot close: blocked by %v\n", blockers)
|
fmt.Fprintf(os.Stderr, "cannot close: blocked by %v\n", blockers)
|
||||||
return
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
198
e2e_test.go
Normal file
198
e2e_test.go
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -134,7 +134,7 @@ func (n *Node) AddRelation(relType RelType, target string) {
|
|||||||
if n.relations == nil {
|
if n.relations == nil {
|
||||||
n.relations = make(map[string][]string)
|
n.relations = make(map[string][]string)
|
||||||
}
|
}
|
||||||
if relType == RelAssignee || relType == RelCreated {
|
if relType == RelAssignee || relType == RelCreated || relType == RelInNamespace {
|
||||||
n.relations[string(relType)] = []string{target}
|
n.relations[string(relType)] = []string{target}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user