From 737599e45430314a8f0a8f0cda54776b3231e0f4 Mon Sep 17 00:00:00 2001 From: Bharat Kunwar Date: Tue, 18 Aug 2026 16:45:57 +0530 Subject: [PATCH] feat: add review detail and answers commands review detail fetches /apiv2/review-entries/{id}/detail (note the plural), the endpoint the web app uses for an entry's live question set. Questions are snapshotted per cycle, so this is authoritative where the review-template endpoint can be stale. review answers reads saved answers for an entry, or one answer via the by-question-id path the web app auto-saves through. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Ruvnu5ui6v2jxCM1dNcKdT --- README.md | 2 ++ cmd/zeltapp/review.go | 47 ++++++++++++++++++++++++++++++ cmd/zeltapp/review_detail_test.go | 48 +++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 cmd/zeltapp/review_detail_test.go diff --git a/README.md b/README.md index bd770ca..b7dc935 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,8 @@ zeltapp review progress zeltapp review participation zeltapp review result [--user me|] zeltapp review entry [--user me|] +zeltapp review detail # entry + the cycle's live question set +zeltapp review answers [] # saved answers zeltapp goal list [--user me|] ``` diff --git a/cmd/zeltapp/review.go b/cmd/zeltapp/review.go index b54adad..bdedb72 100644 --- a/cmd/zeltapp/review.go +++ b/cmd/zeltapp/review.go @@ -21,6 +21,8 @@ func reviewCmd() *cobra.Command { reviewParticipationCmd(), reviewResultCmd(), reviewEntryCmd(), + reviewDetailCmd(), + reviewAnswersCmd(), ) return cmd } @@ -206,6 +208,51 @@ func reviewEntryCmd() *cobra.Command { return cmd } +func reviewDetailCmd() *cobra.Command { + return &cobra.Command{ + Use: "detail ENTRY_UUID", + Short: "Entry detail incl. the cycle's live question set", + Long: "Fetches /apiv2/review-entries/{id}/detail (note the plural), the endpoint the web app uses. Questions are snapshotted per cycle, so this is the authoritative question set for an entry; the review-template endpoint can be stale.", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateOutput(); err != nil { + return err + } + return withClient(func(c *client) error { + var v json.RawMessage + if err := c.do("GET", "/apiv2/review-entries/"+args[0]+"/detail", nil, &v); err != nil { + return err + } + return emit(&resourceView{raw: rawToAny(v)}) + }) + }, + } +} + +func reviewAnswersCmd() *cobra.Command { + return &cobra.Command{ + Use: "answers ENTRY_UUID [QUESTION_UUID]", + Short: "Saved answers for an entry (optionally one question)", + Args: cobra.RangeArgs(1, 2), + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateOutput(); err != nil { + return err + } + return withClient(func(c *client) error { + path := "/apiv2/review-answer/entry/" + args[0] + if len(args) == 2 { + path = "/apiv2/review-answer/" + args[0] + "/" + args[1] + "/by-question-id" + } + var v json.RawMessage + if err := c.do("GET", path, nil, &v); err != nil { + return err + } + return emit(&resourceView{raw: rawToAny(v)}) + }) + }, + } +} + func goalCmd() *cobra.Command { cmd := &cobra.Command{ Use: "goal", diff --git a/cmd/zeltapp/review_detail_test.go b/cmd/zeltapp/review_detail_test.go new file mode 100644 index 0000000..0fe2069 --- /dev/null +++ b/cmd/zeltapp/review_detail_test.go @@ -0,0 +1,48 @@ +package main + +import ( + "encoding/json" + "testing" +) + +// ---------- review detail / answers ---------- + +func TestReview_Detail_UsesPluralEntriesPath(t *testing.T) { + srv, st, _, buf := withTestEnv(t) + _ = srv + st.jsonRoute("GET", "/apiv2/review-entries/abc-123/detail", 200, + []byte(`{"id":"abc-123","questions":[{"id":"q1","questionMain":"What went well?"}]}`)) + if err := runCmd(t, "review", "detail", "abc-123"); err != nil { + t.Fatal(err) + } + var out map[string]any + if err := json.Unmarshal([]byte(buf.String()), &out); err != nil { + t.Fatalf("output not JSON: %s", buf.String()) + } + if out["id"] != "abc-123" { + t.Errorf("expected entry id in output, got %v", out) + } + if len(st.requestsTo("GET", "/apiv2/review-entries/abc-123/detail")) != 1 { + t.Error("expected exactly one request to the plural review-entries detail path") + } +} + +func TestReview_Answers_EntryAndByQuestion(t *testing.T) { + srv, st, _, _ := withTestEnv(t) + _ = srv + st.jsonRoute("GET", "/apiv2/review-answer/entry/abc-123", 200, []byte(`[]`)) + if err := runCmd(t, "review", "answers", "abc-123"); err != nil { + t.Fatal(err) + } + if len(st.requestsTo("GET", "/apiv2/review-answer/entry/abc-123")) != 1 { + t.Error("expected request to the entry answers path") + } + + st.jsonRoute("GET", "/apiv2/review-answer/abc-123/q1/by-question-id", 200, []byte(`{"id":"a1"}`)) + if err := runCmd(t, "review", "answers", "abc-123", "q1"); err != nil { + t.Fatal(err) + } + if len(st.requestsTo("GET", "/apiv2/review-answer/abc-123/q1/by-question-id")) != 1 { + t.Error("expected request to the by-question-id path") + } +}