fix: 补充招聘结果与分页契约 to#85340676
This commit is contained in:
@@ -274,6 +274,7 @@ type agentExampleFiles struct {
|
||||
markdown string
|
||||
json string
|
||||
batch string
|
||||
job string
|
||||
binary string
|
||||
image string
|
||||
}
|
||||
@@ -283,12 +284,14 @@ func newAgentExampleFiles(t testing.TB, root string) agentExampleFiles {
|
||||
markdown := filepath.Join(root, "content.md")
|
||||
jsonFile := filepath.Join(root, "report.json")
|
||||
batch := filepath.Join(root, "styles.json")
|
||||
job := filepath.Join(root, "job.json")
|
||||
binary := filepath.Join(root, "report.pdf")
|
||||
image := filepath.Join(root, "chart.png")
|
||||
for path, content := range map[string][]byte{
|
||||
markdown: []byte("# Agent dry-run fixture\n\nNo business call is allowed.\n"),
|
||||
jsonFile: []byte(`[{"content":"Agent dry-run fixture","sort":"0","key":"fixture","contentType":"markdown","type":"1"}]`),
|
||||
batch: []byte(`[{"sheetId":"Sheet1","range":"A1:B2","fontWeight":"bold"}]`),
|
||||
job: []byte(`{"name":"Java 工程师","description":"服务端开发","jobNature":"FULL_TIME","requiredEdu":1,"minSalary":20000,"maxSalary":35000}`),
|
||||
binary: []byte("%PDF-1.4\n%%EOF\n"),
|
||||
image: {0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'},
|
||||
} {
|
||||
@@ -301,6 +304,7 @@ func newAgentExampleFiles(t testing.TB, root string) agentExampleFiles {
|
||||
markdown: "./" + filepath.Base(markdown),
|
||||
json: "./" + filepath.Base(jsonFile),
|
||||
batch: "./" + filepath.Base(batch),
|
||||
job: "./" + filepath.Base(job),
|
||||
binary: "./" + filepath.Base(binary),
|
||||
image: "./" + filepath.Base(image),
|
||||
}
|
||||
@@ -351,6 +355,10 @@ func materializeAgentExampleArgv(argv []string, files agentExampleFiles) []strin
|
||||
replacement = files.markdown
|
||||
case "contents-file":
|
||||
replacement = files.json
|
||||
case "from":
|
||||
if strings.HasSuffix(strings.ToLower(value), "job.json") {
|
||||
replacement = files.job
|
||||
}
|
||||
case "batch":
|
||||
if strings.HasSuffix(strings.ToLower(value), "styles.json") {
|
||||
replacement = files.batch
|
||||
|
||||
@@ -23,10 +23,29 @@ const (
|
||||
)
|
||||
|
||||
var recruitDryRun = &contract.DryRunSpec{
|
||||
PreviewKind: contract.DryRunPreviewInvocation,
|
||||
PreviewKind: contract.DryRunPreviewRequest,
|
||||
RemoteReads: false,
|
||||
}
|
||||
|
||||
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}`),
|
||||
}
|
||||
recruitJobDetailResult = &contract.ResultSpec{
|
||||
Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomeFailure},
|
||||
DataSchema: json.RawMessage(`{"type":"object","description":"招聘职位详情","properties":{"jobId":{"type":"string","description":"职位 ID"},"name":{"type":"string","description":"职位名称"},"description":{"type":"string","description":"职位描述"},"status":{"type":"number","description":"职位状态枚举值"}},"additionalProperties":true}`),
|
||||
}
|
||||
recruitCreateJobResult = &contract.ResultSpec{
|
||||
Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomeFailure},
|
||||
DataSchema: json.RawMessage(`{"type":"object","description":"招聘职位创建结果","properties":{"jobId":{"type":"string","description":"新创建的职位 ID"}},"required":["jobId"],"additionalProperties":true}`),
|
||||
}
|
||||
)
|
||||
|
||||
func recruitCursorPagination() *contract.PaginationSpec {
|
||||
return &contract.PaginationSpec{Kind: contract.PaginationKindCursor, CursorParameter: "cursor"}
|
||||
}
|
||||
|
||||
func newRecruitCommand() *cobra.Command {
|
||||
contract.RegisterProductDecl(contract.ProductDecl{
|
||||
ID: "recruit",
|
||||
@@ -86,6 +105,8 @@ func newRecruitJobListCommand() *cobra.Command {
|
||||
Identity: contract.ToolIdentitySpec{ProductID: "recruit", Name: recruitListJobsTool, CanonicalPath: "recruit.list_jobs", CLIPath: "recruit job list", PrimaryCLIPath: "recruit job list"},
|
||||
Description: "按条件分页查询招聘职位",
|
||||
DryRun: recruitDryRun,
|
||||
Result: recruitListResult,
|
||||
Pagination: recruitCursorPagination(),
|
||||
Interface: recruitMCPInterface(recruitListJobsTool),
|
||||
Selection: contract.SelectionSpec{
|
||||
AgentSummary: "按关键词、状态、创建人等条件分页查询招聘职位",
|
||||
@@ -130,6 +151,7 @@ func newRecruitJobGetCommand() *cobra.Command {
|
||||
Identity: contract.ToolIdentitySpec{ProductID: "recruit", Name: recruitGetJobTool, CanonicalPath: "recruit.get_job_detail", CLIPath: "recruit job get", PrimaryCLIPath: "recruit job get"},
|
||||
Description: "根据职位 ID 查询招聘职位详情",
|
||||
DryRun: recruitDryRun,
|
||||
Result: recruitJobDetailResult,
|
||||
Interface: recruitMCPInterface(recruitGetJobTool),
|
||||
Selection: contract.SelectionSpec{
|
||||
AgentSummary: "获取指定招聘职位的完整信息",
|
||||
@@ -159,6 +181,7 @@ func newRecruitJobCreateCommand() *cobra.Command {
|
||||
Identity: contract.ToolIdentitySpec{ProductID: "recruit", Name: recruitCreateJobTool, CanonicalPath: "recruit.create_job", CLIPath: "recruit job create", PrimaryCLIPath: "recruit job create"},
|
||||
Description: "从 JSON 文件创建招聘职位",
|
||||
DryRun: recruitDryRun,
|
||||
Result: recruitCreateJobResult,
|
||||
Interface: recruitMCPInterface(recruitCreateJobTool),
|
||||
Selection: contract.SelectionSpec{
|
||||
AgentSummary: "使用结构化 JSON 创建招聘职位",
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"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/pkg/edition"
|
||||
)
|
||||
@@ -196,6 +198,52 @@ func TestRecruitCreateValidatesJobFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecruitValidationBranches(t *testing.T) {
|
||||
statuses, err := transformRecruitStatuses(" open, ,open,closed ")
|
||||
if err != nil || !reflect.DeepEqual(statuses, []int{1, 3}) {
|
||||
t.Fatalf("statuses = %#v, err = %v", statuses, err)
|
||||
}
|
||||
|
||||
negativeCursor := newRecruitJobListCommand()
|
||||
negativeCursor.SetArgs([]string{"--cursor", "-1"})
|
||||
if err := negativeCursor.Execute(); err == nil || !strings.Contains(err.Error(), "大于或等于 0") {
|
||||
t.Fatalf("negative cursor error = %v", err)
|
||||
}
|
||||
|
||||
nullPath := filepath.Join(t.TempDir(), "null.json")
|
||||
if err := os.WriteFile(nullPath, []byte("null"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := loadRecruitJobFile(nullPath); err == nil || !strings.Contains(err.Error(), "顶层必须是对象") {
|
||||
t.Fatalf("null job error = %v", err)
|
||||
}
|
||||
|
||||
base := map[string]any{
|
||||
"name": "Java 工程师", "description": "服务端开发", "jobNature": "FULL_TIME",
|
||||
"requiredEdu": float64(1), "minSalary": float64(20000), "maxSalary": float64(35000),
|
||||
}
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
mutate func(map[string]any)
|
||||
want string
|
||||
}{
|
||||
{name: "string type", mutate: func(job map[string]any) { job["name"] = 1 }, want: "name 必须是字符串"},
|
||||
{name: "number type", mutate: func(job map[string]any) { job["requiredEdu"] = "本科" }, want: "requiredEdu 必须是数字"},
|
||||
{name: "salary range", mutate: func(job map[string]any) { job["minSalary"] = float64(40000) }, want: "minSalary 不能大于 maxSalary"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
job := make(map[string]any, len(base))
|
||||
for key, value := range base {
|
||||
job[key] = value
|
||||
}
|
||||
test.mutate(job)
|
||||
if err := validateRecruitJob(job); err == nil || !strings.Contains(err.Error(), test.want) {
|
||||
t.Fatalf("validation error = %v, want %q", err, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecruitCommandTree(t *testing.T) {
|
||||
root := newRecruitCommand()
|
||||
for _, path := range [][]string{{"job", "list"}, {"job", "get"}, {"job", "create"}} {
|
||||
@@ -205,3 +253,54 @@ func TestRecruitCommandTree(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecruitResultAndPaginationContractsReachContractFinal(t *testing.T) {
|
||||
root := newRecruitCommand()
|
||||
tests := []struct {
|
||||
path []string
|
||||
canonical string
|
||||
resultField string
|
||||
pagination bool
|
||||
}{
|
||||
{path: []string{"job", "list"}, canonical: "recruit.list_jobs", resultField: "jobs", pagination: true},
|
||||
{path: []string{"job", "get"}, canonical: "recruit.get_job_detail", resultField: "jobId"},
|
||||
{path: []string{"job", "create"}, canonical: "recruit.create_job", resultField: "jobId"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.canonical, func(t *testing.T) {
|
||||
leaf, _, err := root.Find(test.path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
final, ok := contractfinal.RuntimeContractFinal(leaf)
|
||||
if !ok || final.Identity == nil || final.Identity.CanonicalPath != test.canonical {
|
||||
t.Fatalf("final identity = %#v, found = %v", final.Identity, ok)
|
||||
}
|
||||
if final.Result == nil {
|
||||
t.Fatal("final Result is nil")
|
||||
}
|
||||
wantOutcomes := []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomeFailure}
|
||||
if !reflect.DeepEqual(final.Result.Outcomes, wantOutcomes) {
|
||||
t.Fatalf("outcomes = %#v, want %#v", final.Result.Outcomes, wantOutcomes)
|
||||
}
|
||||
var schema struct {
|
||||
Type string `json:"type"`
|
||||
Properties map[string]json.RawMessage `json:"properties"`
|
||||
}
|
||||
if err := json.Unmarshal(final.Result.DataSchema, &schema); err != nil {
|
||||
t.Fatalf("unmarshal result data_schema: %v", err)
|
||||
}
|
||||
if schema.Type != "object" || len(schema.Properties[test.resultField]) == 0 {
|
||||
t.Fatalf("result data_schema = %s, want object property %q", final.Result.DataSchema, test.resultField)
|
||||
}
|
||||
if test.pagination {
|
||||
if final.Pagination == nil || final.Pagination.Kind != contract.PaginationKindCursor ||
|
||||
final.Pagination.CursorParameter != "cursor" || final.Pagination.MetaPath != contract.PaginationMetaPath {
|
||||
t.Fatalf("pagination = %#v, want cursor contract", final.Pagination)
|
||||
}
|
||||
} else if final.Pagination != nil {
|
||||
t.Fatalf("pagination = %#v, want nil", final.Pagination)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[root]
|
||||
runnable: true
|
||||
hidden: false
|
||||
commands: agoal, aisearch, aitable, api, attendance, audit, auth, cache, calendar, chat, completion, config, contact, dev, devapp, devdoc, ding, doc, doctor, drive, event, help, hrbrain, live, mail, markdown, mcp, minutes, oa, pat, plugin, profile, recovery, report, schema, sheet, skill, todo, upgrade, version, whiteboard, wiki
|
||||
commands: agoal, aisearch, aitable, api, attendance, audit, auth, cache, calendar, chat, completion, config, contact, dev, devapp, devdoc, ding, doc, doctor, drive, event, help, hrbrain, live, mail, markdown, mcp, minutes, oa, pat, plugin, profile, recovery, recruit, report, schema, sheet, skill, todo, upgrade, version, whiteboard, wiki
|
||||
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=persistent, --client-secret:string|required=false|hidden=false|no-opt=""|scope=persistent, --debug:bool|required=false|hidden=false|no-opt="true"|scope=persistent, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=persistent, --fields:string|required=false|hidden=false|no-opt=""|scope=persistent, -f/--format:string|required=false|hidden=false|no-opt=""|scope=persistent, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=persistent, --mock:bool|required=false|hidden=false|no-opt="true"|scope=persistent, -o/--output:string|required=false|hidden=true|no-opt=""|scope=persistent, --profile:string|required=false|hidden=false|no-opt=""|scope=persistent, --timeout:int|required=false|hidden=false|no-opt=""|scope=persistent, --token:string|required=false|hidden=true|no-opt=""|scope=persistent, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=persistent, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=persistent
|
||||
|
||||
[agoal]
|
||||
|
||||
Reference in New Issue
Block a user