- default engine: fielded BM25 ensemble (action_v1 remains a shadow candidate) - add --explain score_breakdown: per-field contributions, structured multiplier, query classification and avoid_when penalty - add DWS_TOOL_SEARCH_* env overrides (algorithm, k1/b, field weights, explain) for release-independent tuning and rollback - tokenizer: CJK unigram+bigram so single-character intents match; align isToolSearchCJK with unicode.Han used by the evaluation slices - structured routing: extend vocabularies with English synonyms, word-boundary matching for ASCII values, restrict the rerank gate to technical identifiers only - avoid_when demotion layer wraps every lexical algorithm (Forbidden@5 proxy 37.55% -> 0); intent-overlap matching rejected after control showed it demoted gold answers (Recall@5 0.8664 -> 0.7845) - remove the OA task_id +1.00 fixture-specific multiplier - build the default engine once per process for long-lived hosts - evaluation baseline: intent-proxy R@5 990/1123 (88.16%); update integration sentinels accordingly
96 lines
3.7 KiB
Go
96 lines
3.7 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func TestSchemaSearchCommandExactAndVersioned(t *testing.T) {
|
|
installSchemaSearchTestEngine(t)
|
|
root := &cobra.Command{Use: "dws"}
|
|
root.AddCommand(NewSchemaCommand())
|
|
var stdout bytes.Buffer
|
|
root.SetOut(&stdout)
|
|
root.SetArgs([]string{"schema", "search", "--query", "chat.read_status"})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("schema search execute: %v", err)
|
|
}
|
|
var response ToolSearchResponse
|
|
if err := json.Unmarshal(stdout.Bytes(), &response); err != nil {
|
|
t.Fatalf("decode response: %v\n%s", err, stdout.String())
|
|
}
|
|
if response.Version != "tool-search.v1" || response.Strategy != "exact_guard" {
|
|
t.Fatalf("response = %#v", response)
|
|
}
|
|
if len(response.Candidates) != 1 || response.Candidates[0].CanonicalPath != "chat.read_status" {
|
|
t.Fatalf("candidates = %#v", response.Candidates)
|
|
}
|
|
wantCatalog := CatalogVersionRef{SourceHash: "source-test", SurfaceHash: "surface-test"}
|
|
if response.Catalog != wantCatalog {
|
|
t.Fatalf("Catalog = %#v, want %#v", response.Catalog, wantCatalog)
|
|
}
|
|
for _, removed := range []string{"external_ranking", "degraded", "degraded_reason_code", "warning_codes", "_fallback"} {
|
|
if strings.Contains(stdout.String(), removed) {
|
|
t.Fatalf("removed fallback field %q leaked into response: %s", removed, stdout.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSchemaSearchRequestRejectsUnboundedOrAmbiguousInput(t *testing.T) {
|
|
if _, err := decodeToolSearchV1Request(strings.NewReader(`{"version":"tool-search.v1","query":"x","unknown":true}`)); validationReason(err) != "invalid_request_json" {
|
|
t.Fatal("unknown field was accepted")
|
|
}
|
|
if _, err := decodeToolSearchV1Request(strings.NewReader(`{"version":"tool-search.v1","query":"x","external_ranking":{}}`)); validationReason(err) != "invalid_request_json" {
|
|
t.Fatal("removed external_ranking field was accepted")
|
|
}
|
|
oversized := strings.NewReader(strings.Repeat("x", maxToolSearchRequestBytes+1))
|
|
if _, err := decodeToolSearchV1Request(oversized); validationReason(err) != "request_too_large" {
|
|
t.Fatal("oversized request was accepted")
|
|
}
|
|
_, err := validateToolSearchV1Request(ToolSearchV1Request{Query: "x", Subqueries: []string{"x"}})
|
|
if validationReason(err) != "unsupported_version" || !strings.Contains(err.Error(), `"version":"tool-search.v1"`) || !strings.Contains(err.Error(), "subqueries") {
|
|
t.Fatalf("missing-version guidance = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSchemaSearchHelpDocumentsStructuredRequest(t *testing.T) {
|
|
command := newSchemaSearchCommand()
|
|
var output bytes.Buffer
|
|
command.SetOut(&output)
|
|
command.SetArgs([]string{"--help"})
|
|
if err := command.Execute(); err != nil {
|
|
t.Fatalf("schema search help: %v", err)
|
|
}
|
|
for _, required := range []string{`"version":"tool-search.v1"`, `"subqueries"`, "--request-json -", "不支持 --fields、--jq", "非 JSON --format"} {
|
|
if !strings.Contains(output.String(), required) {
|
|
t.Fatalf("schema search help omits %q:\n%s", required, output.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func installSchemaSearchTestEngine(t *testing.T) {
|
|
t.Helper()
|
|
testseam.Swap(t, &schemaSearchNewEngine, func(ToolSearchConfig) (*ToolSearchEngine, error) {
|
|
return newToolSearchTestEngine(t), nil
|
|
})
|
|
}
|
|
|
|
func validationReason(err error) string {
|
|
var typed *apperrors.Error
|
|
if errors.As(err, &typed) && typed.Category == apperrors.CategoryValidation {
|
|
return typed.Reason
|
|
}
|
|
return ""
|
|
}
|