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 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) } }