mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-27 03:46:39 +08:00
fix(migration): treat an unselected runner id as not_legacy
A saved pipeline may carry the post-migration shape
{'ai': {'runner': {'id': ''}, 'runner_config': {}}} while no runner has been
selected yet. The planner matched the empty id against the certified
'plugin:author/name/component' form, failed, and returned a malformed-id
blocker, so the whole 'migrate all' batch stopped with both pipelines reported
as blocked even though there was no legacy section and nothing to convert.
A blank id with no legacy runner section now reports not_legacy: no work is
needed and no target is synthesized. A blank id that still coexists with a
legacy section remains an explicit invalid_runner_id blocker, because that
ambiguous state must be resolved by the operator rather than guessed.
This commit is contained in:
@@ -634,6 +634,12 @@ def plan_legacy_pipeline(config, extensions_preferences=None) -> dict:
|
|||||||
return _block(result, 'mixed_runner_selection', 'ai.runner')
|
return _block(result, 'mixed_runner_selection', 'ai.runner')
|
||||||
if 'id' in selection:
|
if 'id' in selection:
|
||||||
current = selection['id']
|
current = selection['id']
|
||||||
|
# A blank runner id paired with no legacy runner section means the saved
|
||||||
|
# pipeline simply has no runner selected. There is nothing to migrate and
|
||||||
|
# nothing to convert, so report not_legacy instead of failing the whole
|
||||||
|
# batch with a malformed-id blocker.
|
||||||
|
if type(current) is str and not current.strip() and not any(legacy in ai for legacy in _TARGETS):
|
||||||
|
return result
|
||||||
if type(current) is not str or not re.fullmatch(r'plugin:[^/\s]+/[^/\s]+/[^/\s]+', current):
|
if type(current) is not str or not re.fullmatch(r'plugin:[^/\s]+/[^/\s]+/[^/\s]+', current):
|
||||||
return _block(result, 'invalid_runner_id', 'ai.runner.id')
|
return _block(result, 'invalid_runner_id', 'ai.runner.id')
|
||||||
result['state'] = 'already_current'
|
result['state'] = 'already_current'
|
||||||
|
|||||||
@@ -641,12 +641,40 @@ def test_dify_saved_timeout_is_not_blindly_activated(timeout):
|
|||||||
assert {'code': 'dify.timeout_default', 'field': 'ai.dify-service-api.timeout'} in result['warnings']
|
assert {'code': 'dify.timeout_default', 'field': 'ai.dify-service-api.timeout'} in result['warnings']
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('current', [None, '', False, [], 'plugin:bad', ' plugin:a/b/c', 'plugin:a/b/c/extra'])
|
@pytest.mark.parametrize('current', [None, False, [], 'plugin:bad', ' plugin:a/b/c', 'plugin:a/b/c/extra'])
|
||||||
def test_malformed_current_id_is_not_already_current(current):
|
def test_malformed_current_id_is_not_already_current(current):
|
||||||
source = {'ai': {'runner': {'id': current}}}
|
source = {'ai': {'runner': {'id': current}}}
|
||||||
assert_block(plan(source), 'invalid_runner_id', 'ai.runner.id')
|
assert_block(plan(source), 'invalid_runner_id', 'ai.runner.id')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('blank', ['', ' '])
|
||||||
|
def test_blank_current_id_without_legacy_section_is_not_legacy(blank):
|
||||||
|
"""A saved pipeline that simply has no runner selected is not legacy.
|
||||||
|
|
||||||
|
It has no legacy runner section to convert and no target to synthesize, so it
|
||||||
|
must report not_legacy instead of blocking the whole batch with a
|
||||||
|
malformed-id diagnostic.
|
||||||
|
"""
|
||||||
|
source = {'ai': {'runner': {'id': blank, 'expire-time': 0}, 'runner_config': {}}}
|
||||||
|
result = plan(source)
|
||||||
|
assert result['state'] == 'not_legacy'
|
||||||
|
assert result['config'] is None
|
||||||
|
assert result['blockers'] == []
|
||||||
|
assert result['changed_paths'] == []
|
||||||
|
assert result['target_plugin'] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_blank_current_id_with_legacy_section_stays_blocked():
|
||||||
|
"""A blank id cannot silently coexist with a legacy section.
|
||||||
|
|
||||||
|
An unselected plugin runner plus a legacy section is an ambiguous state that
|
||||||
|
the operator must resolve, so it keeps the malformed-id blocker.
|
||||||
|
"""
|
||||||
|
source = source_for()
|
||||||
|
source['ai']['runner'] = {'id': '', 'expire-time': 0}
|
||||||
|
assert_block(plan(source), 'invalid_runner_id', 'ai.runner.id')
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('runner', TARGETS)
|
@pytest.mark.parametrize('runner', TARGETS)
|
||||||
def test_deterministic_results_and_input_nonmutation_for_all_nine(runner):
|
def test_deterministic_results_and_input_nonmutation_for_all_nine(runner):
|
||||||
source = source_for(runner)
|
source = source_for(runner)
|
||||||
|
|||||||
Reference in New Issue
Block a user