Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions docs/specifications/contracts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
{
"version": 1,
"owner": "CLI",
"requirements": [
{
"id": "exam.cli-complete-list",
"scope": "private",
"rule": "Without an explicit page or limit, the exam command exhausts all server pages and preserves the total.",
"acceptance": {
"given": "A signed-in user has 104 exams across two REST pages.",
"when": "The real exam command prints JSON without pagination flags.",
"then": [
"All 104 exams, including the last page, are printed once.",
"The combined total and page size are 104."
],
"test": {
"file": "internal/cmd/workspace/exam_test.go",
"name": "TestSpecExamReadsEveryPageAndPreservesTotal/exam.cli-complete-list"
}
}
},
{
"id": "exam.cli-explicit-pagination",
"scope": "private",
"rule": "Explicit page or limit flags select one REST page without auto-fetching further pages.",
"acceptance": {
"given": "An exam command includes page and/or limit plus date and semester filters.",
"when": "The real command requests its list and prints a table.",
"then": [
"Exactly one request uses the selected page and pageSize."
],
"test": {
"file": "internal/cmd/workspace/exam_test.go",
"name": "TestSpecExamExplicitPaginationAndFilters/exam.cli-explicit-pagination"
}
}
},
{
"id": "exam.cli-page-failure",
"scope": "private",
"rule": "A failed later exam page fails the command without printing partial results.",
"acceptance": {
"given": "The first exam page succeeds and the second returns forbidden.",
"when": "The command tries to read the complete exam list.",
"then": [
"The command returns an error.",
"No partial success is printed."
],
"test": {
"file": "internal/cmd/workspace/exam_test.go",
"name": "TestSpecExamLaterPageFailureDoesNotPrintPartialSuccess/exam.cli-page-failure"
}
}
},
{
"id": "exam.cli-positive-input",
"scope": "private",
"rule": "Explicit page, limit and semester identifiers must be positive; limit is at most 100.",
"acceptance": {
"given": "Exam flags contain zero, negative values or limit 101.",
"when": "The command validates explicit flags.",
"then": [
"Invalid input fails before reading exams."
],
"test": {
"file": "internal/cmd/workspace/exam_test.go",
"name": "TestSpecExamRejectsInvalidExplicitPaginationAndSemester/exam.cli-positive-input"
}
}
},
{
"id": "room-map.cli-request",
"scope": "public",
"rule": "Room-map lookup uses the catalog room code route and preserves the returned room code.",
"acceptance": {
"given": "The server provides a highlighted map for B001.",
"when": "The production CLI room-map client requests B001.",
"then": [
"GET /api/catalog/rooms/B001/map is used.",
"The returned code is B001."
],
"test": {
"file": "internal/cmd/room/room_http_test.go",
"name": "TestSpecFetchRoomMapUsesRoomCodePath/room-map.cli-request"
}
}
},
{
"id": "room-map.cli-input",
"scope": "public",
"rule": "Room codes are trimmed and empty codes are rejected.",
"acceptance": {
"given": "Room input has surrounding whitespace or only whitespace.",
"when": "The command normalizes the code.",
"then": [
"Nonempty codes preserve their content after trimming.",
"Whitespace-only input fails."
],
"test": {
"file": "internal/cmd/room/room_test.go",
"name": "TestSpecNormalizeRoomCodeTrimsAndRejectsEmpty/room-map.cli-input"
}
}
},
{
"id": "exam.cli-semester-display",
"scope": "private",
"rule": "Each exam row in the table identifies its semester.",
"acceptance": {
"given": "A returned exam belongs to 2026 Fall.",
"when": "The actual exam command prints its table.",
"then": [
"The table includes the exam semester."
],
"test": {
"file": "internal/cmd/workspace/exam_test.go",
"name": "TestSpecExamSemesterDisplay/exam.cli-semester-display"
}
}
}
]
}
43 changes: 23 additions & 20 deletions internal/cmd/room/room_http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,28 @@ import (
"github.com/Life-USTC/CLI/internal/api"
)

func TestFetchRoomMapUsesRoomCodePath(t *testing.T) {
t.Setenv("LIFE_USTC_CONFIG_DIR", t.TempDir())
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || r.URL.Path != "/api/catalog/rooms/B001/map" {
t.Fatalf("request = %s %s", r.Method, r.URL)
func TestSpecFetchRoomMapUsesRoomCodePath(t *testing.T) {
t.Run("room-map.cli-request", func(t *testing.T) {
t.Setenv("LIFE_USTC_CONFIG_DIR", t.TempDir())
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || r.URL.Path != "/api/catalog/rooms/B001/map" {
t.Fatalf("request = %s %s", r.Method, r.URL)
}
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"code":"B001","status":"highlighted","building":"Main","floor":"1","imageUrl":"https://example.test/map.png"}`)
}))
defer server.Close()
client, err := api.NewTypedClient(server.URL, false)
if err != nil {
t.Fatal(err)
}
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"code":"B001","status":"highlighted","building":"Main","floor":"1","imageUrl":"https://example.test/map.png"}`)
}))
defer server.Close()
client, err := api.NewTypedClient(server.URL, false)
if err != nil {
t.Fatal(err)
}
data, err := fetchRoomMap(client, "B001")
if err != nil {
t.Fatal(err)
}
if got := data.(map[string]any)["code"]; got != "B001" {
t.Fatalf("room code = %#v", got)
}
data, err := fetchRoomMap(client, "B001")
if err != nil {
t.Fatal(err)
}
if got := data.(map[string]any)["code"]; got != "B001" {
t.Fatalf("room code = %#v", got)
}

})
}
17 changes: 10 additions & 7 deletions internal/cmd/room/room_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ package room

import "testing"

func TestNormalizeRoomCodeTrimsAndRejectsEmpty(t *testing.T) {
if got, err := normalizeRoomCode(" 西区 "); err != nil || got != "西区" {
t.Fatalf("normalizeRoomCode = %q, %v", got, err)
}
if _, err := normalizeRoomCode(" "); err == nil {
t.Fatal("normalizeRoomCode accepted empty code")
}
func TestSpecNormalizeRoomCodeTrimsAndRejectsEmpty(t *testing.T) {
t.Run("room-map.cli-input", func(t *testing.T) {
if got, err := normalizeRoomCode(" 西区 "); err != nil || got != "西区" {
t.Fatalf("normalizeRoomCode = %q, %v", got, err)
}
if _, err := normalizeRoomCode(" "); err == nil {
t.Fatal("normalizeRoomCode accepted empty code")
}

})
}
Loading
Loading