484 lines
14 KiB
Go
484 lines
14 KiB
Go
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)
|
|
}
|
|
}
|