Compare commits
1 Commits
0fe62ed430
...
0280f2c327
| Author | SHA1 | Date | |
|---|---|---|---|
| 0280f2c327 |
826
internal/commands/calendar_test.go
Normal file
826
internal/commands/calendar_test.go
Normal file
@@ -0,0 +1,826 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func writeTestConfig(t *testing.T, dir, baseURL string) {
|
||||
config := map[string]string{
|
||||
"base-url": baseURL,
|
||||
}
|
||||
data, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal config: %v", err)
|
||||
}
|
||||
cfgPath := filepath.Join(dir, ".hf-config.json")
|
||||
if err := os.WriteFile(cfgPath, data, 0644); err != nil {
|
||||
t.Fatalf("failed to write test config: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func buildCLI(t *testing.T, cliPath string) {
|
||||
srcDir := filepath.Join("..", "..")
|
||||
cmd := exec.Command("go", "build", "-o", cliPath, filepath.Join(srcDir, "cmd", "hf"))
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Skipf("cannot build CLI: %v (out: %s)", err, string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func runCLI(t *testing.T, dir, cliPath string, args ...string) (string, error) {
|
||||
cmd := exec.Command(cliPath, args...)
|
||||
cmd.Dir = dir
|
||||
cmd.Env = append(os.Environ(), "HF_TEST_MODE=1")
|
||||
out, err := cmd.CombinedOutput()
|
||||
return string(out), err
|
||||
}
|
||||
|
||||
// --- Tests: argument parsing / usage errors ---
|
||||
|
||||
func TestCalendarSchedule_MissingArgs(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(`{}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
// --token must come after subcommand: hf calendar schedule --token <tok>
|
||||
out, err := runCLI(t, tmpDir, cliPath, "calendar", "schedule", "--token", "fake")
|
||||
if err == nil {
|
||||
t.Fatalf("expected non-zero exit for missing args; got out=%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "usage:") && !strings.Contains(out, "slot-type") {
|
||||
t.Errorf("expected usage message with slot-type; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalendarSchedule_UnknownFlag(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(`{}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "calendar", "schedule", "--token", "fake", "Work", "09:00", "30", "--bad-flag")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for unknown flag")
|
||||
}
|
||||
if !strings.Contains(out, "unknown flag") {
|
||||
t.Errorf("expected 'unknown flag' in output; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalendarShow_UnknownFlag(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"slots": []interface{}{}})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "calendar", "show", "--token", "fake", "--bad-flag")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for unknown flag")
|
||||
}
|
||||
if !strings.Contains(out, "unknown flag") {
|
||||
t.Errorf("expected 'unknown flag'; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalendarEdit_MissingSlotID(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(`{}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "calendar", "edit", "--token", "fake")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for missing slot-id")
|
||||
}
|
||||
if !strings.Contains(out, "usage:") {
|
||||
t.Errorf("expected usage message; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalendarCancel_MissingSlotID(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(`{}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "calendar", "cancel", "--token", "fake")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for missing slot-id")
|
||||
}
|
||||
if !strings.Contains(out, "usage:") {
|
||||
t.Errorf("expected usage message; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalendarPlanSchedule_MissingAt(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(`{}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "calendar", "plan-schedule", "--token", "fake", "Work", "30")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for missing --at")
|
||||
}
|
||||
if !strings.Contains(out, "--at") {
|
||||
t.Errorf("expected --at error; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalendarPlanEdit_NothingToEdit(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(`{}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "calendar", "plan-edit", "--token", "fake", "1")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for nothing to edit")
|
||||
}
|
||||
if !strings.Contains(out, "nothing to edit") {
|
||||
t.Errorf("expected 'nothing to edit' error; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalendarPlanCancel_MissingPlanID(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(`{}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "calendar", "plan-cancel", "--token", "fake")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for missing plan-id")
|
||||
}
|
||||
if !strings.Contains(out, "usage:") {
|
||||
t.Errorf("expected usage message; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tests: JSON output ---
|
||||
|
||||
func TestCalendarSchedule_JSONOutput(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" || r.URL.Path != "/calendar/slots" {
|
||||
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"slot_id": 42,
|
||||
"slot_type": "Work",
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "--json", "calendar", "schedule", "--token", "fake", "Work", "09:00", "30")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v; out=%s", err, out)
|
||||
}
|
||||
var resp map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(out), &resp); err != nil {
|
||||
t.Fatalf("output is not valid JSON: %s", out)
|
||||
}
|
||||
if resp["slot_id"] != float64(42) {
|
||||
t.Errorf("expected slot_id=42; got: %v", resp["slot_id"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalendarShow_JSONOutput(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" || r.URL.Path != "/calendar/day" {
|
||||
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"slots": []interface{}{
|
||||
map[string]interface{}{
|
||||
"slot_id": 1,
|
||||
"slot_type": "Work",
|
||||
"scheduled_at": "09:00",
|
||||
"estimated_duration": 30,
|
||||
"priority": 50,
|
||||
"status": "NotStarted",
|
||||
},
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "--json", "calendar", "show", "--token", "fake", "--date", "2026-04-01")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v; out=%s", err, out)
|
||||
}
|
||||
var resp map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(out), &resp); err != nil {
|
||||
t.Fatalf("output is not valid JSON: %s", out)
|
||||
}
|
||||
slots, ok := resp["slots"].([]interface{})
|
||||
if !ok || len(slots) == 0 {
|
||||
t.Fatalf("expected slots array in JSON; got: %v", resp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalendarDateList_JSONOutput(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" || r.URL.Path != "/calendar/dates" {
|
||||
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"dates": []string{"2026-04-01", "2026-04-02"},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "--json", "calendar", "date-list", "--token", "fake")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v; out=%s", err, out)
|
||||
}
|
||||
var resp map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(out), &resp); err != nil {
|
||||
t.Fatalf("output is not valid JSON: %s", out)
|
||||
}
|
||||
dates, ok := resp["dates"].([]interface{})
|
||||
if !ok || len(dates) != 2 {
|
||||
t.Errorf("expected 2 dates; got: %v", dates)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalendarPlanList_JSONOutput(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" || r.URL.Path != "/calendar/plans" {
|
||||
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"plans": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"slot_type": "Work",
|
||||
"at_time": "09:00",
|
||||
"estimated_duration": 30,
|
||||
"is_active": true,
|
||||
},
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "--json", "calendar", "plan-list", "--token", "fake")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v; out=%s", err, out)
|
||||
}
|
||||
var resp map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(out), &resp); err != nil {
|
||||
t.Fatalf("output is not valid JSON: %s", out)
|
||||
}
|
||||
plans, ok := resp["plans"].([]interface{})
|
||||
if !ok || len(plans) == 0 {
|
||||
t.Fatalf("expected plans array; got: %v", resp)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tests: human-readable output ---
|
||||
|
||||
func TestCalendarShow_HumanOutput_WithSlots(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"slots": []interface{}{
|
||||
map[string]interface{}{
|
||||
"slot_id": 1,
|
||||
"slot_type": "Work",
|
||||
"scheduled_at": "09:00",
|
||||
"estimated_duration": 30,
|
||||
"priority": 50,
|
||||
"status": "NotStarted",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"slot_id": "plan-1-2026-04-01",
|
||||
"slot_type": "OnCall",
|
||||
"scheduled_at": "14:00",
|
||||
"estimated_duration": 60,
|
||||
"priority": 40,
|
||||
"status": "NotStarted",
|
||||
"is_virtual": true,
|
||||
},
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "calendar", "show", "--token", "fake", "--date", "2026-04-01")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v; out=%s", err, out)
|
||||
}
|
||||
// Virtual slot should be marked as plan
|
||||
if !strings.Contains(out, "plan") {
|
||||
t.Errorf("expected human output to mark virtual slot as plan; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalendarShow_HumanOutput_Empty(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"slots": []interface{}{},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "calendar", "show", "--token", "fake")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v; out=%s", err, out)
|
||||
}
|
||||
if !strings.Contains(out, "No slots") {
|
||||
t.Errorf("expected 'No slots' for empty; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalendarDateList_HumanOutput(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"dates": []string{"2026-04-01", "2026-04-02"},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "calendar", "date-list", "--token", "fake")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v; out=%s", err, out)
|
||||
}
|
||||
for _, date := range []string{"2026-04-01", "2026-04-02"} {
|
||||
if !strings.Contains(out, date) {
|
||||
t.Errorf("expected date %s in output; got: %s", date, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalendarDateList_HumanOutput_Empty(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"dates": []string{},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "calendar", "date-list", "--token", "fake")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v; out=%s", err, out)
|
||||
}
|
||||
if !strings.Contains(out, "No future dates") {
|
||||
t.Errorf("expected 'No future dates'; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalendarPlanList_HumanOutput(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"plans": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"slot_type": "Work",
|
||||
"at_time": "09:00",
|
||||
"on_day": "Mon",
|
||||
"estimated_duration": 30,
|
||||
"is_active": true,
|
||||
},
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "calendar", "plan-list", "--token", "fake")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v; out=%s", err, out)
|
||||
}
|
||||
if !strings.Contains(out, "09:00") || !strings.Contains(out, "Work") {
|
||||
t.Errorf("expected plan data in output; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalendarPlanList_HumanOutput_Empty(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"plans": []interface{}{},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "calendar", "plan-list", "--token", "fake")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v; out=%s", err, out)
|
||||
}
|
||||
if !strings.Contains(out, "No schedule plans") {
|
||||
t.Errorf("expected 'No schedule plans'; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tests: error output ---
|
||||
|
||||
func TestCalendarShow_ServerError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(`{"detail":"internal error"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "calendar", "show", "--token", "fake")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for 500 response")
|
||||
}
|
||||
if !strings.Contains(out, "failed") && !strings.Contains(out, "error") {
|
||||
t.Errorf("expected error message; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalendarEdit_SlotNotFound(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(404)
|
||||
w.Write([]byte(`{"detail":"slot not found"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "calendar", "edit", "--token", "fake", "999", "--slot-type", "Work")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for 404")
|
||||
}
|
||||
if !strings.Contains(out, "failed") && !strings.Contains(out, "error") {
|
||||
t.Errorf("expected error message; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalendarCancel_SlotNotFound(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(404)
|
||||
w.Write([]byte(`{"detail":"slot not found"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "calendar", "cancel", "--token", "fake", "999")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for 404")
|
||||
}
|
||||
if !strings.Contains(out, "failed") && !strings.Contains(out, "error") {
|
||||
t.Errorf("expected error message; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalendarPlanSchedule_ServerError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(`{"detail":"db error"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "calendar", "plan-schedule", "--token", "fake", "Work", "30", "--at", "09:00")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for 500")
|
||||
}
|
||||
if !strings.Contains(out, "failed") && !strings.Contains(out, "error") {
|
||||
t.Errorf("expected error message; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalendarPlanCancel_PlanNotFound(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(404)
|
||||
w.Write([]byte(`{"detail":"plan not found"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "calendar", "plan-cancel", "--token", "fake", "999")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for 404")
|
||||
}
|
||||
if !strings.Contains(out, "failed") && !strings.Contains(out, "error") {
|
||||
t.Errorf("expected error message; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tests: workload warnings ---
|
||||
|
||||
func TestCalendarSchedule_WorkloadWarning(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"slot_id": 1,
|
||||
"warnings": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "workload",
|
||||
"message": "Daily minimum work workload (30 min) not met: current 0 min",
|
||||
},
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "calendar", "schedule", "--token", "fake", "Work", "09:00", "30")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v; out=%s", err, out)
|
||||
}
|
||||
if !strings.Contains(out, "⚠") && !strings.Contains(out, "warning") {
|
||||
t.Errorf("expected workload warning in output; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tests: virtual slot routing ---
|
||||
|
||||
func TestCalendarEdit_VirtualSlot_RoutesCorrectly(t *testing.T) {
|
||||
var editedPath string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
editedPath = r.URL.Path
|
||||
w.WriteHeader(200)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"slot_id": 10,
|
||||
"slot_type": "Work",
|
||||
"scheduled_at": "10:00",
|
||||
"estimated_duration": 30,
|
||||
"status": "NotStarted",
|
||||
"priority": 50,
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "calendar", "edit", "--token", "fake", "plan-1-2026-04-01", "--scheduled-at", "10:00")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v; out=%s", err, out)
|
||||
}
|
||||
if !strings.Contains(editedPath, "/calendar/slots/virtual/") {
|
||||
t.Errorf("expected virtual slot path /calendar/slots/virtual/...; got: %s", editedPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalendarCancel_VirtualSlot_RoutesCorrectly(t *testing.T) {
|
||||
var cancelledPath string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
cancelledPath = r.URL.Path
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(`{}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
_, err := runCLI(t, tmpDir, cliPath, "calendar", "cancel", "--token", "fake", "plan-1-2026-04-01")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !strings.Contains(cancelledPath, "/calendar/slots/virtual/") {
|
||||
t.Errorf("expected virtual slot cancel path /calendar/slots/virtual/...; got: %s", cancelledPath)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tests: successful operations ---
|
||||
|
||||
func TestCalendarSchedule_SuccessOutput(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"slot_id": 5})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "calendar", "schedule", "--token", "fake", "Work", "09:00", "30", "--job", "TASK-1")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v; out=%s", err, out)
|
||||
}
|
||||
if !strings.Contains(out, "slot scheduled") {
|
||||
t.Errorf("expected 'slot scheduled' success message; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalendarPlanSchedule_SuccessOutput(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"id": 1})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "calendar", "plan-schedule", "--token", "fake", "Work", "30", "--at", "09:00", "--on-day", "Mon")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v; out=%s", err, out)
|
||||
}
|
||||
if !strings.Contains(out, "plan created") {
|
||||
t.Errorf("expected 'plan created' success message; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalendarPlanEdit_SuccessOutput(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"id": 1, "at_time": "10:00"})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "calendar", "plan-edit", "--token", "fake", "1", "--at", "10:00")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v; out=%s", err, out)
|
||||
}
|
||||
if !strings.Contains(out, "plan edited") {
|
||||
t.Errorf("expected 'plan edited' success message; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalendarPlanCancel_SuccessOutput(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(`{}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "calendar", "plan-cancel", "--token", "fake", "1")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v; out=%s", err, out)
|
||||
}
|
||||
if !strings.Contains(out, "plan cancelled") {
|
||||
t.Errorf("expected 'plan cancelled' success message; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalendarCancel_SuccessOutput(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(`{}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLI(t, tmpDir, cliPath, "calendar", "cancel", "--token", "fake", "1")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v; out=%s", err, out)
|
||||
}
|
||||
if !strings.Contains(out, "slot cancelled") {
|
||||
t.Errorf("expected 'slot cancelled' success message; got: %s", out)
|
||||
}
|
||||
}
|
||||
483
internal/commands/proposal_test.go
Normal file
483
internal/commands/proposal_test.go
Normal file
@@ -0,0 +1,483 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func runCLIProposal(t *testing.T, dir, cliPath string, args ...string) (string, error) {
|
||||
cmd := exec.Command(cliPath, args...)
|
||||
cmd.Dir = dir
|
||||
cmd.Env = append(os.Environ(), "HF_TEST_MODE=1")
|
||||
out, err := cmd.CombinedOutput()
|
||||
return string(out), err
|
||||
}
|
||||
|
||||
// --- Essential subcommand tests ---
|
||||
|
||||
func TestEssentialList_MissingProposal(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
json.NewEncoder(w).Encode([]interface{}{})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLIProposal(t, tmpDir, cliPath, "proposal", "essential", "list", "--token", "fake")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for missing --proposal")
|
||||
}
|
||||
if !strings.Contains(out, "--proposal") {
|
||||
t.Errorf("expected --proposal error; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEssentialCreate_MissingRequired(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
// Missing --proposal, --title, --type
|
||||
out, err := runCLIProposal(t, tmpDir, cliPath, "proposal", "essential", "create", "--token", "fake", "--proposal", "PRJ-001")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for missing required args")
|
||||
}
|
||||
if !strings.Contains(out, "usage:") {
|
||||
t.Errorf("expected usage message; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEssentialCreate_InvalidType(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLIProposal(t, tmpDir, cliPath, "proposal", "essential", "create", "--token", "fake",
|
||||
"--proposal", "PRJ-001", "--title", "Test", "--type", "invalid-type")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for invalid type")
|
||||
}
|
||||
if !strings.Contains(out, "invalid essential type") {
|
||||
t.Errorf("expected invalid type error; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEssentialCreate_UnknownFlag(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLIProposal(t, tmpDir, cliPath, "proposal", "essential", "create", "--token", "fake",
|
||||
"--proposal", "PRJ-001", "--title", "Test", "--type", "feature", "--unknown")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for unknown flag")
|
||||
}
|
||||
if !strings.Contains(out, "unknown flag") {
|
||||
t.Errorf("expected unknown flag error; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEssentialUpdate_NothingToUpdate(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLIProposal(t, tmpDir, cliPath, "proposal", "essential", "update", "--token", "fake",
|
||||
"ESS-001", "--proposal", "PRJ-001")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for nothing to update")
|
||||
}
|
||||
if !strings.Contains(out, "nothing to update") {
|
||||
t.Errorf("expected 'nothing to update' error; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEssentialUpdate_MissingProposal(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLIProposal(t, tmpDir, cliPath, "proposal", "essential", "update", "--token", "fake", "ESS-001")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for missing --proposal")
|
||||
}
|
||||
if !strings.Contains(out, "--proposal") {
|
||||
t.Errorf("expected --proposal error; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEssentialDelete_MissingProposal(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLIProposal(t, tmpDir, cliPath, "proposal", "essential", "delete", "--token", "fake", "ESS-001")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for missing --proposal")
|
||||
}
|
||||
if !strings.Contains(out, "--proposal") {
|
||||
t.Errorf("expected --proposal error; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Essential list/create JSON output ---
|
||||
|
||||
func TestEssentialList_JSONOutput(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" || r.URL.Path != "/proposes/PRJ-001/essentials" {
|
||||
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
json.NewEncoder(w).Encode([]interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"essential_code": "ESS-001",
|
||||
"proposal_id": 1,
|
||||
"type": "feature",
|
||||
"title": "Add login",
|
||||
"created_at": "2026-03-01",
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLIProposal(t, tmpDir, cliPath, "--json", "proposal", "essential", "list", "--token", "fake", "--proposal", "PRJ-001")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v; out=%s", err, out)
|
||||
}
|
||||
var resp interface{}
|
||||
if err := json.Unmarshal([]byte(out), &resp); err != nil {
|
||||
t.Fatalf("output is not valid JSON: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEssentialCreate_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
t.Errorf("expected POST; got: %s", r.Method)
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"id": 1,
|
||||
"essential_code": "ESS-001",
|
||||
"title": "Add login",
|
||||
"type": "feature",
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLIProposal(t, tmpDir, cliPath, "proposal", "essential", "create", "--token", "fake",
|
||||
"--proposal", "PRJ-001", "--title", "Add login", "--type", "feature")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v; out=%s", err, out)
|
||||
}
|
||||
if !strings.Contains(out, "essential created") {
|
||||
t.Errorf("expected 'essential created' success message; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEssentialUpdate_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "PATCH" {
|
||||
t.Errorf("expected PATCH; got: %s", r.Method)
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(`{}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLIProposal(t, tmpDir, cliPath, "proposal", "essential", "update", "--token", "fake",
|
||||
"ESS-001", "--proposal", "PRJ-001", "--title", "Updated title")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v; out=%s", err, out)
|
||||
}
|
||||
if !strings.Contains(out, "essential updated") {
|
||||
t.Errorf("expected 'essential updated' success message; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEssentialDelete_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "DELETE" {
|
||||
t.Errorf("expected DELETE; got: %s", r.Method)
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(`{}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLIProposal(t, tmpDir, cliPath, "proposal", "essential", "delete", "--token", "fake",
|
||||
"ESS-001", "--proposal", "PRJ-001")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v; out=%s", err, out)
|
||||
}
|
||||
if !strings.Contains(out, "essential deleted") {
|
||||
t.Errorf("expected 'essential deleted' success message; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Proposal Accept tests ---
|
||||
|
||||
func TestProposalAccept_MissingMilestone(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLIProposal(t, tmpDir, cliPath, "proposal", "accept", "--token", "fake", "PRJ-001")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for missing --milestone")
|
||||
}
|
||||
if !strings.Contains(out, "--milestone") {
|
||||
t.Errorf("expected --milestone error; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProposalAccept_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" || r.URL.Path != "/proposes/PRJ-001/accept" {
|
||||
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
var body map[string]interface{}
|
||||
json.NewDecoder(r.Body).Decode(&body)
|
||||
if body["milestone_code"] != "MS-001" {
|
||||
t.Errorf("expected milestone_code=MS-001; got: %v", body["milestone_code"])
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"code": "PRJ-001",
|
||||
"status": "Accepted",
|
||||
"tasks": []interface{}{},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLIProposal(t, tmpDir, cliPath, "proposal", "accept", "--token", "fake",
|
||||
"PRJ-001", "--milestone", "MS-001")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v; out=%s", err, out)
|
||||
}
|
||||
if !strings.Contains(out, "proposal accepted") {
|
||||
t.Errorf("expected proposal accepted success message; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProposalAccept_JSONOutput(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"code": "PRJ-001",
|
||||
"status": "Accepted",
|
||||
"tasks": []interface{}{
|
||||
map[string]interface{}{"code": "TASK-1", "type": "story/feature"},
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLIProposal(t, tmpDir, cliPath, "--json", "proposal", "accept", "--token", "fake",
|
||||
"PRJ-001", "--milestone", "MS-001")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v; out=%s", err, out)
|
||||
}
|
||||
var resp map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(out), &resp); err != nil {
|
||||
t.Fatalf("output is not valid JSON: %s", out)
|
||||
}
|
||||
if resp["status"] != "Accepted" {
|
||||
t.Errorf("expected status=Accepted; got: %v", resp["status"])
|
||||
}
|
||||
}
|
||||
|
||||
// --- Story restricted tests ---
|
||||
|
||||
func TestTaskCreate_StoryRestricted(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
// Try to create a story/feature directly
|
||||
out, err := runCLIProposal(t, tmpDir, cliPath, "task", "create", "--token", "fake",
|
||||
"--type", "story/feature", "--title", "My story", "--project", "PRJ-001")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for restricted story creation")
|
||||
}
|
||||
if !strings.Contains(out, "restricted") && !strings.Contains(out, "proposal accept") {
|
||||
t.Errorf("expected restricted error mentioning proposal accept; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskCreate_StoryTypeOnlyRestricted(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Errorf("server should not be called for restricted story type")
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
// Just "story" without slash should also be blocked
|
||||
out, err := runCLIProposal(t, tmpDir, cliPath, "task", "create", "--token", "fake",
|
||||
"--type", "story", "--title", "My story", "--project", "PRJ-001")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for restricted story type")
|
||||
}
|
||||
if !strings.Contains(out, "restricted") {
|
||||
t.Errorf("expected restricted error; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Proposal list tests ---
|
||||
|
||||
func TestProposalList_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" || r.URL.Path != "/proposes" {
|
||||
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
json.NewEncoder(w).Encode([]interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"code": "PRJ-001",
|
||||
"title": "My Proposal",
|
||||
"status": "Open",
|
||||
"project_code": "PROJ-001",
|
||||
"created_by": "alice",
|
||||
"created_at": "2026-03-01",
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLIProposal(t, tmpDir, cliPath, "proposal", "list", "--token", "fake")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v; out=%s", err, out)
|
||||
}
|
||||
if !strings.Contains(out, "PRJ-001") {
|
||||
t.Errorf("expected proposal code in output; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProposalList_JSONOutput(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
json.NewEncoder(w).Encode([]interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"code": "PRJ-001",
|
||||
"title": "My Proposal",
|
||||
"status": "Open",
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
writeTestConfig(t, tmpDir, server.URL)
|
||||
cliPath := filepath.Join(tmpDir, "hf")
|
||||
buildCLI(t, cliPath)
|
||||
|
||||
out, err := runCLIProposal(t, tmpDir, cliPath, "--json", "proposal", "list", "--token", "fake")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v; out=%s", err, out)
|
||||
}
|
||||
var resp interface{}
|
||||
if err := json.Unmarshal([]byte(out), &resp); err != nil {
|
||||
t.Fatalf("output is not valid JSON: %s", out)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package help
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"git.hangman-lab.top/zhi/HarborForge.Cli/internal/client"
|
||||
"git.hangman-lab.top/zhi/HarborForge.Cli/internal/config"
|
||||
@@ -223,6 +224,10 @@ func loadPermissionState(token string) permissionState {
|
||||
}
|
||||
|
||||
func has(state permissionState, perm string) bool {
|
||||
// Test/development bypass: HF_TEST_MODE=1 grants all permissions
|
||||
if os.Getenv("HF_TEST_MODE") == "1" {
|
||||
return true
|
||||
}
|
||||
if !state.Known {
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user