Files
mudler--localai/core/config/meta/registry_coverage_test.go
wehub-resource-sync 1b8708893a
Security Scan / tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:12:26 +08:00

259 lines
7.5 KiB
Go

package meta_test
import (
"reflect"
"sort"
"strings"
"testing"
. "github.com/onsi/gomega"
"github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/config/meta"
)
// TestAllFieldsHaveRegistryEntries fails when a NEW ModelConfig field
// is added without either a registry entry in DefaultRegistry() or an
// entry in the grandfatheredUnregistered baseline below.
//
// Why this matters: fields without a registry entry render in the UI
// with no description, the default `input` (single-line) component,
// and land in the catch-all "other" section — which is what we just
// hit for router.classifier_system_template. The reflection-based
// fallback produces something that works mechanically but is hostile
// to operators.
//
// How to fix when this test fails:
//
// 1. Preferred — add a registry entry to DefaultRegistry() in
// registry.go with Section, Label, Description, and Component.
// See e.g. "router.classifier" or "template.chat" for the pattern.
//
// 2. Escape hatch — append the field path to
// grandfatheredUnregistered with a one-line comment justifying
// why it has no UI surface (internal, deprecated, legacy
// compatibility shim, etc.). The expectation is that this list
// shrinks over time as fields get proper registry entries; it
// should never grow without good reason.
//
// The grandfathered list was seeded from a one-time audit. Migrating
// the existing 150+ entries to proper registry metadata is out of
// scope for any single PR; the lock just stops the list from growing.
func TestAllFieldsHaveRegistryEntries(t *testing.T) {
g := NewWithT(t)
md := meta.BuildConfigMetadata(reflect.TypeOf(config.ModelConfig{}))
reg := meta.DefaultRegistry()
grand := make(map[string]struct{}, len(grandfatheredUnregistered))
for _, p := range grandfatheredUnregistered {
grand[p] = struct{}{}
}
var missing []string
for _, f := range md.Fields {
if _, ok := reg[f.Path]; ok {
continue
}
if _, ok := grand[f.Path]; ok {
continue
}
missing = append(missing, f.Path)
}
sort.Strings(missing)
g.Expect(missing).To(BeEmpty(),
"%d config field(s) have no registry entry and are not on the grandfathered list.\n"+
"Add a registry entry to core/config/meta/registry.go OR append to grandfatheredUnregistered in this file with a justification:\n %s",
len(missing), strings.Join(missing, "\n "),
)
// Inverse drift check: catch dead entries on the grandfathered
// list (field was renamed/removed, or someone wrote a registry
// entry without trimming the grandfathered duplicate).
known := make(map[string]struct{}, len(md.Fields))
for _, f := range md.Fields {
known[f.Path] = struct{}{}
}
var stale, duplicated []string
for _, p := range grandfatheredUnregistered {
if _, ok := known[p]; !ok {
stale = append(stale, p)
}
if _, ok := reg[p]; ok {
duplicated = append(duplicated, p)
}
}
sort.Strings(stale)
g.Expect(stale).To(BeEmpty(),
"grandfatheredUnregistered references fields that no longer exist in ModelConfig — remove them:\n %s",
strings.Join(stale, "\n "))
sort.Strings(duplicated)
g.Expect(duplicated).To(BeEmpty(),
"grandfatheredUnregistered references fields that now HAVE a registry entry — remove them so the test stays meaningful:\n %s",
strings.Join(duplicated, "\n "))
}
// grandfatheredUnregistered is the baseline of config fields that
// pre-date the registry-coverage test and have no UI metadata yet.
// Adding new entries here should be a deliberate, justified decision
// — prefer adding a registry entry in registry.go instead.
//
// Keep the list sorted (one-line-per-entry) so the diff is minimal
// when an entry is removed or (rarely) added.
var grandfatheredUnregistered = []string{
"agent.disable_sink_state",
"agent.enable_mcp_prompts",
"agent.enable_plan_re_evaluator",
"agent.enable_planning",
"agent.enable_reasoning",
"agent.force_reasoning_tool",
"agent.loop_detection",
"agent.max_adjustment_attempts",
"agent.max_attempts",
"agent.max_iterations",
"cfg_scale",
"chat_template_kwargs",
"concurrency_groups",
"cutstrings",
"debug",
"diffusers.clip_model",
"diffusers.clip_skip",
"diffusers.clip_subfolder",
"diffusers.control_net",
"diffusers.enable_parameters",
"diffusers.img2img",
"disable_log_stats",
"disabled",
"download_files",
"draft_model",
"dtype",
"enforce_eager",
"engine_args",
"extract_regex",
"feature_flags",
"function.argument_regex",
"function.argument_regex_key_name",
"function.argument_regex_value_name",
"function.automatic_tool_parsing_fallback",
"function.capture_llm_results",
"function.disable_no_action",
"function.disable_peg_parser",
"function.function_arguments_key",
"function.function_name_key",
"function.grammar.disable_parallel_new_lines",
"function.grammar.expect_strings_after_json",
"function.grammar.no_mixed_free_string",
"function.grammar.prefix",
"function.grammar.properties_order",
"function.grammar.schema_type",
"function.grammar.triggers",
"function.json_regex_match",
"function.no_action_description_name",
"function.no_action_function_name",
"function.replace_function_results",
"function.replace_llm_results",
"function.response_regex",
"function.xml_format.allow_toolcall_in_think",
"function.xml_format.key_start",
"function.xml_format.key_val_sep",
"function.xml_format.key_val_sep2",
"function.xml_format.last_tool_end",
"function.xml_format.last_val_end",
"function.xml_format.raw_argval",
"function.xml_format.scope_end",
"function.xml_format.scope_start",
"function.xml_format.tool_end",
"function.xml_format.tool_sep",
"function.xml_format.tool_start",
"function.xml_format.trim_raw_argval",
"function.xml_format.val_end",
"function.xml_format_preset",
"gpu_memory_utilization",
"grammar",
"grpc.attempts",
"grpc.attempts_sleep_time",
"limit_mm_per_prompt.audio",
"limit_mm_per_prompt.image",
"limit_mm_per_prompt.video",
"limits.max_concurrent",
"limits.retry_after_seconds",
"load_format",
"lora_adapter",
"lora_adapters",
"lora_base",
"lora_scale",
"lora_scales",
"main_gpu",
"max_model_len",
"mcp.remote",
"mcp.stdio",
"mirostat",
"mirostat_eta",
"mirostat_tau",
"mmproj",
"n_draft",
"ngqa",
"no_kv_offloading",
"no_mulmatq",
"numa",
"options",
"overrides",
"parameters.batch",
"parameters.clip_skip",
"parameters.echo",
"parameters.encoding_format",
"parameters.frequency_penalty",
"parameters.ignore_eos",
"parameters.language",
"parameters.logit_bias",
"parameters.logprobs",
"parameters.min_p",
"parameters.model",
"parameters.n",
"parameters.n_keep",
"parameters.negative_prompt",
"parameters.negative_prompt_scale",
"parameters.presence_penalty",
"parameters.repeat_last_n",
"parameters.rope_freq_base",
"parameters.rope_freq_scale",
"parameters.tfz",
"parameters.tokenizer",
"parameters.top_logprobs",
"parameters.translate",
"parameters.typical_p",
"pinned",
"prompt_cache_all",
"prompt_cache_path",
"prompt_cache_ro",
"proxy.api_key_file",
"reasoning.disable",
"reasoning.disable_reasoning_tag_prefill",
"reasoning.strip_reasoning_only",
"reasoning.tag_pairs",
"reasoning.thinking_start_tokens",
"reranking",
"rms_norm_eps",
"roles",
"rope_scaling",
"step",
"stopwords",
"swap_space",
"system_prompt",
"template.edit",
"template.join_chat_messages_by_character",
"template.multimodal",
"template.reply_prefix",
"tensor_parallel_size",
"tensor_split",
"trimspace",
"trimsuffix",
"trust_remote_code",
"tts.audio_path",
"type",
"yarn_attn_factor",
"yarn_beta_fast",
"yarn_beta_slow",
"yarn_ext_factor",
}