diff --git a/README.md b/README.md index 47b82900..bada3332 100644 --- a/README.md +++ b/README.md @@ -213,7 +213,7 @@ spec: ```bash kubectl apply -f workspace.yaml kubectl apply -f task.yaml -kubectl get tasks -w +axon get tasks -w ``` diff --git a/examples/01-simple-task/README.md b/examples/01-simple-task/README.md index caa0cf9f..2857e311 100644 --- a/examples/01-simple-task/README.md +++ b/examples/01-simple-task/README.md @@ -28,7 +28,7 @@ kubectl apply -f examples/01-simple-task/ 3. **Watch the Task:** ```bash -kubectl get tasks -w +axon get tasks -w ``` 4. **Stream the agent logs:** diff --git a/examples/02-task-with-workspace/README.md b/examples/02-task-with-workspace/README.md index 12d29c09..b79fcae9 100644 --- a/examples/02-task-with-workspace/README.md +++ b/examples/02-task-with-workspace/README.md @@ -33,7 +33,7 @@ kubectl apply -f examples/02-task-with-workspace/ 4. **Watch the Task:** ```bash -kubectl get tasks -w +axon get tasks -w ``` 5. **Stream the agent logs:** diff --git a/examples/03-taskspawner-github-issues/README.md b/examples/03-taskspawner-github-issues/README.md index b032ec73..e454da5a 100644 --- a/examples/03-taskspawner-github-issues/README.md +++ b/examples/03-taskspawner-github-issues/README.md @@ -43,7 +43,7 @@ kubectl apply -f examples/03-taskspawner-github-issues/ 4. **Verify the spawner is running:** ```bash -kubectl get taskspawners -w +axon get taskspawners -w ``` 5. **Create a test issue** with the `bug` label in your repository. The @@ -52,7 +52,7 @@ kubectl get taskspawners -w 6. **Watch spawned Tasks:** ```bash -kubectl get tasks -w +axon get tasks -w ``` 7. **Cleanup:** diff --git a/examples/04-taskspawner-cron/README.md b/examples/04-taskspawner-cron/README.md index 31ce2837..0a365e39 100644 --- a/examples/04-taskspawner-cron/README.md +++ b/examples/04-taskspawner-cron/README.md @@ -34,13 +34,13 @@ kubectl apply -f examples/04-taskspawner-cron/ 5. **Verify the spawner is running:** ```bash -kubectl get taskspawners -w +axon get taskspawners -w ``` 6. **Watch spawned Tasks after the schedule fires:** ```bash -kubectl get tasks -w +axon get tasks -w ``` 7. **Cleanup:** diff --git a/examples/05-task-with-agentconfig/README.md b/examples/05-task-with-agentconfig/README.md index fd7d23ee..f9b92629 100644 --- a/examples/05-task-with-agentconfig/README.md +++ b/examples/05-task-with-agentconfig/README.md @@ -48,7 +48,7 @@ kubectl apply -f examples/05-task-with-agentconfig/ 5. **Watch the Task:** ```bash -kubectl get tasks -w +axon get tasks -w ``` 6. **Stream the agent logs:** diff --git a/examples/README.md b/examples/README.md index 7fcd9d12..7d0f2172 100644 --- a/examples/README.md +++ b/examples/README.md @@ -31,7 +31,7 @@ kubectl apply -f examples// 5. Watch the Task progress: ```bash -kubectl get tasks -w +axon get tasks -w ``` ## Tips diff --git a/internal/cli/get.go b/internal/cli/get.go index bf733e6f..681658ba 100644 --- a/internal/cli/get.go +++ b/internal/cli/get.go @@ -34,6 +34,7 @@ func newGetCommand(cfg *ClientConfig) *cobra.Command { func newGetTaskSpawnerCommand(cfg *ClientConfig, allNamespaces *bool) *cobra.Command { var output string + var watch bool cmd := &cobra.Command{ Use: "taskspawner [name]", @@ -45,6 +46,14 @@ func newGetTaskSpawnerCommand(cfg *ClientConfig, allNamespaces *bool) *cobra.Com return fmt.Errorf("unknown output format %q: must be one of yaml, json", output) } + if watch && output != "" { + return fmt.Errorf("--watch is not supported with --output") + } + + if watch && len(args) == 1 { + return fmt.Errorf("--watch is only supported when listing resources") + } + if *allNamespaces && len(args) == 1 { return fmt.Errorf("a resource cannot be retrieved by name across all namespaces") } @@ -74,11 +83,16 @@ func newGetTaskSpawnerCommand(cfg *ClientConfig, allNamespaces *bool) *cobra.Com } } - tsList := &axonv1alpha1.TaskSpawnerList{} var listOpts []client.ListOption if !*allNamespaces { listOpts = append(listOpts, client.InNamespace(ns)) } + + if watch { + return watchTaskSpawners(ctx, cl, listOpts, *allNamespaces) + } + + tsList := &axonv1alpha1.TaskSpawnerList{} if err := cl.List(ctx, tsList, listOpts...); err != nil { return fmt.Errorf("listing task spawners: %w", err) } @@ -97,6 +111,7 @@ func newGetTaskSpawnerCommand(cfg *ClientConfig, allNamespaces *bool) *cobra.Com } cmd.Flags().StringVarP(&output, "output", "o", "", "Output format (yaml or json)") + cmd.Flags().BoolVarP(&watch, "watch", "w", false, "Watch for changes") cmd.ValidArgsFunction = completeTaskSpawnerNames(cfg) _ = cmd.RegisterFlagCompletionFunc("output", cobra.FixedCompletions([]string{"yaml", "json"}, cobra.ShellCompDirectiveNoFileComp)) @@ -106,6 +121,7 @@ func newGetTaskSpawnerCommand(cfg *ClientConfig, allNamespaces *bool) *cobra.Com func newGetTaskCommand(cfg *ClientConfig, allNamespaces *bool) *cobra.Command { var output string + var watch bool cmd := &cobra.Command{ Use: "task [name]", @@ -117,6 +133,14 @@ func newGetTaskCommand(cfg *ClientConfig, allNamespaces *bool) *cobra.Command { return fmt.Errorf("unknown output format %q: must be one of yaml, json", output) } + if watch && output != "" { + return fmt.Errorf("--watch is not supported with --output") + } + + if watch && len(args) == 1 { + return fmt.Errorf("--watch is only supported when listing resources") + } + if *allNamespaces && len(args) == 1 { return fmt.Errorf("a resource cannot be retrieved by name across all namespaces") } @@ -146,11 +170,16 @@ func newGetTaskCommand(cfg *ClientConfig, allNamespaces *bool) *cobra.Command { } } - taskList := &axonv1alpha1.TaskList{} var listOpts []client.ListOption if !*allNamespaces { listOpts = append(listOpts, client.InNamespace(ns)) } + + if watch { + return watchTasks(ctx, cl, listOpts, *allNamespaces) + } + + taskList := &axonv1alpha1.TaskList{} if err := cl.List(ctx, taskList, listOpts...); err != nil { return fmt.Errorf("listing tasks: %w", err) } @@ -169,6 +198,7 @@ func newGetTaskCommand(cfg *ClientConfig, allNamespaces *bool) *cobra.Command { } cmd.Flags().StringVarP(&output, "output", "o", "", "Output format (yaml or json)") + cmd.Flags().BoolVarP(&watch, "watch", "w", false, "Watch for changes") cmd.ValidArgsFunction = completeTaskNames(cfg) _ = cmd.RegisterFlagCompletionFunc("output", cobra.FixedCompletions([]string{"yaml", "json"}, cobra.ShellCompDirectiveNoFileComp)) @@ -178,6 +208,7 @@ func newGetTaskCommand(cfg *ClientConfig, allNamespaces *bool) *cobra.Command { func newGetWorkspaceCommand(cfg *ClientConfig, allNamespaces *bool) *cobra.Command { var output string + var watch bool cmd := &cobra.Command{ Use: "workspace [name]", @@ -189,6 +220,14 @@ func newGetWorkspaceCommand(cfg *ClientConfig, allNamespaces *bool) *cobra.Comma return fmt.Errorf("unknown output format %q: must be one of yaml, json", output) } + if watch && output != "" { + return fmt.Errorf("--watch is not supported with --output") + } + + if watch && len(args) == 1 { + return fmt.Errorf("--watch is only supported when listing resources") + } + if *allNamespaces && len(args) == 1 { return fmt.Errorf("a resource cannot be retrieved by name across all namespaces") } @@ -218,11 +257,16 @@ func newGetWorkspaceCommand(cfg *ClientConfig, allNamespaces *bool) *cobra.Comma } } - wsList := &axonv1alpha1.WorkspaceList{} var listOpts []client.ListOption if !*allNamespaces { listOpts = append(listOpts, client.InNamespace(ns)) } + + if watch { + return watchWorkspaces(ctx, cl, listOpts, *allNamespaces) + } + + wsList := &axonv1alpha1.WorkspaceList{} if err := cl.List(ctx, wsList, listOpts...); err != nil { return fmt.Errorf("listing workspaces: %w", err) } @@ -241,6 +285,7 @@ func newGetWorkspaceCommand(cfg *ClientConfig, allNamespaces *bool) *cobra.Comma } cmd.Flags().StringVarP(&output, "output", "o", "", "Output format (yaml or json)") + cmd.Flags().BoolVarP(&watch, "watch", "w", false, "Watch for changes") cmd.ValidArgsFunction = completeWorkspaceNames(cfg) _ = cmd.RegisterFlagCompletionFunc("output", cobra.FixedCompletions([]string{"yaml", "json"}, cobra.ShellCompDirectiveNoFileComp)) diff --git a/internal/cli/watch.go b/internal/cli/watch.go new file mode 100644 index 00000000..3e301572 --- /dev/null +++ b/internal/cli/watch.go @@ -0,0 +1,213 @@ +package cli + +import ( + "context" + "fmt" + "io" + "os" + "os/signal" + "time" + + "k8s.io/apimachinery/pkg/util/duration" + "sigs.k8s.io/controller-runtime/pkg/client" + + axonv1alpha1 "github.com/axon-core/axon/api/v1alpha1" +) + +const watchPollInterval = 2 * time.Second + +// watchTasks polls for tasks and prints a new row whenever a task's +// phase changes. It blocks until the context is cancelled. +func watchTasks(ctx context.Context, cl client.Client, listOpts []client.ListOption, allNamespaces bool) error { + ctx, stop := signal.NotifyContext(ctx, os.Interrupt) + defer stop() + + // Print initial table. + taskList := &axonv1alpha1.TaskList{} + if err := cl.List(ctx, taskList, listOpts...); err != nil { + return fmt.Errorf("listing tasks: %w", err) + } + printTaskTable(os.Stdout, taskList.Items, allNamespaces) + + known := make(map[string]axonv1alpha1.TaskPhase) + for i := range taskList.Items { + t := &taskList.Items[i] + known[taskKey(t.Namespace, t.Name)] = t.Status.Phase + } + + for { + select { + case <-ctx.Done(): + return nil + case <-time.After(watchPollInterval): + } + + taskList = &axonv1alpha1.TaskList{} + if err := cl.List(ctx, taskList, listOpts...); err != nil { + if ctx.Err() != nil { + return nil + } + return fmt.Errorf("listing tasks: %w", err) + } + + for i := range taskList.Items { + t := &taskList.Items[i] + key := taskKey(t.Namespace, t.Name) + prev, exists := known[key] + if !exists || prev != t.Status.Phase { + printTaskRow(os.Stdout, t, allNamespaces) + known[key] = t.Status.Phase + } + } + } +} + +// watchTaskSpawners polls for task spawners and prints a new row +// whenever a spawner's status changes. +func watchTaskSpawners(ctx context.Context, cl client.Client, listOpts []client.ListOption, allNamespaces bool) error { + ctx, stop := signal.NotifyContext(ctx, os.Interrupt) + defer stop() + + tsList := &axonv1alpha1.TaskSpawnerList{} + if err := cl.List(ctx, tsList, listOpts...); err != nil { + return fmt.Errorf("listing task spawners: %w", err) + } + printTaskSpawnerTable(os.Stdout, tsList.Items, allNamespaces) + + type spawnerState struct { + phase axonv1alpha1.TaskSpawnerPhase + totalDiscovered int + totalTaskCreated int + } + known := make(map[string]spawnerState) + for i := range tsList.Items { + s := &tsList.Items[i] + known[taskKey(s.Namespace, s.Name)] = spawnerState{ + phase: s.Status.Phase, + totalDiscovered: s.Status.TotalDiscovered, + totalTaskCreated: s.Status.TotalTasksCreated, + } + } + + for { + select { + case <-ctx.Done(): + return nil + case <-time.After(watchPollInterval): + } + + tsList = &axonv1alpha1.TaskSpawnerList{} + if err := cl.List(ctx, tsList, listOpts...); err != nil { + if ctx.Err() != nil { + return nil + } + return fmt.Errorf("listing task spawners: %w", err) + } + + for i := range tsList.Items { + s := &tsList.Items[i] + key := taskKey(s.Namespace, s.Name) + cur := spawnerState{ + phase: s.Status.Phase, + totalDiscovered: s.Status.TotalDiscovered, + totalTaskCreated: s.Status.TotalTasksCreated, + } + if prev, exists := known[key]; !exists || prev != cur { + printTaskSpawnerRow(os.Stdout, s, allNamespaces) + known[key] = cur + } + } + } +} + +// watchWorkspaces polls for workspaces and prints a new row whenever a +// workspace changes. +func watchWorkspaces(ctx context.Context, cl client.Client, listOpts []client.ListOption, allNamespaces bool) error { + ctx, stop := signal.NotifyContext(ctx, os.Interrupt) + defer stop() + + wsList := &axonv1alpha1.WorkspaceList{} + if err := cl.List(ctx, wsList, listOpts...); err != nil { + return fmt.Errorf("listing workspaces: %w", err) + } + printWorkspaceTable(os.Stdout, wsList.Items, allNamespaces) + + known := make(map[string]string) // key -> resourceVersion + for i := range wsList.Items { + ws := &wsList.Items[i] + known[taskKey(ws.Namespace, ws.Name)] = ws.ResourceVersion + } + + for { + select { + case <-ctx.Done(): + return nil + case <-time.After(watchPollInterval): + } + + wsList = &axonv1alpha1.WorkspaceList{} + if err := cl.List(ctx, wsList, listOpts...); err != nil { + if ctx.Err() != nil { + return nil + } + return fmt.Errorf("listing workspaces: %w", err) + } + + for i := range wsList.Items { + ws := &wsList.Items[i] + key := taskKey(ws.Namespace, ws.Name) + if prev, exists := known[key]; !exists || prev != ws.ResourceVersion { + printWorkspaceRow(os.Stdout, ws, allNamespaces) + known[key] = ws.ResourceVersion + } + } + } +} + +func taskKey(namespace, name string) string { + return namespace + "/" + name +} + +// printTaskRow prints a single task row without the header. +func printTaskRow(w io.Writer, t *axonv1alpha1.Task, allNamespaces bool) { + age := duration.HumanDuration(time.Since(t.CreationTimestamp.Time)) + if allNamespaces { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", t.Namespace, t.Name, t.Spec.Type, t.Status.Phase, age) + } else { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", t.Name, t.Spec.Type, t.Status.Phase, age) + } +} + +// printTaskSpawnerRow prints a single task spawner row without the header. +func printTaskSpawnerRow(w io.Writer, s *axonv1alpha1.TaskSpawner, allNamespaces bool) { + age := duration.HumanDuration(time.Since(s.CreationTimestamp.Time)) + source := "" + if s.Spec.When.GitHubIssues != nil { + if s.Spec.TaskTemplate.WorkspaceRef != nil { + source = s.Spec.TaskTemplate.WorkspaceRef.Name + } else { + source = "GitHub Issues" + } + } else if s.Spec.When.Cron != nil { + source = "cron: " + s.Spec.When.Cron.Schedule + } + if allNamespaces { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\t%d\t%s\n", + s.Namespace, s.Name, source, s.Status.Phase, + s.Status.TotalDiscovered, s.Status.TotalTasksCreated, age) + } else { + fmt.Fprintf(w, "%s\t%s\t%s\t%d\t%d\t%s\n", + s.Name, source, s.Status.Phase, + s.Status.TotalDiscovered, s.Status.TotalTasksCreated, age) + } +} + +// printWorkspaceRow prints a single workspace row without the header. +func printWorkspaceRow(w io.Writer, ws *axonv1alpha1.Workspace, allNamespaces bool) { + age := duration.HumanDuration(time.Since(ws.CreationTimestamp.Time)) + if allNamespaces { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", ws.Namespace, ws.Name, ws.Spec.Repo, ws.Spec.Ref, age) + } else { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", ws.Name, ws.Spec.Repo, ws.Spec.Ref, age) + } +} diff --git a/internal/cli/watch_test.go b/internal/cli/watch_test.go new file mode 100644 index 00000000..74e7e737 --- /dev/null +++ b/internal/cli/watch_test.go @@ -0,0 +1,276 @@ +package cli + +import ( + "bytes" + "strings" + "testing" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + axonv1alpha1 "github.com/axon-core/axon/api/v1alpha1" +) + +func TestGetTaskCommand_WatchWithOutput(t *testing.T) { + cmd := NewRootCommand() + cmd.SetArgs([]string{"get", "task", "--watch", "--output", "yaml"}) + + err := cmd.Execute() + if err == nil { + t.Fatal("Expected error when --watch is used with --output") + } + if !strings.Contains(err.Error(), "--watch is not supported with --output") { + t.Errorf("Expected '--watch is not supported with --output' error, got: %v", err) + } +} + +func TestGetTaskCommand_WatchWithName(t *testing.T) { + cmd := NewRootCommand() + cmd.SetArgs([]string{"get", "task", "my-task", "--watch"}) + + err := cmd.Execute() + if err == nil { + t.Fatal("Expected error when --watch is used with a resource name") + } + if !strings.Contains(err.Error(), "--watch is only supported when listing resources") { + t.Errorf("Expected '--watch is only supported when listing resources' error, got: %v", err) + } +} + +func TestGetTaskSpawnerCommand_WatchWithOutput(t *testing.T) { + cmd := NewRootCommand() + cmd.SetArgs([]string{"get", "taskspawner", "--watch", "--output", "json"}) + + err := cmd.Execute() + if err == nil { + t.Fatal("Expected error when --watch is used with --output") + } + if !strings.Contains(err.Error(), "--watch is not supported with --output") { + t.Errorf("Expected '--watch is not supported with --output' error, got: %v", err) + } +} + +func TestGetTaskSpawnerCommand_WatchWithName(t *testing.T) { + cmd := NewRootCommand() + cmd.SetArgs([]string{"get", "taskspawner", "my-spawner", "--watch"}) + + err := cmd.Execute() + if err == nil { + t.Fatal("Expected error when --watch is used with a resource name") + } + if !strings.Contains(err.Error(), "--watch is only supported when listing resources") { + t.Errorf("Expected '--watch is only supported when listing resources' error, got: %v", err) + } +} + +func TestGetWorkspaceCommand_WatchWithOutput(t *testing.T) { + cmd := NewRootCommand() + cmd.SetArgs([]string{"get", "workspace", "--watch", "--output", "yaml"}) + + err := cmd.Execute() + if err == nil { + t.Fatal("Expected error when --watch is used with --output") + } + if !strings.Contains(err.Error(), "--watch is not supported with --output") { + t.Errorf("Expected '--watch is not supported with --output' error, got: %v", err) + } +} + +func TestGetWorkspaceCommand_WatchWithName(t *testing.T) { + cmd := NewRootCommand() + cmd.SetArgs([]string{"get", "workspace", "my-ws", "--watch"}) + + err := cmd.Execute() + if err == nil { + t.Fatal("Expected error when --watch is used with a resource name") + } + if !strings.Contains(err.Error(), "--watch is only supported when listing resources") { + t.Errorf("Expected '--watch is only supported when listing resources' error, got: %v", err) + } +} + +func TestPrintTaskRow(t *testing.T) { + task := &axonv1alpha1.Task{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-task", + Namespace: "default", + CreationTimestamp: metav1.NewTime(time.Now().Add(-1 * time.Hour)), + }, + Spec: axonv1alpha1.TaskSpec{ + Type: "claude-code", + }, + Status: axonv1alpha1.TaskStatus{ + Phase: axonv1alpha1.TaskPhaseRunning, + }, + } + + var buf bytes.Buffer + printTaskRow(&buf, task, false) + output := buf.String() + + if !strings.Contains(output, "test-task") { + t.Errorf("expected task name in output, got %q", output) + } + if !strings.Contains(output, "claude-code") { + t.Errorf("expected task type in output, got %q", output) + } + if !strings.Contains(output, "Running") { + t.Errorf("expected phase in output, got %q", output) + } + if strings.Contains(output, "default") { + t.Errorf("expected no namespace when allNamespaces is false, got %q", output) + } +} + +func TestPrintTaskRowAllNamespaces(t *testing.T) { + task := &axonv1alpha1.Task{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-task", + Namespace: "ns-a", + CreationTimestamp: metav1.NewTime(time.Now().Add(-1 * time.Hour)), + }, + Spec: axonv1alpha1.TaskSpec{ + Type: "claude-code", + }, + Status: axonv1alpha1.TaskStatus{ + Phase: axonv1alpha1.TaskPhaseRunning, + }, + } + + var buf bytes.Buffer + printTaskRow(&buf, task, true) + output := buf.String() + + if !strings.Contains(output, "ns-a") { + t.Errorf("expected namespace in output, got %q", output) + } + if !strings.Contains(output, "test-task") { + t.Errorf("expected task name in output, got %q", output) + } +} + +func TestPrintTaskSpawnerRow(t *testing.T) { + spawner := &axonv1alpha1.TaskSpawner{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-spawner", + Namespace: "default", + CreationTimestamp: metav1.NewTime(time.Now().Add(-1 * time.Hour)), + }, + Spec: axonv1alpha1.TaskSpawnerSpec{ + When: axonv1alpha1.When{ + Cron: &axonv1alpha1.Cron{ + Schedule: "*/5 * * * *", + }, + }, + }, + Status: axonv1alpha1.TaskSpawnerStatus{ + Phase: axonv1alpha1.TaskSpawnerPhaseRunning, + TotalDiscovered: 10, + TotalTasksCreated: 5, + }, + } + + var buf bytes.Buffer + printTaskSpawnerRow(&buf, spawner, false) + output := buf.String() + + if !strings.Contains(output, "test-spawner") { + t.Errorf("expected spawner name in output, got %q", output) + } + if !strings.Contains(output, "Running") { + t.Errorf("expected phase in output, got %q", output) + } + if !strings.Contains(output, "10") { + t.Errorf("expected discovered count in output, got %q", output) + } + if !strings.Contains(output, "5") { + t.Errorf("expected tasks created count in output, got %q", output) + } +} + +func TestPrintTaskSpawnerRowAllNamespaces(t *testing.T) { + spawner := &axonv1alpha1.TaskSpawner{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-spawner", + Namespace: "ns-b", + CreationTimestamp: metav1.NewTime(time.Now().Add(-1 * time.Hour)), + }, + Spec: axonv1alpha1.TaskSpawnerSpec{ + When: axonv1alpha1.When{ + GitHubIssues: &axonv1alpha1.GitHubIssues{}, + }, + TaskTemplate: axonv1alpha1.TaskTemplate{ + WorkspaceRef: &axonv1alpha1.WorkspaceReference{ + Name: "my-ws", + }, + }, + }, + Status: axonv1alpha1.TaskSpawnerStatus{ + Phase: axonv1alpha1.TaskSpawnerPhaseRunning, + }, + } + + var buf bytes.Buffer + printTaskSpawnerRow(&buf, spawner, true) + output := buf.String() + + if !strings.Contains(output, "ns-b") { + t.Errorf("expected namespace in output, got %q", output) + } + if !strings.Contains(output, "test-spawner") { + t.Errorf("expected spawner name in output, got %q", output) + } +} + +func TestPrintWorkspaceRow(t *testing.T) { + ws := &axonv1alpha1.Workspace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ws", + Namespace: "default", + CreationTimestamp: metav1.NewTime(time.Now().Add(-1 * time.Hour)), + }, + Spec: axonv1alpha1.WorkspaceSpec{ + Repo: "https://github.com/org/repo.git", + Ref: "main", + }, + } + + var buf bytes.Buffer + printWorkspaceRow(&buf, ws, false) + output := buf.String() + + if !strings.Contains(output, "test-ws") { + t.Errorf("expected workspace name in output, got %q", output) + } + if !strings.Contains(output, "https://github.com/org/repo.git") { + t.Errorf("expected repo URL in output, got %q", output) + } + if !strings.Contains(output, "main") { + t.Errorf("expected ref in output, got %q", output) + } +} + +func TestPrintWorkspaceRowAllNamespaces(t *testing.T) { + ws := &axonv1alpha1.Workspace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ws", + Namespace: "ns-c", + CreationTimestamp: metav1.NewTime(time.Now().Add(-1 * time.Hour)), + }, + Spec: axonv1alpha1.WorkspaceSpec{ + Repo: "https://github.com/org/repo.git", + Ref: "main", + }, + } + + var buf bytes.Buffer + printWorkspaceRow(&buf, ws, true) + output := buf.String() + + if !strings.Contains(output, "ns-c") { + t.Errorf("expected namespace in output, got %q", output) + } + if !strings.Contains(output, "test-ws") { + t.Errorf("expected workspace name in output, got %q", output) + } +} diff --git a/self-development/README.md b/self-development/README.md index 1d604b5f..018c3488 100644 --- a/self-development/README.md +++ b/self-development/README.md @@ -121,7 +121,7 @@ kubectl apply -f self-development/axon-workers.yaml **Monitor:** ```bash # Watch for new tasks being created -kubectl get tasks -w +axon get tasks -w # Check TaskSpawner status kubectl get taskspawner axon-workers -o yaml