diff --git a/internal/app/schema_calendar_shortcut_contract_test.go b/internal/app/schema_calendar_shortcut_contract_test.go index bd39b780..6e93fea2 100644 --- a/internal/app/schema_calendar_shortcut_contract_test.go +++ b/internal/app/schema_calendar_shortcut_contract_test.go @@ -28,4 +28,22 @@ func TestCrossPlatformCoverageCalendarAgendaFinalSchemaPreservesCompositePropert if got := schemaContractString(tool["interface_mode"]); got != "composite" { t.Fatalf("calendar.shortcut_agenda interface_mode=%q, want composite", got) } + result := schemaContractMap(tool["result"]) + dataSchema := schemaContractMap(result["data_schema"]) + properties := schemaContractMap(dataSchema["properties"]) + for _, field := range []string{"hasMore", "nextCursor"} { + if _, exists := properties[field]; exists { + t.Fatalf("calendar.shortcut_agenda Result data_schema leaked pagination field %q", field) + } + } + if properties["complete"] == nil { + t.Fatal("calendar.shortcut_agenda Result data_schema is missing complete") + } + pagination, ok := tool["pagination"].(map[string]any) + if !ok { + t.Fatalf("calendar.shortcut_agenda pagination=%T, want object", tool["pagination"]) + } + if got := schemaContractString(pagination["meta_path"]); got != "meta.pagination" { + t.Fatalf("calendar.shortcut_agenda pagination meta_path=%q, want meta.pagination", got) + } } diff --git a/internal/shortcut/calendar/calendar_additional_cross_platform_coverage_test.go b/internal/shortcut/calendar/calendar_additional_cross_platform_coverage_test.go index 840fc61e..e728844d 100644 --- a/internal/shortcut/calendar/calendar_additional_cross_platform_coverage_test.go +++ b/internal/shortcut/calendar/calendar_additional_cross_platform_coverage_test.go @@ -4,6 +4,9 @@ package calendar import ( + "bytes" + "context" + "encoding/json" "math" "strconv" "strings" @@ -39,6 +42,21 @@ func TestCrossPlatformCoverageCalendarCommonBranches(t *testing.T) { if calendarReadShortcut("+x", "x", "x", "items", nil, nil, nil).Contract.Result == nil { t.Fatal("collection read shortcut did not build a result") } + collectionResult := calendarCollectionResult("items", "items") + var collectionSchema struct { + Properties map[string]json.RawMessage `json:"properties"` + } + if err := json.Unmarshal(collectionResult.DataSchema, &collectionSchema); err != nil { + t.Fatalf("decode collection result schema: %v", err) + } + for _, field := range []string{"hasMore", "nextCursor"} { + if _, exists := collectionSchema.Properties[field]; exists { + t.Fatalf("pagination field %q leaked into business Result schema", field) + } + } + if _, exists := collectionSchema.Properties["complete"]; !exists { + t.Fatal("collection Result schema lost business completeness evidence") + } if calendarWriteShortcut("+x", "x", "x", "dws calendar +x", nil, nil, nil).Safety.Confirmation != "user_required" { t.Fatal("write shortcut did not build safety") } @@ -178,6 +196,44 @@ func TestCrossPlatformCoverageCalendarCommonBranches(t *testing.T) { if err := runCalendarCoverage(t, legacyPage, &calendarCoverageCaller{responses: map[string][]string{}}, "--event", "event-1"); err != nil { t.Fatalf("legacy pagination output: %v", err) } + + unifiedPage := EventGet + unifiedPage.OutputRollout = output.RolloutUnifiedActive + cmd := corecmd.New(shortcut.FromShortcut(unifiedPage)) + ctx, _ := output.WithResultStore(context.Background()) + cmd.SetContext(ctx) + var stdout bytes.Buffer + cmd.SetOut(&stdout) + rt := shortcut.RuntimeContextForTest(cmd, unifiedPage) + if err := outputCalendarPage(rt, map[string]any{"count": 1, "items": []any{map[string]any{"id": "item-1"}}}, calendarPageEvidence{Known: true, HasMore: true, NextCursor: "cursor-2"}); err != nil { + t.Fatalf("store unified pagination result: %v", err) + } + if code, emitted, err := output.EmitStoredResult(cmd); err != nil || !emitted || code != 0 { + t.Fatalf("emit unified pagination result: code=%d emitted=%v err=%v", code, emitted, err) + } + var envelope struct { + Data map[string]any `json:"data"` + Meta struct { + Pagination *struct { + EndpointExhausted bool `json:"endpoint_exhausted"` + NextToken string `json:"next_token"` + } `json:"pagination"` + } `json:"meta"` + } + if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil { + t.Fatalf("decode unified pagination output: %v\n%s", err, stdout.String()) + } + for _, field := range []string{"hasMore", "nextCursor"} { + if _, exists := envelope.Data[field]; exists { + t.Fatalf("pagination field %q leaked into business data: %s", field, stdout.String()) + } + } + if envelope.Data["complete"] != false { + t.Fatalf("business completeness=%#v, want false: %s", envelope.Data["complete"], stdout.String()) + } + if envelope.Meta.Pagination == nil || envelope.Meta.Pagination.EndpointExhausted || envelope.Meta.Pagination.NextToken != "cursor-2" { + t.Fatalf("pagination meta=%+v: %s", envelope.Meta.Pagination, stdout.String()) + } } func TestCrossPlatformCoverageCalendarEventGetSearchAndSuggestion(t *testing.T) { diff --git a/internal/shortcut/calendar/common.go b/internal/shortcut/calendar/common.go index 988f7fdd..e0d7c27f 100644 --- a/internal/shortcut/calendar/common.go +++ b/internal/shortcut/calendar/common.go @@ -66,7 +66,7 @@ func calendarCollectionResult(collection, description string) *contract.ResultSp return &contract.ResultSpec{ Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomeFailure}, DataSchema: json.RawMessage(fmt.Sprintf( - `{"type":"object","description":%q,"properties":{"count":{"type":"integer","description":"本页有效业务记录数量"},%q:{"type":"array","description":%q,"items":{"type":"object","description":"Calendar 业务条目","additionalProperties":true}},"complete":{"type":"boolean","description":"服务端分页证据是否证明结果已完整"},"hasMore":{"type":"boolean","description":"服务端是否明确仍有后续页"},"nextCursor":{"type":"string","description":"服务端返回的下一页游标"}},"required":["count",%q,"complete"],"additionalProperties":true}`, + `{"type":"object","description":%q,"properties":{"count":{"type":"integer","description":"本页有效业务记录数量"},%q:{"type":"array","description":%q,"items":{"type":"object","description":"Calendar 业务条目","additionalProperties":true}},"complete":{"type":"boolean","description":"服务端分页证据是否证明结果已完整"}},"required":["count",%q,"complete"],"additionalProperties":true}`, description, collection, description, collection, )), }