Add unit tests for task lifecycle commands#2464
Conversation
There was a problem hiding this comment.
Hey - I've left some high level feedback:
- Several tests explicitly codify latent bugs as current behavior (e.g.
parsed_args.tasklist passed tocontrol.revoke, empty tasks module for unknown worker type, builtinformattohandle_task); consider marking these with a clear TODO/xfail or a dedicated test naming convention so they’re easy to find and update when the underlying bugs are fixed. - The
test_lock_user_help_bakes_in_operator_user_at_parser_build_timetest relies onparser._actions, which is an internal argparse detail; it would be more robust to locate the--useraction via a public API or helper rather than depending on private attributes. - There is repeated setup logic across command tests for env-based concurrency defaults and
check_task_lock_and_exitordering; consider extracting shared helpers/fixtures (e.g. for building parsers and asserting lock-check-before-schedule semantics) to reduce duplication and keep behavior changes easier to maintain.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Several tests explicitly codify latent bugs as current behavior (e.g. `parsed_args.task` list passed to `control.revoke`, empty tasks module for unknown worker type, builtin `format` to `handle_task`); consider marking these with a clear TODO/xfail or a dedicated test naming convention so they’re easy to find and update when the underlying bugs are fixed.
- The `test_lock_user_help_bakes_in_operator_user_at_parser_build_time` test relies on `parser._actions`, which is an internal argparse detail; it would be more robust to locate the `--user` action via a public API or helper rather than depending on private attributes.
- There is repeated setup logic across command tests for env-based concurrency defaults and `check_task_lock_and_exit` ordering; consider extracting shared helpers/fixtures (e.g. for building parsers and asserting lock-check-before-schedule semantics) to reduce duplication and keep behavior changes easier to maintain.Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Cover the task lifecycle CLI command modules under osism/commands/ with unit tests following the established pattern (command instantiated with mocked app/options, args built via get_parser().parse_args()): - task: parser contract and Celery revoke with terminate=True; pins the latent bug that the one-element nargs=1 list reaches control.revoke - worker: concurrency defaults (CPU count capped at 4, OSISM_CELERY_CONCURRENCY read at parser-build time), worker type to tasks module/queue mapping including the osism-ansible special case, and the unvalidated unknown-type command as current behavior - reconciler (extended): deprecation warning and concurrency of the worker runner; sync wait/no-wait paths, task timeout forwarding, and the OSISM_TASK_TIMEOUT parser default - lock: lock/unlock/status flows including already-locked warnings, OPERATOR_USER default, unknown-key fallbacks, and non-zero returns on lock backend failures - service: subprocess command lines for api, listener, beat (one scheduler per task module), flower, and reconciler; watchdog observer setup/cleanup and inventory event handling; unknown type as silent no-op - container: one-shot and interactive SSH docker execution, known-hosts fallback warning, and prompt exit keywords - configuration: task scheduling with auto_release_time and rc propagation via handle_task; pins the latent builtin-format argument - set/noset: state playbook scheduling parametrized over the maintenance/bootstrap command pairs All Celery task calls, subprocesses, and the Redis task lock are mocked; the tests also assert that check_task_lock_and_exit runs before any task is scheduled or process is started. Closes #2362 Assisted-by: Claude:claude-fable-5 Signed-off-by: Christian Berendt <berendt@osism.tech>
cc543f2 to
6f0b977
Compare
|
|
||
|
|
||
| @pytest.mark.latent_bug | ||
| def test_unknown_service_type_is_a_silent_noop(): |
There was a problem hiding this comment.
This pins an unknown service type passing without error. The test is green only because service.py silently does nothing for an unrecognized type, and latent_bug does not exclude it from the default suite — so whoever adds the error path turns this test red and must rewrite it. Assert the intended behavior (unknown type should error) and fix service.py in a dedicated commit.
|
|
||
|
|
||
| @pytest.mark.latent_bug | ||
| def test_take_action_unknown_type_yields_empty_tasks_module(): |
There was a problem hiding this comment.
This pins a broken command as intended output. An unvalidated short type makes queue[:-8] empty, producing celery -A osism.tasks. worker …. Reject unknown worker types (or map them explicitly) in a dedicated commit, and assert the error rather than the malformed command.
|
|
||
|
|
||
| @pytest.mark.latent_bug | ||
| def test_parser_rejects_leading_option_arguments(): |
There was a problem hiding this comment.
This pins a parser that rejects the Ansible options its arguments remainder exists to forward. nargs=REMAINDER only starts capturing at the first positional, so osism configuration sync -e foo=1 is rejected — and since every forwardable option starts with - and there is no natural leading positional here, no option can be passed in practice. Switch to the parse_known_args approach already used in openstack, in a dedicated commit, and assert the option is forwarded.
| assert result == rc | ||
|
|
||
|
|
||
| @pytest.mark.latent_bug |
There was a problem hiding this comment.
This pins a real bug — the wrong value is passed. Only this caller passes the builtin format instead of a format string (manage.py, netbox.py → "script"; apply.py, validate.py → parsed_args.format); it is inert today only because wait=True short-circuits before it is read, but --no-wait (as reconciler.Sync already has) would make the else branch print nothing. Fix production to pass "log" (or wire --format) in a dedicated commit and assert that; do not pin the builtin as intended.
|
|
||
|
|
||
| @pytest.mark.latent_bug | ||
| def test_revoke_passes_task_list_instead_of_id(): |
There was a problem hiding this comment.
This is the only nargs=1 positional passed on without [0] unpacking — service.py:25, worker.py:34, and set/noset/container all unpack. control.revoke accepts both a string and a list, so the current call is not broken; this is a sibling-consistency gap, not a bug. If uniformity is the goal, unpack here too — app.control.revoke(task[0], terminate=True) — in a dedicated commit, and drop the latent_bug marker; keep the behavioral assertion.
| from ._helpers import assert_not_called_before_lock_check, parse_args | ||
|
|
||
|
|
||
| def _run_worker(monkeypatch, *, env=None, cpu_count=16): |
There was a problem hiding this comment.
The commit message's lock-ordering claim is false because the ordering is missing on this path. reconciler.Run.take_action (osism/commands/reconciler.py:14-27) starts a worker subprocess with no check_task_lock_and_exit() — only Sync calls it (:49) — so the commit body's "before any task is scheduled or process is started" does not hold, and this _run_worker helper neither patches nor asserts the lock. It is a deprecated path (its replacement service.Run guards the lock), so severity is low. Pick one: (a) add the missing check in a dedicated commit — then the commit message is accurate as written and this test should assert the lock; or (b) leave the deprecated path and correct the commit message to name reconciler.Run as the exception rather than claiming universal coverage.
| mock_prompt.assert_not_called() | ||
| mock_call.assert_called_once() | ||
| ssh_command = mock_call.call_args[0][0] | ||
| assert mock_call.call_args[1] == {"shell": True} |
There was a problem hiding this comment.
These assertions would block a safe rewrite. call_args[1] == {"shell": True} plus the exact command string would fail a future hardening to an argv vector (subprocess.call([...]), no shell=True) for no behavioral reason. Assert only the observable contract — ssh targets testuser@node1 and runs docker ps -a. Separately, the shell=True interpolation in osism/commands/container.py:47,55 (already # FIXME'd) is a pre-existing, codebase-wide hardening candidate for its own PR; it is operator-supplied input, so not a security blocker here.
Implements #2362 (part of the Tier 8 meta issue #2199).
Adds unit tests for the nine small task-lifecycle CLI command modules under
osism/commands/, following the established pattern fromtests/unit/commands/test_reconciler.py/test_wait.py(Cmd(MagicMock(), MagicMock())+get_parser("test").parse_args([...])).New test files
test_task.py— parser contract and Celery revoke withterminate=True; pins the latent bug that the one-elementnargs=1list reachescontrol.revoketest_worker.py— concurrency defaults (CPU count capped at 4,OSISM_CELERY_CONCURRENCYread at parser-build time), worker-type → tasks-module/queue mapping including theosism-ansiblespecial case and suffix stripping,-cpropagation, and the unvalidated unknown-type command pinned as current behaviortest_lock.py—Lock/Unlock/LockStatusflows: already-locked warnings (with/without reason),--user/--reasonpass-through,OPERATOR_USERdefault (also baked into the--userhelp at parser-build time), unknown-key fallbacks, and non-zero returns when the lock backend failstest_service.py— exact subprocess command lines forapi,listener,beat(seven schedulers, one per task module),flower, andreconciler(env/CPU concurrency logic); watchdog observer setup on/opt/configuration/inventory, cleanup viafinally,on_any_eventrebinding, and the inventory event triggeringreconciler.run.delay(); unknown service type as silent no-optest_container.py— parserhost/REMAINDER split, one-shot SSHdockerexecution (key, known-hosts option, operator user), known-hosts init failure warning, interactive prompt loop, andExit/exit/EXITloop terminationtest_configuration.py— REMAINDER forwarding,delay(..., auto_release_time=60), rc propagation viahandle_task; pins the latent builtin-formatargumenttest_set.py/test_noset.py— state playbook scheduling parametrized over theMaintenance/Bootstrapcommand pairs (status=Truevs.status=False)Extended
test_reconciler.py— deprecation warning and concurrency of the deprecated worker runner;Syncwait/no-wait paths,--task-timeoutforwarding, and theOSISM_TASK_TIMEOUTparser default (the existing timeout test is kept unchanged)All Celery task calls, subprocesses, and the Redis task lock are mocked at the command module (lazy imports patched at the source module); the tests also assert that
check_task_lock_and_exitruns before any task is scheduled or process is started.Closes #2362
🤖 Generated with Claude Code