diff --git a/internal/commands/essential.go b/internal/commands/essential.go index f1f6c27..8cf99e0 100644 --- a/internal/commands/essential.go +++ b/internal/commands/essential.go @@ -13,7 +13,6 @@ import ( type essentialResponse struct { ID int `json:"id"` EssentialCode string `json:"essential_code"` - ProposalID int `json:"proposal_id"` Type string `json:"type"` Title string `json:"title"` Description *string `json:"description"` @@ -49,8 +48,7 @@ func RunEssentialList(args []string, tokenFlag string) { output.Errorf("config error: %v", err) } c := client.New(cfg.BaseURL, token) - project := resolveProposalProject(c, proposalCode) - data, err := c.Get("/projects/" + project + "/proposals/" + proposalCode + "/essentials") + data, err := c.Get(proposalPath(c, proposalCode) + "/essentials") if err != nil { output.Errorf("failed to list essentials: %v", err) } @@ -147,8 +145,7 @@ func RunEssentialCreate(args []string, tokenFlag string) { output.Errorf("config error: %v", err) } c := client.New(cfg.BaseURL, token) - project := resolveProposalProject(c, proposalCode) - data, err := c.Post("/projects/"+project+"/proposals/"+proposalCode+"/essentials", bytes.NewReader(body)) + data, err := c.Post(proposalPath(c, proposalCode)+"/essentials", bytes.NewReader(body)) if err != nil { output.Errorf("failed to create essential: %v", err) } @@ -231,8 +228,7 @@ func RunEssentialUpdate(essentialCode string, args []string, tokenFlag string) { output.Errorf("config error: %v", err) } c := client.New(cfg.BaseURL, token) - project := resolveProposalProject(c, proposalCode) - _, err = c.Patch("/projects/"+project+"/proposals/"+proposalCode+"/essentials/"+essentialCode, bytes.NewReader(body)) + _, err = c.Patch(proposalPath(c, proposalCode)+"/essentials/"+essentialCode, bytes.NewReader(body)) if err != nil { output.Errorf("failed to update essential: %v", err) } @@ -269,8 +265,7 @@ func RunEssentialDeleteFull(essentialCode string, args []string, tokenFlag strin output.Errorf("config error: %v", err) } c := client.New(cfg.BaseURL, token) - project := resolveProposalProject(c, proposalCode) - _, err = c.Delete("/projects/" + project + "/proposals/" + proposalCode + "/essentials/" + essentialCode) + _, err = c.Delete(proposalPath(c, proposalCode) + "/essentials/" + essentialCode) if err != nil { output.Errorf("failed to delete essential: %v", err) } diff --git a/internal/commands/meeting.go b/internal/commands/meeting.go index 0d34285..55f4ad9 100644 --- a/internal/commands/meeting.go +++ b/internal/commands/meeting.go @@ -36,7 +36,7 @@ func RunMeetingList(args []string, tokenFlag string) { output.Error("--project requires a value") } i++ - query = appendQuery(query, "project", args[i]) + query = appendQuery(query, "project_code", args[i]) case "--status": if i+1 >= len(args) { output.Error("--status requires a value") diff --git a/internal/commands/milestone.go b/internal/commands/milestone.go index dea241c..69cbe70 100644 --- a/internal/commands/milestone.go +++ b/internal/commands/milestone.go @@ -44,7 +44,7 @@ func RunMilestoneList(args []string, tokenFlag string) { output.Error("--project requires a value") } i++ - query = appendQuery(query, "project", args[i]) + query = appendQuery(query, "project_code", args[i]) case "--status": if i+1 >= len(args) { output.Error("--status requires a value") diff --git a/internal/commands/proposal_test.go b/internal/commands/proposal_test.go index c0c89a9..db41ce3 100644 --- a/internal/commands/proposal_test.go +++ b/internal/commands/proposal_test.go @@ -170,20 +170,29 @@ func TestEssentialDelete_MissingProposal(t *testing.T) { 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" { + switch { + case r.Method == "GET" && r.URL.Path == "/projects": + w.WriteHeader(200) + json.NewEncoder(w).Encode([]interface{}{map[string]interface{}{"id": 1, "project_code": "PROJ-001"}}) + case r.Method == "GET" && r.URL.Path == "/projects/PROJ-001/proposals/PRJ-001": + w.WriteHeader(200) + json.NewEncoder(w).Encode(map[string]interface{}{"code": "PRJ-001", "project_code": "PROJ-001"}) + case r.Method == "GET" && r.URL.Path == "/projects/PROJ-001/proposals/PRJ-001/essentials": + 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", + }, + }) + default: t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + w.WriteHeader(404) } - 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() @@ -204,16 +213,25 @@ func TestEssentialList_JSONOutput(t *testing.T) { 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) + switch { + case r.Method == "GET" && r.URL.Path == "/projects": + w.WriteHeader(200) + json.NewEncoder(w).Encode([]interface{}{map[string]interface{}{"id": 1, "project_code": "PROJ-001"}}) + case r.Method == "GET" && r.URL.Path == "/projects/PROJ-001/proposals/PRJ-001": + w.WriteHeader(200) + json.NewEncoder(w).Encode(map[string]interface{}{"code": "PRJ-001", "project_code": "PROJ-001"}) + case r.Method == "POST" && r.URL.Path == "/projects/PROJ-001/proposals/PRJ-001/essentials": + w.WriteHeader(200) + json.NewEncoder(w).Encode(map[string]interface{}{ + "id": 1, + "essential_code": "ESS-001", + "title": "Add login", + "type": "feature", + }) + default: + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + w.WriteHeader(404) } - w.WriteHeader(200) - json.NewEncoder(w).Encode(map[string]interface{}{ - "id": 1, - "essential_code": "ESS-001", - "title": "Add login", - "type": "feature", - }) })) defer server.Close() @@ -234,11 +252,20 @@ func TestEssentialCreate_Success(t *testing.T) { 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) + switch { + case r.Method == "GET" && r.URL.Path == "/projects": + w.WriteHeader(200) + json.NewEncoder(w).Encode([]interface{}{map[string]interface{}{"id": 1, "project_code": "PROJ-001"}}) + case r.Method == "GET" && r.URL.Path == "/projects/PROJ-001/proposals/PRJ-001": + w.WriteHeader(200) + json.NewEncoder(w).Encode(map[string]interface{}{"code": "PRJ-001", "project_code": "PROJ-001"}) + case r.Method == "PATCH" && r.URL.Path == "/projects/PROJ-001/proposals/PRJ-001/essentials/ESS-001": + w.WriteHeader(200) + w.Write([]byte(`{}`)) + default: + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + w.WriteHeader(404) } - w.WriteHeader(200) - w.Write([]byte(`{}`)) })) defer server.Close() @@ -259,11 +286,20 @@ func TestEssentialUpdate_Success(t *testing.T) { 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) + switch { + case r.Method == "GET" && r.URL.Path == "/projects": + w.WriteHeader(200) + json.NewEncoder(w).Encode([]interface{}{map[string]interface{}{"id": 1, "project_code": "PROJ-001"}}) + case r.Method == "GET" && r.URL.Path == "/projects/PROJ-001/proposals/PRJ-001": + w.WriteHeader(200) + json.NewEncoder(w).Encode(map[string]interface{}{"code": "PRJ-001", "project_code": "PROJ-001"}) + case r.Method == "DELETE" && r.URL.Path == "/projects/PROJ-001/proposals/PRJ-001/essentials/ESS-001": + w.WriteHeader(200) + w.Write([]byte(`{}`)) + default: + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + w.WriteHeader(404) } - w.WriteHeader(200) - w.Write([]byte(`{}`)) })) defer server.Close() @@ -306,20 +342,29 @@ func TestProposalAccept_MissingMilestone(t *testing.T) { 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" { + switch { + case r.Method == "GET" && r.URL.Path == "/projects": + w.WriteHeader(200) + json.NewEncoder(w).Encode([]interface{}{map[string]interface{}{"id": 1, "project_code": "PROJ-001"}}) + case r.Method == "GET" && r.URL.Path == "/projects/PROJ-001/proposals/PRJ-001": + w.WriteHeader(200) + json.NewEncoder(w).Encode(map[string]interface{}{"code": "PRJ-001", "project_code": "PROJ-001"}) + case r.Method == "POST" && r.URL.Path == "/projects/PROJ-001/proposals/PRJ-001/accept": + 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{}{}, + }) + default: t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + w.WriteHeader(404) } - 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() @@ -421,7 +466,7 @@ func TestTaskCreate_StoryTypeOnlyRestricted(t *testing.T) { 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" { + if r.Method != "GET" || r.URL.Path != "/projects/PROJ-001/proposals" { t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) } w.WriteHeader(200) @@ -444,7 +489,7 @@ func TestProposalList_Success(t *testing.T) { cliPath := filepath.Join(tmpDir, "hf") buildCLI(t, cliPath) - out, err := runCLIProposal(t, tmpDir, cliPath, "proposal", "list", "--token", "fake") + out, err := runCLIProposal(t, tmpDir, cliPath, "proposal", "list", "--token", "fake", "--project", "PROJ-001") if err != nil { t.Fatalf("unexpected error: %v; out=%s", err, out) } @@ -455,6 +500,9 @@ func TestProposalList_Success(t *testing.T) { func TestProposalList_JSONOutput(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" || r.URL.Path != "/projects/PROJ-001/proposals" { + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + } w.WriteHeader(200) json.NewEncoder(w).Encode([]interface{}{ map[string]interface{}{ @@ -472,7 +520,7 @@ func TestProposalList_JSONOutput(t *testing.T) { cliPath := filepath.Join(tmpDir, "hf") buildCLI(t, cliPath) - out, err := runCLIProposal(t, tmpDir, cliPath, "--json", "proposal", "list", "--token", "fake") + out, err := runCLIProposal(t, tmpDir, cliPath, "--json", "proposal", "list", "--token", "fake", "--project", "PROJ-001") if err != nil { t.Fatalf("unexpected error: %v; out=%s", err, out) } diff --git a/internal/commands/propose.go b/internal/commands/propose.go index d572aea..93226a5 100644 --- a/internal/commands/propose.go +++ b/internal/commands/propose.go @@ -32,21 +32,30 @@ type projectLookup struct { func resolveProposalProject(c *client.Client, proposalCode string) string { data, err := c.Get("/projects") if err != nil { - output.Errorf("failed to list projects for proposal lookup: %v", err) + return "" } var projects []projectLookup if err := json.Unmarshal(data, &projects); err != nil { - output.Errorf("cannot parse project list for proposal lookup: %v", err) + return "" } for _, p := range projects { + if p.ProjectCode == "" { + continue + } if _, err := c.Get("/projects/" + p.ProjectCode + "/proposals/" + proposalCode); err == nil { return p.ProjectCode } } - output.Errorf("proposal not found: %s", proposalCode) return "" } +func proposalPath(c *client.Client, proposalCode string) string { + if project := resolveProposalProject(c, proposalCode); project != "" { + return "/projects/" + project + "/proposals/" + proposalCode + } + return "/proposes/" + proposalCode +} + // RunProposeList implements `hf propose list --project `. func RunProposeList(args []string, tokenFlag string) { token := ResolveToken(tokenFlag) @@ -77,8 +86,9 @@ func RunProposeList(args []string, tokenFlag string) { output.Errorf("unknown flag: %s", args[i]) } } + legacyPath := false if project == "" { - output.Error("usage: hf propose list --project [--status ] [--order-by ]") + legacyPath = true } cfg, err := config.Load() @@ -87,6 +97,9 @@ func RunProposeList(args []string, tokenFlag string) { } c := client.New(cfg.BaseURL, token) path := "/projects/" + project + "/proposals" + if legacyPath { + path = "/proposes" + } if encoded := query.Encode(); encoded != "" { path += "?" + encoded } @@ -133,8 +146,7 @@ func RunProposeGet(proposeCode, tokenFlag string) { output.Errorf("config error: %v", err) } c := client.New(cfg.BaseURL, token) - project := resolveProposalProject(c, proposeCode) - data, err := c.Get("/projects/" + project + "/proposals/" + proposeCode) + data, err := c.Get(proposalPath(c, proposeCode)) if err != nil { output.Errorf("failed to get proposal: %v", err) } @@ -281,8 +293,7 @@ func RunProposeUpdate(proposeCode string, args []string, tokenFlag string) { output.Errorf("config error: %v", err) } c := client.New(cfg.BaseURL, token) - project := resolveProposalProject(c, proposeCode) - _, err = c.Patch("/projects/"+project+"/proposals/"+proposeCode, bytes.NewReader(body)) + _, err = c.Patch(proposalPath(c, proposeCode), bytes.NewReader(body)) if err != nil { output.Errorf("failed to update proposal: %v", err) } @@ -340,8 +351,7 @@ func RunProposeAccept(proposeCode string, args []string, tokenFlag string) { output.Errorf("config error: %v", err) } c := client.New(cfg.BaseURL, token) - project := resolveProposalProject(c, proposeCode) - data, err := c.Post("/projects/"+project+"/proposals/"+proposeCode+"/accept", bytes.NewReader(body)) + data, err := c.Post(proposalPath(c, proposeCode)+"/accept", bytes.NewReader(body)) if err != nil { output.Errorf("failed to accept proposal: %v", err) } @@ -362,7 +372,7 @@ func RunProposeAccept(proposeCode string, args []string, tokenFlag string) { if err := json.Unmarshal(data, &resp); err == nil && len(resp.GeneratedTasks) > 0 { fmt.Printf("\nGenerated %d story task(s):\n", len(resp.GeneratedTasks)) for _, gt := range resp.GeneratedTasks { - code := "" + code := "(no task_code)" if gt.TaskCode != nil { code = *gt.TaskCode } @@ -410,8 +420,7 @@ func RunProposeReject(proposeCode string, args []string, tokenFlag string) { output.Errorf("config error: %v", err) } c := client.New(cfg.BaseURL, token) - project := resolveProposalProject(c, proposeCode) - _, err = c.Post("/projects/"+project+"/proposals/"+proposeCode+"/reject", body) + _, err = c.Post(proposalPath(c, proposeCode)+"/reject", body) if err != nil { output.Errorf("failed to reject proposal: %v", err) } @@ -428,8 +437,7 @@ func RunProposeReopen(proposeCode, tokenFlag string) { output.Errorf("config error: %v", err) } c := client.New(cfg.BaseURL, token) - project := resolveProposalProject(c, proposeCode) - _, err = c.Post("/projects/"+project+"/proposals/"+proposeCode+"/reopen", nil) + _, err = c.Post(proposalPath(c, proposeCode)+"/reopen", nil) if err != nil { output.Errorf("failed to reopen proposal: %v", err) } diff --git a/internal/commands/task.go b/internal/commands/task.go index be2c30d..7f6a5ab 100644 --- a/internal/commands/task.go +++ b/internal/commands/task.go @@ -38,13 +38,13 @@ func RunTaskList(args []string, tokenFlag string) { output.Error("--project requires a value") } i++ - query = appendQuery(query, "project", args[i]) + query = appendQuery(query, "project_code", args[i]) case "--milestone": if i+1 >= len(args) { output.Error("--milestone requires a value") } i++ - query = appendQuery(query, "milestone", args[i]) + query = appendQuery(query, "milestone_code", args[i]) case "--status": if i+1 >= len(args) { output.Error("--status requires a value") @@ -426,7 +426,7 @@ func RunTaskSearch(args []string, tokenFlag string) { output.Error("--project requires a value") } i++ - query = appendQuery(query, "project", args[i]) + query = appendQuery(query, "project_code", args[i]) case "--status": if i+1 >= len(args) { output.Error("--status requires a value")