Skip to content

Add unit tests for task lifecycle commands#2464

Open
berendt wants to merge 1 commit into
mainfrom
unit-tests-task-lifecycle-commands
Open

Add unit tests for task lifecycle commands#2464
berendt wants to merge 1 commit into
mainfrom
unit-tests-task-lifecycle-commands

Conversation

@berendt

@berendt berendt commented Jul 13, 2026

Copy link
Copy Markdown
Member

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 from tests/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 with terminate=True; pins the latent bug that the one-element nargs=1 list reaches control.revoke
  • test_worker.py — concurrency defaults (CPU count capped at 4, OSISM_CELERY_CONCURRENCY read at parser-build time), worker-type → tasks-module/queue mapping including the osism-ansible special case and suffix stripping, -c propagation, and the unvalidated unknown-type command pinned as current behavior
  • test_lock.pyLock/Unlock/LockStatus flows: already-locked warnings (with/without reason), --user/--reason pass-through, OPERATOR_USER default (also baked into the --user help at parser-build time), unknown-key fallbacks, and non-zero returns when the lock backend fails
  • test_service.py — exact subprocess command lines for api, listener, beat (seven schedulers, one per task module), flower, and reconciler (env/CPU concurrency logic); watchdog observer setup on /opt/configuration/inventory, cleanup via finally, on_any_event rebinding, and the inventory event triggering reconciler.run.delay(); unknown service type as silent no-op
  • test_container.py — parser host/REMAINDER split, one-shot SSH docker execution (key, known-hosts option, operator user), known-hosts init failure warning, interactive prompt loop, and Exit/exit/EXIT loop termination
  • test_configuration.py — REMAINDER forwarding, delay(..., auto_release_time=60), rc propagation via handle_task; pins the latent builtin-format argument
  • test_set.py / test_noset.py — state playbook scheduling parametrized over the Maintenance/Bootstrap command pairs (status=True vs. status=False)

Extended

  • test_reconciler.py — deprecation warning and concurrency of the deprecated worker runner; Sync wait/no-wait paths, --task-timeout forwarding, and the OSISM_TASK_TIMEOUT parser 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_exit runs before any task is scheduled or process is started.

Closes #2362

🤖 Generated with Claude Code

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • 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.
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.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
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>
@berendt berendt force-pushed the unit-tests-task-lifecycle-commands branch from cc543f2 to 6f0b977 Compare July 15, 2026 07:27
@berendt berendt moved this from New to Ready for review in Human Board Jul 15, 2026
@berendt berendt requested a review from ideaship July 15, 2026 08:07


@pytest.mark.latent_bug
def test_unknown_service_type_is_a_silent_noop():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.pyparsed_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():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only nargs=1 positional passed on without [0] unpackingservice.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):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-project-automation github-project-automation Bot moved this from Ready for review to In review in Human Board Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

Unit tests for osism/commands/ — task lifecycle (task, worker, reconciler, lock, service, container, configuration, set, noset)

3 participants