118 lines
3.9 KiB
Python
118 lines
3.9 KiB
Python
"""Regression: schema-declared defaults must be materialised into
|
|
``inputs.collected.<step>`` on resume.
|
|
|
|
Without ``_merge_clarify_defaults`` the runtime would only write the
|
|
fields the user actually submitted, so any downstream Jinja template
|
|
referencing an optional field (e.g.
|
|
``{{ inputs.collected.paper_collect.language }}``) would explode with
|
|
``UndefinedError`` whenever the user skipped it — even though the
|
|
SKILL.md schema declared ``default: en``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from opensquilla.skills.meta.orchestrator import _merge_clarify_defaults
|
|
|
|
|
|
def _schema_dict() -> dict:
|
|
return {
|
|
"mode": "form",
|
|
"fields": [
|
|
{"name": "topic", "type": "string", "required": True},
|
|
{"name": "paper_mode", "type": "enum", "required": True,
|
|
"choices": ["A", "B"]},
|
|
{"name": "language", "type": "enum",
|
|
"choices": ["en", "zh"], "default": "en"},
|
|
{"name": "target_length_pages", "type": "int",
|
|
"min": 1, "max": 50, "default": 10},
|
|
{"name": "audience", "type": "enum",
|
|
"choices": ["academic"], "default": "academic"},
|
|
],
|
|
}
|
|
|
|
|
|
def test_defaults_back_fill_when_optional_fields_omitted():
|
|
"""User submitted only the 2 required fields; the 3 optional ones
|
|
should get their schema-declared defaults."""
|
|
merged = _merge_clarify_defaults(
|
|
_schema_dict(),
|
|
{"topic": "RAG", "paper_mode": "A"},
|
|
)
|
|
assert merged == {
|
|
"topic": "RAG",
|
|
"paper_mode": "A",
|
|
"language": "en",
|
|
"target_length_pages": 10,
|
|
"audience": "academic",
|
|
}
|
|
|
|
|
|
def test_user_values_override_defaults():
|
|
"""When the user IS supplying an optional field, the default must
|
|
not stomp on it."""
|
|
merged = _merge_clarify_defaults(
|
|
_schema_dict(),
|
|
{
|
|
"topic": "RAG",
|
|
"paper_mode": "A",
|
|
"language": "zh",
|
|
"target_length_pages": 25,
|
|
},
|
|
)
|
|
assert merged["language"] == "zh"
|
|
assert merged["target_length_pages"] == 25
|
|
assert merged["audience"] == "academic" # unsupplied → default
|
|
|
|
|
|
def test_unknown_keys_are_dropped():
|
|
"""Prompt-injection / nl_extract leaks must not enter collected."""
|
|
merged = _merge_clarify_defaults(
|
|
_schema_dict(),
|
|
{"topic": "RAG", "paper_mode": "A", "evil": "rm -rf"},
|
|
)
|
|
assert "evil" not in merged
|
|
assert merged["topic"] == "RAG"
|
|
|
|
|
|
def test_empty_user_input_still_gets_defaults():
|
|
"""Edge case: user pressed cancel-form-and-submit-empty (shouldn't
|
|
happen via the form, but nl_extract could land here)."""
|
|
merged = _merge_clarify_defaults(_schema_dict(), {})
|
|
# Required fields remain missing (downstream will raise — fine);
|
|
# optional fields with defaults are populated.
|
|
assert "topic" not in merged
|
|
assert "paper_mode" not in merged
|
|
assert merged["language"] == "en"
|
|
assert merged["target_length_pages"] == 10
|
|
assert merged["audience"] == "academic"
|
|
|
|
|
|
def test_field_without_default_stays_absent():
|
|
"""A schema field that is optional AND has no default must NOT
|
|
appear in merged output — that's the author's explicit choice."""
|
|
schema = {
|
|
"fields": [
|
|
{"name": "notes", "type": "string"}, # optional, no default
|
|
],
|
|
}
|
|
merged = _merge_clarify_defaults(schema, {})
|
|
assert merged == {}
|
|
|
|
|
|
def test_default_none_is_treated_as_unset():
|
|
"""``default: None`` is the same as no default declaration."""
|
|
schema = {
|
|
"fields": [
|
|
{"name": "notes", "type": "string", "default": None},
|
|
],
|
|
}
|
|
merged = _merge_clarify_defaults(schema, {})
|
|
assert merged == {}
|
|
|
|
|
|
def test_no_fields_no_crash():
|
|
"""A clarify config with empty fields list is degenerate but parses;
|
|
helper must not crash."""
|
|
assert _merge_clarify_defaults({"fields": []}, {"x": 1}) == {}
|
|
assert _merge_clarify_defaults({}, {"x": 1}) == {}
|