初始提交

This commit is contained in:
焦龙言
2026-07-24 09:43:10 +08:00
commit b5b4721456
793 changed files with 116305 additions and 0 deletions
@@ -0,0 +1,68 @@
import importlib
import sys
from collections.abc import Callable
from pathlib import Path
from types import ModuleType
import pytest
BACKEND_DIR = Path(__file__).resolve().parents[3]
if str(BACKEND_DIR) not in sys.path:
sys.path.insert(0, str(BACKEND_DIR))
def _load_module(module_name: str) -> ModuleType:
sys.modules.pop(module_name, None)
sys.modules.pop('cli', None)
return importlib.import_module(module_name)
@pytest.fixture
def backend_dir() -> Path:
return BACKEND_DIR
@pytest.fixture
def load_module() -> Callable[[str], ModuleType]:
return _load_module
@pytest.fixture
def cli_main(load_module: Callable[[str], ModuleType]) -> ModuleType:
return load_module('cli.main')
@pytest.fixture
def app_run_flow(load_module: Callable[[str], ModuleType]) -> ModuleType:
return load_module('cli.wizard.flows.app_run')
@pytest.fixture
def cache_clear_flow(load_module: Callable[[str], ModuleType]) -> ModuleType:
return load_module('cli.wizard.flows.cache_clear')
@pytest.fixture
def db_upgrade_flow(load_module: Callable[[str], ModuleType]) -> ModuleType:
return load_module('cli.wizard.flows.db_upgrade')
@pytest.fixture
def gen_export_flow(load_module: Callable[[str], ModuleType]) -> ModuleType:
return load_module('cli.wizard.flows.gen_export')
@pytest.fixture
def gen_import_flow(load_module: Callable[[str], ModuleType]) -> ModuleType:
return load_module('cli.wizard.flows.gen_import')
@pytest.fixture
def prod_check_flow(load_module: Callable[[str], ModuleType]) -> ModuleType:
return load_module('cli.wizard.flows.prod_check')
@pytest.fixture
def exit_codes(load_module: Callable[[str], ModuleType]) -> ModuleType:
return load_module('cli.exit_codes')
@@ -0,0 +1,33 @@
from types import ModuleType, SimpleNamespace
from pytest import MonkeyPatch
def test_app_run_wizard_execs_nested_cli_after_confirmation(
monkeypatch: MonkeyPatch,
app_run_flow: ModuleType,
) -> None:
captured: dict[str, object] = {}
answers = iter(['dev', False, True])
monkeypatch.setattr(
app_run_flow.AppRunWizardFlow,
'prompt_service',
SimpleNamespace(
prompt_env=lambda default_env='dev': next(answers),
prompt_confirm=lambda prompt_text, default_value=False: next(answers),
),
)
monkeypatch.setattr(
app_run_flow.AppRunWizardFlow.context_factory,
'build_readonly',
lambda env, output: SimpleNamespace(env=env, output=output),
)
monkeypatch.setattr(
app_run_flow.AppRunWizardFlow,
'nested_live_command_runner',
lambda *arguments: captured.update({'arguments': arguments}),
)
app_run_flow.run_app_run_wizard()
assert captured['arguments'] == ('app', 'run', '--env=dev')
@@ -0,0 +1,88 @@
from types import ModuleType, SimpleNamespace
from pytest import MonkeyPatch
def test_cache_clear_wizard_cancels_when_confirmation_rejected(
monkeypatch: MonkeyPatch,
cache_clear_flow: ModuleType,
exit_codes: ModuleType,
) -> None:
captured: dict[str, object] = {}
answers = iter(['dev', 'cache-name', 'sys_config', True, False])
monkeypatch.setattr(
cache_clear_flow.CacheClearWizardFlow,
'prompt_service',
SimpleNamespace(
prompt_env=lambda default_env='dev': next(answers),
prompt_choice=lambda prompt_text, choices, default_value: next(answers),
prompt_required_text=lambda prompt_text, default_value='': next(answers),
prompt_optional_text=lambda prompt_text, default_value='': '',
prompt_confirm=lambda prompt_text, default_value=False: next(answers),
),
)
monkeypatch.setattr(
cache_clear_flow.CacheClearWizardFlow.context_factory,
'build_regular',
lambda env, output, allow_prod, yes, dry_run: SimpleNamespace(env=env, output=output),
)
monkeypatch.setattr(
cache_clear_flow.CacheClearWizardFlow.execution_service,
'complete_result',
lambda ctx, result: captured.update({'ctx': ctx, 'result': result}),
)
cache_clear_flow.run_cache_clear_wizard('text')
assert captured['ctx'].env == 'dev'
assert captured['result'].exit_code == exit_codes.GUARD_REJECTED
assert captured['result'].data['message'] == '已取消向导执行:wizard cache-clear'
def test_cache_clear_wizard_accepts_default_context_values(
monkeypatch: MonkeyPatch,
cache_clear_flow: ModuleType,
) -> None:
captured_defaults: dict[str, object] = {}
captured_confirm_defaults: list[tuple[str, bool]] = []
answers = iter(['dev', 'cache-name', 'sys_config', True, False])
monkeypatch.setattr(
cache_clear_flow.CacheClearWizardFlow,
'prompt_service',
SimpleNamespace(
prompt_env=lambda default_env='dev': captured_defaults.update({'env': default_env}) or next(answers),
prompt_choice=lambda prompt_text, choices, default_value: (
captured_defaults.update({'mode': default_value}) or next(answers)
),
prompt_required_text=lambda prompt_text, default_value='': (
captured_defaults.update({'cache_name': default_value}) or next(answers)
),
prompt_optional_text=lambda prompt_text, default_value='': '',
prompt_confirm=lambda prompt_text, default_value=False: (
captured_confirm_defaults.append((prompt_text, default_value)) or next(answers)
),
),
)
monkeypatch.setattr(
cache_clear_flow.CacheClearWizardFlow.context_factory,
'build_regular',
lambda env, output, allow_prod, yes, dry_run: SimpleNamespace(env=env, output=output),
)
monkeypatch.setattr(
cache_clear_flow.CacheClearWizardFlow.execution_service,
'complete_result',
lambda ctx, result: None,
)
cache_clear_flow.run_cache_clear_wizard(
'text',
default_env='dev',
default_mode='cache-name',
default_cache_name='sys_config',
default_dry_run=True,
)
assert captured_defaults['env'] == 'dev'
assert captured_defaults['mode'] == 'cache-name'
assert captured_defaults['cache_name'] == 'sys_config'
assert captured_confirm_defaults[0][1] is True
@@ -0,0 +1,24 @@
from pathlib import Path
from types import ModuleType
from pytest import MonkeyPatch
from typer.testing import CliRunner
def test_wizard_help_lists_supported_subcommands(
monkeypatch: MonkeyPatch,
backend_dir: Path,
cli_main: ModuleType,
) -> None:
runner = CliRunner()
monkeypatch.chdir(backend_dir)
result = runner.invoke(cli_main.CLI_APPLICATION_BUILDER.build(), ['wizard', '--help'])
assert result.exit_code == 0
assert 'app-run' in result.stdout
assert 'db-upgrade' in result.stdout
assert 'cache-clear' in result.stdout
assert 'gen-export' in result.stdout
assert 'gen-import' in result.stdout
assert 'prod-check' in result.stdout
@@ -0,0 +1,99 @@
from types import ModuleType, SimpleNamespace
from pytest import MonkeyPatch
def test_db_upgrade_wizard_executes_nested_cli_with_confirmed_inputs(
monkeypatch: MonkeyPatch,
db_upgrade_flow: ModuleType,
) -> None:
captured: dict[str, object] = {}
monkeypatch.setattr(
db_upgrade_flow.DbUpgradeWizardFlow,
'prompt_service',
SimpleNamespace(
prompt_env=lambda default_env='dev': 'dev',
prompt_required_text=lambda prompt_text, default_value='': 'head',
prompt_confirm=lambda prompt_text, default_value=False: True,
),
)
monkeypatch.setattr(
db_upgrade_flow.DbUpgradeWizardFlow.context_factory,
'build_regular',
lambda env, output, allow_prod, yes, dry_run: SimpleNamespace(
env=env,
output=output,
allow_prod=allow_prod,
yes=yes,
dry_run=dry_run,
),
)
monkeypatch.setattr(
db_upgrade_flow.DbUpgradeWizardFlow,
'nested_command_runner',
lambda *arguments, parse_json=False: SimpleNamespace(
returncode=0,
payload={'ok': True, 'message': '数据库已升级到 head', 'dryRun': True},
),
)
monkeypatch.setattr(
db_upgrade_flow.DbUpgradeWizardFlow.execution_service,
'complete_payload',
lambda ctx, payload, default_exit_code=0: captured.update(
{
'ctx': ctx,
'payload': payload,
'default_exit_code': default_exit_code,
}
),
)
db_upgrade_flow.run_db_upgrade_wizard('json')
assert captured['ctx'].env == 'dev'
assert captured['ctx'].output == 'json'
assert captured['ctx'].dry_run is True
assert captured['payload'] == {'ok': True, 'message': '数据库已升级到 head', 'dryRun': True}
def test_db_upgrade_wizard_accepts_default_context_values(
monkeypatch: MonkeyPatch,
db_upgrade_flow: ModuleType,
) -> None:
captured_defaults: dict[str, object] = {}
captured_confirm_defaults: list[tuple[str, bool]] = []
confirm_answers = iter([True, False])
monkeypatch.setattr(
db_upgrade_flow.DbUpgradeWizardFlow,
'prompt_service',
SimpleNamespace(
prompt_env=lambda default_env='dev': captured_defaults.update({'env': default_env}) or 'dev',
prompt_required_text=lambda prompt_text, default_value='': (
captured_defaults.update({'revision': default_value}) or 'head'
),
prompt_confirm=lambda prompt_text, default_value=False: (
captured_confirm_defaults.append((prompt_text, default_value)) or next(confirm_answers)
),
),
)
monkeypatch.setattr(
db_upgrade_flow.DbUpgradeWizardFlow.context_factory,
'build_regular',
lambda env, output, allow_prod, yes, dry_run: SimpleNamespace(env=env, output=output),
)
monkeypatch.setattr(
db_upgrade_flow.DbUpgradeWizardFlow.execution_service,
'complete_result',
lambda ctx, result: None,
)
db_upgrade_flow.run_db_upgrade_wizard(
'text',
default_env='dev',
default_revision='head',
default_dry_run=True,
)
assert captured_defaults['env'] == 'dev'
assert captured_defaults['revision'] == 'head'
assert captured_confirm_defaults[0][1] is True
@@ -0,0 +1,120 @@
from types import ModuleType, SimpleNamespace
from pytest import MonkeyPatch
def test_gen_export_wizard_executes_nested_cli_with_selected_tables(
monkeypatch: MonkeyPatch,
gen_export_flow: ModuleType,
) -> None:
captured: dict[str, object] = {}
answers = iter(['dev', 'sys_user,sys_role', 'zip', 'build/demo.zip', True, True])
monkeypatch.setattr(
gen_export_flow.GenExportWizardFlow,
'prompt_service',
SimpleNamespace(
prompt_env=lambda default_env='dev': next(answers),
prompt_required_text=lambda prompt_text, default_value='': next(answers),
prompt_choice=lambda prompt_text, choices, default_value: next(answers),
prompt_optional_text=lambda prompt_text, default_value='': next(answers),
prompt_confirm=lambda prompt_text, default_value=False: next(answers),
),
)
monkeypatch.setattr(
gen_export_flow.GenExportWizardFlow.context_factory,
'build_regular',
lambda env, output, allow_prod, yes, dry_run: SimpleNamespace(
env=env,
output=output,
allow_prod=allow_prod,
yes=yes,
dry_run=dry_run,
),
)
monkeypatch.setattr(
gen_export_flow.GenExportWizardFlow,
'nested_command_runner',
lambda *arguments, parse_json=False: SimpleNamespace(
returncode=0,
payload={
'ok': True,
'message': '代码导出演练完成,未执行实际导出',
'dryRun': True,
'mode': 'zip',
'tableNames': ['sys_user', 'sys_role'],
'outputFile': '/tmp/demo.zip',
},
),
)
monkeypatch.setattr(
gen_export_flow.GenExportWizardFlow.execution_service,
'complete_payload',
lambda ctx, payload, default_exit_code=0: captured.update(
{
'ctx': ctx,
'payload': payload,
'default_exit_code': default_exit_code,
}
),
)
gen_export_flow.run_gen_export_wizard('json')
assert captured['ctx'].env == 'dev'
assert captured['ctx'].output == 'json'
assert captured['ctx'].dry_run is True
assert captured['payload']['tableNames'] == ['sys_user', 'sys_role']
assert captured['payload']['mode'] == 'zip'
def test_gen_export_wizard_accepts_default_context_values(
monkeypatch: MonkeyPatch,
gen_export_flow: ModuleType,
) -> None:
captured_defaults: dict[str, object] = {}
captured_confirm_defaults: list[tuple[str, bool]] = []
answers = iter(['dev', 'sys_user', 'zip', 'build/demo.zip', True, False])
monkeypatch.setattr(
gen_export_flow.GenExportWizardFlow,
'prompt_service',
SimpleNamespace(
prompt_env=lambda default_env='dev': captured_defaults.update({'env': default_env}) or next(answers),
prompt_required_text=lambda prompt_text, default_value='': (
captured_defaults.update({'table_names': default_value}) or next(answers)
),
prompt_choice=lambda prompt_text, choices, default_value: (
captured_defaults.update({'mode': default_value}) or next(answers)
),
prompt_optional_text=lambda prompt_text, default_value='': (
captured_defaults.update({'output_file': default_value}) or next(answers)
),
prompt_confirm=lambda prompt_text, default_value=False: (
captured_confirm_defaults.append((prompt_text, default_value)) or next(answers)
),
),
)
monkeypatch.setattr(
gen_export_flow.GenExportWizardFlow.context_factory,
'build_regular',
lambda env, output, allow_prod, yes, dry_run: SimpleNamespace(env=env, output=output),
)
monkeypatch.setattr(
gen_export_flow.GenExportWizardFlow.execution_service,
'complete_result',
lambda ctx, result: None,
)
gen_export_flow.run_gen_export_wizard(
'text',
default_env='dev',
default_table_names='sys_user',
default_mode='zip',
default_output_file='build/demo.zip',
default_dry_run=True,
)
assert captured_defaults['env'] == 'dev'
assert captured_defaults['table_names'] == 'sys_user'
assert captured_defaults['mode'] == 'zip'
assert captured_defaults['output_file'] == 'build/demo.zip'
assert captured_confirm_defaults[0][1] is True
@@ -0,0 +1,164 @@
from types import ModuleType, SimpleNamespace
from pytest import MonkeyPatch
def test_gen_import_wizard_executes_nested_cli_with_selected_tables(
monkeypatch: MonkeyPatch,
gen_import_flow: ModuleType,
) -> None:
captured: dict[str, object] = {}
answers = iter(['dev', 'sys_user,sys_role', True, True])
monkeypatch.setattr(
gen_import_flow.GenImportWizardFlow,
'prompt_service',
SimpleNamespace(
prompt_env=lambda default_env='dev': next(answers),
prompt_required_text=lambda prompt_text, default_value='': next(answers),
prompt_confirm=lambda prompt_text, default_value=False: next(answers),
),
)
monkeypatch.setattr(
gen_import_flow.GenImportWizardFlow.context_factory,
'build_regular',
lambda env, output, allow_prod, yes, dry_run: SimpleNamespace(
env=env,
output=output,
allow_prod=allow_prod,
yes=yes,
dry_run=dry_run,
),
)
monkeypatch.setattr(
gen_import_flow.GenImportWizardFlow,
'nested_command_runner',
lambda *arguments, parse_json=False: SimpleNamespace(
returncode=0,
payload={
'ok': True,
'message': '物理表导入预演完成,未执行实际导入',
'dryRun': True,
'tableNames': ['sys_user', 'sys_role'],
},
),
)
monkeypatch.setattr(
gen_import_flow.GenImportWizardFlow.execution_service,
'complete_payload',
lambda ctx, payload, default_exit_code=0: captured.update(
{
'ctx': ctx,
'payload': payload,
'default_exit_code': default_exit_code,
}
),
)
gen_import_flow.run_gen_import_wizard('json')
assert captured['ctx'].env == 'dev'
assert captured['ctx'].output == 'json'
assert captured['ctx'].dry_run is True
assert captured['payload']['tableNames'] == ['sys_user', 'sys_role']
def test_gen_import_wizard_accepts_default_context_values(
monkeypatch: MonkeyPatch,
gen_import_flow: ModuleType,
) -> None:
captured_defaults: dict[str, object] = {}
captured_confirm_defaults: list[tuple[str, bool]] = []
answers = iter(['dev', 'sys_user', True, False])
monkeypatch.setattr(
gen_import_flow.GenImportWizardFlow,
'prompt_service',
SimpleNamespace(
prompt_env=lambda default_env='dev': captured_defaults.update({'env': default_env}) or next(answers),
prompt_required_text=lambda prompt_text, default_value='': (
captured_defaults.update({'table_names': default_value}) or next(answers)
),
prompt_confirm=lambda prompt_text, default_value=False: (
captured_confirm_defaults.append((prompt_text, default_value)) or next(answers)
),
),
)
monkeypatch.setattr(
gen_import_flow.GenImportWizardFlow.context_factory,
'build_regular',
lambda env, output, allow_prod, yes, dry_run: SimpleNamespace(env=env, output=output),
)
monkeypatch.setattr(
gen_import_flow.GenImportWizardFlow.execution_service,
'complete_result',
lambda ctx, result: None,
)
gen_import_flow.run_gen_import_wizard(
'text',
default_env='dev',
default_table_names='sys_user',
default_dry_run=True,
)
assert captured_defaults['env'] == 'dev'
assert captured_defaults['table_names'] == 'sys_user'
assert captured_confirm_defaults[0][1] is True
def test_gen_import_wizard_falls_back_to_failure_payload_when_nested_result_has_no_dict_payload(
monkeypatch: MonkeyPatch,
gen_import_flow: ModuleType,
) -> None:
captured: dict[str, object] = {}
answers = iter(['dev', 'sys_notice', True, True])
monkeypatch.setattr(
gen_import_flow.GenImportWizardFlow,
'prompt_service',
SimpleNamespace(
prompt_env=lambda default_env='dev': next(answers),
prompt_required_text=lambda prompt_text, default_value='': next(answers),
prompt_confirm=lambda prompt_text, default_value=False: next(answers),
),
)
monkeypatch.setattr(
gen_import_flow.GenImportWizardFlow.context_factory,
'build_regular',
lambda env, output, allow_prod, yes, dry_run: SimpleNamespace(
env=env,
output=output,
allow_prod=allow_prod,
yes=yes,
dry_run=dry_run,
),
)
monkeypatch.setattr(
gen_import_flow.GenImportWizardFlow,
'nested_command_runner',
lambda *arguments, parse_json=False: SimpleNamespace(
returncode=0,
stdout='OK SUCCESS\nmessage: 导入表结构演练完成,未执行实际写入',
stderr='',
payload=[],
),
)
monkeypatch.setattr(
gen_import_flow.GenImportWizardFlow.execution_service,
'complete_payload',
lambda ctx, payload, default_exit_code=0: captured.update(
{
'ctx': ctx,
'payload': payload,
'default_exit_code': default_exit_code,
}
),
)
gen_import_flow.run_gen_import_wizard('text')
assert captured['ctx'].env == 'dev'
assert captured['payload'] == {
'ok': False,
'message': '代码生成导入向导执行失败',
'error': 'OK SUCCESS\nmessage: 导入表结构演练完成,未执行实际写入',
'exit_code': 0,
}
@@ -0,0 +1,110 @@
from types import ModuleType, SimpleNamespace
from pytest import MonkeyPatch
def test_prod_check_wizard_aggregates_nested_cli_payloads(
monkeypatch: MonkeyPatch,
prod_check_flow: ModuleType,
) -> None:
captured: dict[str, object] = {}
answers = iter(['prod', True, True])
monkeypatch.setattr(
prod_check_flow.ProdCheckWizardFlow,
'prompt_service',
SimpleNamespace(
prompt_env=lambda default_env='prod': next(answers),
prompt_confirm=lambda prompt_text, default_value=False: next(answers),
),
)
monkeypatch.setattr(
prod_check_flow.ProdCheckWizardFlow.context_factory,
'build_readonly',
lambda env, output: SimpleNamespace(env=env, output=output),
)
def fake_run_nested_cli_command(*arguments: str, parse_json: bool = False) -> SimpleNamespace:
del parse_json
if arguments[0:2] == ('app', 'env'):
return SimpleNamespace(
payload={
'ok': True,
'runtime': {
'cliEnv': 'prod',
'configEnv': 'prod',
'envFile': '.env.prod',
'envFileExists': True,
},
}
)
if arguments[0:2] == ('app', 'doctor'):
return SimpleNamespace(payload={'ok': True, 'message': '应用启动前检查通过'})
return SimpleNamespace(
payload={
'ok': True,
'config': {
'name': 'demo',
'host': '0.0.0.0',
'port': 9099,
'dbType': 'mysql',
'redisHost': '127.0.0.1',
'redisPort': 6379,
},
}
)
monkeypatch.setattr(
prod_check_flow.ProdCheckWizardFlow,
'nested_command_runner',
fake_run_nested_cli_command,
)
monkeypatch.setattr(
prod_check_flow.ProdCheckWizardFlow.execution_service,
'complete_payload',
lambda ctx, payload, default_exit_code=0: captured.update({'ctx': ctx, 'payload': payload}),
)
prod_check_flow.run_prod_check_wizard('json')
assert captured['ctx'].env == 'prod'
assert captured['payload']['ok'] is True
assert captured['payload']['doctor']['message'] == '应用启动前检查通过'
assert captured['payload']['runtime']['cliEnv'] == 'prod'
assert captured['payload']['config']['name'] == 'demo'
def test_prod_check_wizard_accepts_default_context_values(
monkeypatch: MonkeyPatch,
prod_check_flow: ModuleType,
) -> None:
captured_defaults: dict[str, object] = {}
confirm_answers = iter([True, False])
monkeypatch.setattr(
prod_check_flow.ProdCheckWizardFlow,
'prompt_service',
SimpleNamespace(
prompt_env=lambda default_env='prod': captured_defaults.update({'env': default_env}) or 'prod',
prompt_confirm=lambda prompt_text, default_value=False: (
captured_defaults.update({'include_config': default_value}) or next(confirm_answers)
),
),
)
monkeypatch.setattr(
prod_check_flow.ProdCheckWizardFlow.context_factory,
'build_readonly',
lambda env, output: SimpleNamespace(env=env, output=output),
)
monkeypatch.setattr(
prod_check_flow.ProdCheckWizardFlow.execution_service,
'complete_result',
lambda ctx, result: None,
)
prod_check_flow.run_prod_check_wizard(
'text',
default_env='prod',
default_include_config=True,
)
assert captured_defaults['env'] == 'prod'
assert captured_defaults['include_config'] is True