fix(recruit): align result and cursor contracts

This commit is contained in:
恋川
2026-08-13 15:37:49 +08:00
parent 8ee9fc3f48
commit 5ef52503ae
3 changed files with 314 additions and 42 deletions
@@ -0,0 +1,62 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package app
import (
"bytes"
"encoding/json"
"reflect"
"testing"
)
func recruitSchemaLeaf(t *testing.T, canonical string, compact bool) map[string]any {
t.Helper()
root := NewRootCommand()
var stdout, stderr bytes.Buffer
root.SetOut(&stdout)
root.SetErr(&stderr)
args := []string{"schema", canonical, "--format", "json"}
if compact {
args = append(args, "--compact")
}
root.SetArgs(args)
if err := root.Execute(); err != nil {
t.Fatalf("execute schema %s compact=%v: %v; stderr=%s", canonical, compact, err, stderr.String())
}
var payload map[string]any
if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil {
t.Fatalf("decode schema %s compact=%v: %v", canonical, compact, err)
}
return payload
}
func TestRecruitDeliveredSchemaPublishesResultAndPagination(t *testing.T) {
for _, canonical := range []string{"recruit.list_jobs", "recruit.get_job_detail", "recruit.create_job"} {
t.Run(canonical, func(t *testing.T) {
full := recruitSchemaLeaf(t, canonical, false)
compact := recruitSchemaLeaf(t, canonical, true)
if full["result"] == nil || compact["result"] == nil {
t.Fatalf("result missing: full=%#v compact=%#v", full["result"], compact["result"])
}
if !reflect.DeepEqual(full["result"], compact["result"]) {
t.Fatalf("full/compact result mismatch\nfull=%#v\ncompact=%#v", full["result"], compact["result"])
}
if canonical == "recruit.list_jobs" {
if full["pagination"] == nil || compact["pagination"] == nil {
t.Fatalf("list pagination missing: full=%#v compact=%#v", full["pagination"], compact["pagination"])
}
if !reflect.DeepEqual(full["pagination"], compact["pagination"]) {
t.Fatalf("full/compact pagination mismatch\nfull=%#v\ncompact=%#v", full["pagination"], compact["pagination"])
}
parameters, _ := full["parameters"].(map[string]any)
cursor, _ := parameters["cursor"].(map[string]any)
if cursor["type"] != "string" || cursor["interface_type"] != "number" {
t.Fatalf("cursor contract = %#v, want CLI string converted to MCP number", cursor)
}
} else if full["pagination"] != nil || compact["pagination"] != nil {
t.Fatalf("non-list pagination must be absent: full=%#v compact=%#v", full["pagination"], compact["pagination"])
}
})
}
}
+111 -30
View File
@@ -7,12 +7,14 @@ import (
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"github.com/spf13/cobra"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
)
const (
@@ -30,7 +32,7 @@ var recruitDryRun = &contract.DryRunSpec{
var (
recruitListResult = &contract.ResultSpec{
Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomeFailure},
DataSchema: json.RawMessage(`{"type":"object","description":"当前页招聘职位查询结果","properties":{"jobs":{"type":"array","description":"当前页职位记录","items":{"type":"object","description":"招聘职位摘要","properties":{"jobId":{"type":"string","description":"职位 ID"},"name":{"type":"string","description":"职位名称"},"status":{"type":"number","description":"职位状态枚举值"}},"additionalProperties":true}},"hasMore":{"type":"boolean","description":"服务端是否还有下一页"},"nextCursor":{"description":"下一页游标;仅在 hasMore 为 true 时用于续查","oneOf":[{"type":"number"},{"type":"string"}]}},"additionalProperties":true}`),
DataSchema: json.RawMessage(`{"type":"object","description":"当前页招聘职位查询结果;续页信息位于 meta.pagination","properties":{"jobs":{"type":"array","description":"当前页职位记录","items":{"type":"object","description":"招聘职位摘要","properties":{"jobId":{"type":"string","description":"职位 ID"},"name":{"type":"string","description":"职位名称"},"status":{"type":"number","description":"职位状态枚举值"}},"additionalProperties":true}}},"additionalProperties":true}`),
}
recruitJobDetailResult = &contract.ResultSpec{
Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomeFailure},
@@ -78,13 +80,14 @@ func newRecruitCommand() *cobra.Command {
func newRecruitJobListCommand() *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "list",
Short: "查询招聘职位列表",
Long: "按职位 ID、关键词、状态、创建人、职位性质等条件分页查询招聘职位。",
Example: " dws recruit job list --keyword Java --status open --size 20 --format json\n dws recruit job list --job-ids JOB_ID_1,JOB_ID_2 --format json",
Server: recruitServerID,
Tool: recruitListJobsTool,
Safety: recruitSafetyRead(),
Use: "list",
Short: "查询招聘职位列表",
Long: "按职位 ID、关键词、状态、创建人、职位性质等条件分页查询招聘职位。",
Example: " dws recruit job list --keyword Java --status open --size 20 --format json\n dws recruit job list --job-ids JOB_ID_1,JOB_ID_2 --format json",
Server: recruitServerID,
Tool: recruitListJobsTool,
Safety: recruitSafetyRead(),
OutputRollout: output.RolloutUnifiedActive,
Flags: []LeafFlag{
{Name: "job-ids", Usage: "职位 ID,多个值用逗号分隔", Kind: LeafStringSlice, Bind: "jobIds"},
{Name: "required-edu", Usage: "学历要求枚举值", Kind: LeafInt, Bind: "requiredEdu"},
@@ -96,11 +99,11 @@ func newRecruitJobListCommand() *cobra.Command {
{Name: "creator-user-ids", Usage: "创建人 userId,多个值用逗号分隔", Kind: LeafStringSlice, Bind: "creatorUserIds"},
{Name: "keyword", Usage: "职位搜索关键词", Bind: "keyword", Trim: true, OmitEmpty: true},
{Name: "category", Usage: "职位分类", Bind: "category", Trim: true, OmitEmpty: true},
{Name: "cursor", Usage: "分页游标;首次查询不传,翻页时传返回的 nextCursor", Kind: LeafInt, Bind: "cursor"},
{Name: "cursor", Usage: "分页游标;首次查询不传,翻页时原样回填返回的 nextCursor", Bind: "cursor", Trim: true, OmitEmpty: true, Transform: transformRecruitCursor},
{Name: "size", Usage: "分页大小,默认 20", Kind: LeafInt, Bind: "size", ArgDefault: "20"},
},
Validate: validateRecruitList,
Call: runRecruitJobList,
Validate: validateRecruitList,
ResultCall: recruitResultCall,
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{ProductID: "recruit", Name: recruitListJobsTool, CanonicalPath: "recruit.list_jobs", CLIPath: "recruit job list", PrimaryCLIPath: "recruit job list"},
Description: "按条件分页查询招聘职位",
@@ -119,7 +122,7 @@ func newRecruitJobListCommand() *cobra.Command {
})
}
func runRecruitJobList(_ *cobra.Command, tool string, args map[string]any) error {
func recruitListToolArgs(args map[string]any) map[string]any {
params := map[string]any{"param": map[string]any{}, "size": args["size"]}
query := params["param"].(map[string]any)
for key, value := range args {
@@ -131,18 +134,88 @@ func runRecruitJobList(_ *cobra.Command, tool string, args map[string]any) error
query[key] = value
}
}
return callMCPToolOnServer(recruitServerID, tool, params)
return params
}
func recruitResultCall(cmd *cobra.Command, tool string, args map[string]any) (output.CommandResult, error) {
toolArgs := args
if tool == recruitListJobsTool {
toolArgs = recruitListToolArgs(args)
}
if deps.Caller.DryRun() {
return output.Success(map[string]any{
"tool": tool, "arguments": toolArgs, "executed": false,
}, output.WithDryRun()), nil
}
data, err := CallMCPToolDataOnServer(cmd.Context(), recruitServerID, tool, toolArgs)
if err != nil {
return nil, err
}
if tool != recruitListJobsTool {
return output.Success(data), nil
}
clean, meta, err := recruitListResultData(data)
if err != nil {
return output.Failure(&output.ErrorInfo{
Type: "api", Subtype: "pagination_inconsistent", Message: err.Error(),
Hint: "保留原始响应并停止翻页;不要把当前页当作完整结果。",
}), nil
}
return output.Success(clean, output.WithMeta(meta)), nil
}
func recruitListResultData(data any) (any, *output.Meta, error) {
object, ok := data.(map[string]any)
if !ok {
return nil, nil, fmt.Errorf("list_jobs 返回值必须是 JSON 对象")
}
hasMore, ok := object["hasMore"].(bool)
if !ok {
return nil, nil, fmt.Errorf("list_jobs 返回值缺少布尔字段 hasMore")
}
nextToken := ""
if raw, exists := object["nextCursor"]; exists && raw != nil {
switch value := raw.(type) {
case string:
nextToken = strings.TrimSpace(value)
case float64:
nextToken = strconv.FormatFloat(value, 'f', -1, 64)
default:
return nil, nil, fmt.Errorf("list_jobs 的 nextCursor 必须是字符串或数字")
}
}
if hasMore && nextToken == "" {
return nil, nil, fmt.Errorf("list_jobs 返回 hasMore=true 但缺少 nextCursor")
}
if !hasMore {
nextToken = ""
}
clean := make(map[string]any, len(object)-2)
for key, value := range object {
if key != "hasMore" && key != "nextCursor" {
clean[key] = value
}
}
pagination := &output.Pagination{EndpointExhausted: !hasMore, NextToken: nextToken, Pages: 1}
meta := &output.Meta{Pagination: pagination}
if jobs, exists := clean["jobs"].([]any); exists {
meta.Count = output.NewCount(len(jobs))
pagination.Items = len(jobs)
}
return clean, meta, nil
}
func newRecruitJobGetCommand() *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "get",
Short: "查询招聘职位详情",
Long: "根据职位 ID 查询招聘职位详情。",
Example: " dws recruit job get --job-id JOB_ID --format json",
Server: recruitServerID,
Tool: recruitGetJobTool,
Safety: recruitSafetyRead(),
Use: "get",
Short: "查询招聘职位详情",
Long: "根据职位 ID 查询招聘职位详情。",
Example: " dws recruit job get --job-id JOB_ID --format json",
Server: recruitServerID,
Tool: recruitGetJobTool,
Safety: recruitSafetyRead(),
OutputRollout: output.RolloutUnifiedActive,
ResultCall: recruitResultCall,
Flags: []LeafFlag{{
Name: "job-id", Usage: "职位 ID(必填)", Bind: "jobId", Trim: true,
Required: true, RequiredHint: "--job-id 为必填",
@@ -166,13 +239,15 @@ func newRecruitJobGetCommand() *cobra.Command {
func newRecruitJobCreateCommand() *cobra.Command {
return NewLeafCommand(LeafSpec{
Use: "create",
Short: "创建招聘职位",
Long: "从 JSON 文件读取职位信息并创建招聘职位。该操作会写入远端招聘系统,执行前必须确认。",
Example: " dws recruit job create --from ./job.json --dry-run --format json\n dws recruit job create --from ./job.json --format json",
Server: recruitServerID,
Tool: recruitCreateJobTool,
Safety: recruitSafetyCreate(),
Use: "create",
Short: "创建招聘职位",
Long: "从 JSON 文件读取职位信息并创建招聘职位。该操作会写入远端招聘系统,执行前必须确认。",
Example: " dws recruit job create --from ./job.json --dry-run --format json\n dws recruit job create --from ./job.json --format json",
Server: recruitServerID,
Tool: recruitCreateJobTool,
Safety: recruitSafetyCreate(),
OutputRollout: output.RolloutUnifiedActive,
ResultCall: recruitResultCall,
Flags: []LeafFlag{{
Name: "from", Usage: "职位 JSON 文件路径(必填)", Bind: "atsAddJobParam", Trim: true,
Required: true, RequiredHint: "--from 为必填", Transform: loadRecruitJobFile,
@@ -249,10 +324,16 @@ func transformRecruitStatuses(raw string) (any, error) {
return statuses, nil
}
func validateRecruitList(cmd *cobra.Command, _ []string) error {
if cursor, _ := cmd.Flags().GetInt("cursor"); cursor < 0 {
return apperrors.NewValidation("--cursor 必须大于或等于 0")
func transformRecruitCursor(raw string) (any, error) {
value := strings.TrimSpace(raw)
cursor, err := strconv.ParseInt(value, 10, 64)
if err != nil || cursor < 0 {
return nil, apperrors.NewValidation("--cursor 必须是大于或等于 0 的整数")
}
return cursor, nil
}
func validateRecruitList(cmd *cobra.Command, _ []string) error {
size, _ := cmd.Flags().GetInt("size")
if cmd.Flags().Changed("size") && (size < 1 || size > 100) {
return apperrors.NewValidation("--size 必须在 1 到 100 之间")
+141 -12
View File
@@ -14,9 +14,12 @@ import (
"strings"
"testing"
"github.com/spf13/cobra"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contractfinal"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
)
@@ -31,6 +34,9 @@ type recruitCaptureCaller struct {
tool string
args map[string]any
calls []recruitCapturedCall
err error
text string
dryRun bool
}
func (c *recruitCaptureCaller) CallTool(_ context.Context, productID, tool string, args map[string]any) (*edition.ToolResult, error) {
@@ -38,11 +44,24 @@ func (c *recruitCaptureCaller) CallTool(_ context.Context, productID, tool strin
c.tool = tool
c.args = args
c.calls = append(c.calls, recruitCapturedCall{productID: productID, tool: tool, args: args})
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: `{}`}}}, nil
if c.err != nil {
return nil, c.err
}
text := c.text
if text == "" {
text = `{}`
switch tool {
case recruitListJobsTool:
text = `{"jobs":[],"hasMore":false}`
case recruitCreateJobTool:
text = `{"jobId":"created-job"}`
}
}
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: text}}}, nil
}
func (c *recruitCaptureCaller) Format() string { return "json" }
func (c *recruitCaptureCaller) DryRun() bool { return false }
func (c *recruitCaptureCaller) DryRun() bool { return c.dryRun }
func (c *recruitCaptureCaller) Fields() string { return "" }
func (c *recruitCaptureCaller) JQ() string { return "" }
@@ -54,9 +73,15 @@ func withRecruitCaller(t *testing.T) *recruitCaptureCaller {
return caller
}
func prepareRecruitTestCommand(cmd *cobra.Command) *cobra.Command {
ctx, _ := output.WithResultStore(context.Background())
cmd.SetContext(ctx)
return cmd
}
func TestRecruitJobListWrapsQueryParameters(t *testing.T) {
caller := withRecruitCaller(t)
cmd := newRecruitJobListCommand()
cmd := prepareRecruitTestCommand(newRecruitJobListCommand())
cmd.SetArgs([]string{
"--keyword", "Java", "--status", "open,draft", "--creator-user-ids", "u1,u2",
"--campus=false", "--cursor", "10", "--size", "30",
@@ -67,7 +92,7 @@ func TestRecruitJobListWrapsQueryParameters(t *testing.T) {
if caller.productID != recruitServerID || caller.tool != recruitListJobsTool {
t.Fatalf("dispatch = %s/%s, want %s/%s", caller.productID, caller.tool, recruitServerID, recruitListJobsTool)
}
if caller.args["cursor"] != 10 || caller.args["size"] != 30 {
if caller.args["cursor"] != int64(10) || caller.args["size"] != 30 {
t.Fatalf("pagination args = %#v", caller.args)
}
param, ok := caller.args["param"].(map[string]any)
@@ -87,9 +112,29 @@ func TestRecruitJobListWrapsQueryParameters(t *testing.T) {
}
}
func TestRecruitJobListCursorRoundTrip(t *testing.T) {
caller := withRecruitCaller(t)
cmd := prepareRecruitTestCommand(newRecruitJobListCommand())
cmd.SetArgs([]string{"--cursor", "9223372036854775807"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
if got := caller.args["cursor"]; got != int64(9223372036854775807) {
t.Fatalf("cursor = %#v (%T), want max int64", got, got)
}
for _, cursor := range []string{"not-a-cursor", "9223372036854775808"} {
invalid := prepareRecruitTestCommand(newRecruitJobListCommand())
invalid.SetArgs([]string{"--cursor", cursor})
if err := invalid.Execute(); err == nil || !strings.Contains(err.Error(), "大于或等于 0 的整数") {
t.Fatalf("cursor %q error = %v", cursor, err)
}
}
}
func TestRecruitJobListDefaultsAndValidation(t *testing.T) {
caller := withRecruitCaller(t)
cmd := newRecruitJobListCommand()
cmd := prepareRecruitTestCommand(newRecruitJobListCommand())
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
@@ -100,13 +145,13 @@ func TestRecruitJobListDefaultsAndValidation(t *testing.T) {
t.Fatalf("first page unexpectedly sent cursor: %#v", caller.args)
}
badStatus := newRecruitJobListCommand()
badStatus := prepareRecruitTestCommand(newRecruitJobListCommand())
badStatus.SetArgs([]string{"--status", "unknown"})
if err := badStatus.Execute(); err == nil || !strings.Contains(err.Error(), "draft/open/invalid/closed") {
t.Fatalf("invalid status error = %v", err)
}
badSize := newRecruitJobListCommand()
badSize := prepareRecruitTestCommand(newRecruitJobListCommand())
badSize.SetArgs([]string{"--size", "101"})
if err := badSize.Execute(); err == nil || !strings.Contains(err.Error(), "1 到 100") {
t.Fatalf("invalid size error = %v", err)
@@ -115,7 +160,7 @@ func TestRecruitJobListDefaultsAndValidation(t *testing.T) {
func TestRecruitJobGetDispatchesJobID(t *testing.T) {
caller := withRecruitCaller(t)
cmd := newRecruitJobGetCommand()
cmd := prepareRecruitTestCommand(newRecruitJobGetCommand())
cmd.SetArgs([]string{"--job-id", "job-1"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
@@ -145,7 +190,7 @@ func writeRecruitJobFixture(t *testing.T) (string, map[string]any) {
func TestRecruitJobCreateRequiresConfirmationBeforeRemoteCall(t *testing.T) {
caller := withRecruitCaller(t)
path, _ := writeRecruitJobFixture(t)
cmd := newRecruitJobCreateCommand()
cmd := prepareRecruitTestCommand(newRecruitJobCreateCommand())
cmd.Flags().Bool("yes", false, "确认创建")
cmd.SetIn(strings.NewReader(""))
cmd.SetArgs([]string{"--from", path})
@@ -162,7 +207,7 @@ func TestRecruitJobCreateRequiresConfirmationBeforeRemoteCall(t *testing.T) {
func TestRecruitJobCreateCallsRemoteOnceWhenConfirmed(t *testing.T) {
caller := withRecruitCaller(t)
path, _ := writeRecruitJobFixture(t)
cmd := newRecruitJobCreateCommand()
cmd := prepareRecruitTestCommand(newRecruitJobCreateCommand())
cmd.Flags().Bool("yes", false, "确认创建")
cmd.SetArgs([]string{"--from", path, "--yes"})
if err := cmd.Execute(); err != nil {
@@ -190,7 +235,7 @@ func TestRecruitCreateValidatesJobFile(t *testing.T) {
if err := os.WriteFile(badPath, []byte(`{"name":"缺字段"}`), 0o600); err != nil {
t.Fatal(err)
}
bad := newRecruitJobCreateCommand()
bad := prepareRecruitTestCommand(newRecruitJobCreateCommand())
bad.Flags().Bool("yes", false, "确认创建")
bad.SetArgs([]string{"--from", badPath, "--yes"})
if err := bad.Execute(); err == nil || !strings.Contains(err.Error(), "description") {
@@ -204,7 +249,7 @@ func TestRecruitValidationBranches(t *testing.T) {
t.Fatalf("statuses = %#v, err = %v", statuses, err)
}
negativeCursor := newRecruitJobListCommand()
negativeCursor := prepareRecruitTestCommand(newRecruitJobListCommand())
negativeCursor.SetArgs([]string{"--cursor", "-1"})
if err := negativeCursor.Execute(); err == nil || !strings.Contains(err.Error(), "大于或等于 0") {
t.Fatalf("negative cursor error = %v", err)
@@ -217,6 +262,16 @@ func TestRecruitValidationBranches(t *testing.T) {
if _, err := loadRecruitJobFile(nullPath); err == nil || !strings.Contains(err.Error(), "顶层必须是对象") {
t.Fatalf("null job error = %v", err)
}
if _, err := loadRecruitJobFile(filepath.Join(t.TempDir(), "missing.json")); err == nil || !strings.Contains(err.Error(), "读取职位 JSON 失败") {
t.Fatalf("missing job file error = %v", err)
}
invalidPath := filepath.Join(t.TempDir(), "invalid.json")
if err := os.WriteFile(invalidPath, []byte("{"), 0o600); err != nil {
t.Fatal(err)
}
if _, err := loadRecruitJobFile(invalidPath); err == nil || !strings.Contains(err.Error(), "不是有效的 JSON 对象") {
t.Fatalf("invalid job JSON error = %v", err)
}
base := map[string]any{
"name": "Java 工程师", "description": "服务端开发", "jobNature": "FULL_TIME",
@@ -244,6 +299,80 @@ func TestRecruitValidationBranches(t *testing.T) {
}
}
func TestRecruitListResultData(t *testing.T) {
for _, test := range []struct {
name string
data any
want string
}{
{name: "not object", data: []any{}, want: "必须是 JSON 对象"},
{name: "missing hasMore", data: map[string]any{}, want: "缺少布尔字段 hasMore"},
{name: "invalid cursor", data: map[string]any{"hasMore": true, "nextCursor": true}, want: "必须是字符串或数字"},
{name: "missing cursor", data: map[string]any{"hasMore": true}, want: "缺少 nextCursor"},
} {
t.Run(test.name, func(t *testing.T) {
if _, _, err := recruitListResultData(test.data); err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("error = %v, want %q", err, test.want)
}
})
}
data, meta, err := recruitListResultData(map[string]any{
"jobs": []any{map[string]any{"jobId": "job-1"}}, "hasMore": true, "nextCursor": float64(12),
})
if err != nil {
t.Fatal(err)
}
if meta.Count == nil || *meta.Count != 1 || meta.Pagination.EndpointExhausted ||
meta.Pagination.NextToken != "12" || meta.Pagination.Pages != 1 || meta.Pagination.Items != 1 {
t.Fatalf("meta = %#v", meta)
}
clean := data.(map[string]any)
if _, exists := clean["hasMore"]; exists || clean["jobs"] == nil {
t.Fatalf("clean data = %#v", clean)
}
_, terminal, err := recruitListResultData(map[string]any{"jobs": []any{}, "hasMore": false, "nextCursor": "ignored"})
if err != nil || !terminal.Pagination.EndpointExhausted || terminal.Pagination.NextToken != "" {
t.Fatalf("terminal meta = %#v, err = %v", terminal, err)
}
}
func TestRecruitResultCallPropagatesMCPError(t *testing.T) {
caller := withRecruitCaller(t)
caller.err = stderrors.New("transport failed")
cmd := prepareRecruitTestCommand(newRecruitJobGetCommand())
if _, err := recruitResultCall(cmd, recruitGetJobTool, map[string]any{"jobId": "job-1"}); err == nil {
t.Fatal("expected MCP error")
}
}
func TestRecruitResultCallRejectsInconsistentPagination(t *testing.T) {
caller := withRecruitCaller(t)
caller.text = `{"jobs":[],"hasMore":true}`
cmd := prepareRecruitTestCommand(newRecruitJobListCommand())
result, err := recruitResultCall(cmd, recruitListJobsTool, map[string]any{"size": 20})
if err != nil {
t.Fatal(err)
}
if result.Outcome() != output.OutcomeFailure {
t.Fatalf("outcome = %s, want failure", result.Outcome())
}
}
func TestRecruitResultCallDryRun(t *testing.T) {
caller := withRecruitCaller(t)
caller.dryRun = true
cmd := prepareRecruitTestCommand(newRecruitJobGetCommand())
result, err := recruitResultCall(cmd, recruitGetJobTool, map[string]any{"jobId": "job-1"})
if err != nil {
t.Fatal(err)
}
if result.Outcome() != output.OutcomeSuccess || len(caller.calls) != 0 {
t.Fatalf("outcome = %s, calls = %d", result.Outcome(), len(caller.calls))
}
}
func TestRecruitCommandTree(t *testing.T) {
root := newRecruitCommand()
for _, path := range [][]string{{"job", "list"}, {"job", "get"}, {"job", "create"}} {