Files
ctask/cmd/notes_test.go
typebasedio 8120c399df feat(v0.6): AgentSpec field on TaskMeta with backward-compat unmarshal
Replace TaskMeta.Agent (string) with TaskMeta.Agent (AgentSpec) carrying
type/command/args/env. Custom UnmarshalYAML preserves the legacy scalar
form: a built-in name (claude, opencode) maps to that type; any other
scalar maps to type=custom with the scalar as command. A missing agent
field leaves Type empty so the resolver fills in default_agent at launch.

ValidateAgentSpec enforces: known type (claude|opencode|custom),
type=custom requires command, command must be an executable name or
path with no whitespace or shell metacharacters.

Launch-path wiring (Task 3) and the --agent flag rework (Task 4) are
intentionally not part of this commit; cmd/* call sites are patched to
the minimum needed for the build to compile.
2026-05-15 10:58:06 -04:00

140 lines
3.8 KiB
Go

package cmd
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/warrenronsiek/ctask/internal/workspace"
)
// callNotes invokes runNotes with stdout AND stderr captured.
func callNotes(t *testing.T, root, query string) (stdout, stderr string, err error) {
t.Helper()
prevRoot := os.Getenv("CTASK_ROOT")
os.Setenv("CTASK_ROOT", root)
defer func() {
if prevRoot == "" {
os.Unsetenv("CTASK_ROOT")
} else {
os.Setenv("CTASK_ROOT", prevRoot)
}
}()
outR, outW, _ := os.Pipe()
prevStdout := os.Stdout
os.Stdout = outW
defer func() { os.Stdout = prevStdout }()
errR, errW, _ := os.Pipe()
prevStderr := os.Stderr
os.Stderr = errW
defer func() { os.Stderr = prevStderr }()
err = runNotes(notesCmd, []string{query})
outW.Close()
errW.Close()
var bo, be bytes.Buffer
bo.ReadFrom(outR)
be.ReadFrom(errR)
return bo.String(), be.String(), err
}
// makeNotesWs creates a workspace with optional notes.md content (empty
// string means do not create notes.md).
func makeNotesWs(t *testing.T, root, category, dirName, status, notesBody string) string {
t.Helper()
dir := filepath.Join(root, category, dirName)
os.MkdirAll(dir, 0755)
now := time.Now().UTC().Truncate(time.Second)
slug := dirName[11:]
meta := &workspace.TaskMeta{
ID: "t", Slug: slug, Title: slug,
CreatedAt: now, UpdatedAt: now,
Status: status, Category: category, Type: "task",
Mode: "local", Agent: workspace.AgentSpec{Type: "claude"},
}
if status == "archived" {
meta.ArchivedAt = &now
}
workspace.WriteMeta(filepath.Join(dir, "task.yaml"), meta)
if notesBody != "" {
os.WriteFile(filepath.Join(dir, "notes.md"), []byte(notesBody), 0644)
}
return dir
}
func TestNotesPrintsRawContent(t *testing.T) {
root := t.TempDir()
body := "# my notes\n\n- item one\n- item two\n"
makeNotesWs(t, root, "general", "2026-04-22_notes-active", "active", body)
stdout, stderr, err := callNotes(t, root, "notes-active")
if err != nil {
t.Fatalf("notes should succeed: %v", err)
}
if stdout != body {
t.Errorf("stdout mismatch:\nwant:\n%q\ngot:\n%q", body, stdout)
}
if stderr != "" {
t.Errorf("stderr should be empty on success, got: %q", stderr)
}
}
func TestNotesNoFramingOnStdout(t *testing.T) {
// Output must be raw — no banner, no [ctask] prefix on stdout.
root := t.TempDir()
body := "hello world"
makeNotesWs(t, root, "general", "2026-04-22_notes-bare", "active", body)
stdout, _, err := callNotes(t, root, "notes-bare")
if err != nil {
t.Fatalf("notes: %v", err)
}
if strings.Contains(stdout, "[ctask]") {
t.Errorf("stdout must not contain [ctask] framing: %q", stdout)
}
if stdout != body {
t.Errorf("stdout must equal raw file content exactly: want=%q got=%q", body, stdout)
}
}
func TestNotesMissingFileExitsNonZero(t *testing.T) {
root := t.TempDir()
// notesBody="" → notes.md is NOT created
makeNotesWs(t, root, "general", "2026-04-22_notes-gone", "active", "")
stdout, stderr, err := callNotes(t, root, "notes-gone")
if err == nil {
t.Fatal("expected error when notes.md is missing")
}
if stdout != "" {
t.Errorf("stdout should be empty on error, got: %q", stdout)
}
if !strings.Contains(stderr, "[ctask] no notes.md found") {
t.Errorf("stderr should explain missing notes.md, got: %q", stderr)
}
if !strings.Contains(stderr, "notes-gone") {
t.Errorf("stderr should name the workspace, got: %q", stderr)
}
}
func TestNotesWorksOnArchivedWorkspace(t *testing.T) {
root := t.TempDir()
body := "archived workspace notes\n"
makeNotesWs(t, root, "general", "2026-04-22_notes-archived", "archived", body)
stdout, _, err := callNotes(t, root, "notes-archived")
if err != nil {
t.Fatalf("notes should find archived workspace: %v", err)
}
if stdout != body {
t.Errorf("archived ws stdout mismatch: want=%q got=%q", body, stdout)
}
}