Skip to content

fix(ci): host state fails four tests and one case #3585

Open

Description

@dan-petty

Problem

The full gate uv run devops ci fails in the devcontainer on a clean worktree of origin/main. Four tests fail, and one contract case fails and errors. The same result comes from the branch fix/3497-cut-unchanged-github-reads, so the failure does not come from that branch.

Each failure depends on the state of the host:

  • tests/crypto/test_ssh.py::test_get_ssh_key_prefix_resolution: the test expects the repository name my_plain_repo. It gets the name of the checkout directory, devops_cli.
  • tests/telemetry/test_config_env_overrides.py::test_find_project_config_path_skips_directory: a file exists at /tmp/config.yaml on this host, so the path is not None.
  • tests/commands/test_devcontainer_install_tools_dry_run.py::test_mark_only_commands_under_global_dry_run: the devcontainer tool is installed on this host, so its status is not not installed.
  • tests/ai/test_tool_executor_contract.py::...[direct]: a tool under the user home directory is reached without a fake. The same case then errors at teardown, because a network edge was reached without a fake.

Expected

Each test passes on a host that has none of these files or tools. A test that depends on host state must use a fake or a path under tmp_path, as docs/TESTING.md requires. A test must not skip on host state.

Evidence

uv run devops ci test on the four test files gives 4 failed, 177 passed, 1 error on the branch and on origin/main.

Proposed design

Problem statement

The full gate uv run devops ci fails in the devcontainer on a clean worktree of origin/main. Four tests and one contract case read the state of the host: the checkout directory name, a file at /tmp/config.yaml, an installed devcontainer tool and an installed popeye. Each test must pass on every host through a fake or a path under tmp_path, and no test may skip on host state. The change fixes the four tests and the project config search that reaches the host's file.

Acceptance criteria

Requirement: SSH prefix fallback runs in its own directory

The SSH key-prefix test SHALL run the fallback case with the process working directory inside the directory under test.

Scenario: fallback prefix of a plain directory

  • WHEN The test changes into /my_plain_repo and calls get_ssh_key_prefix on that directory.
  • THEN The call returns my_plain_repo on a host whose checkout directory is devops_cli.

Requirement: Project config search stops at a project root

The project config search SHALL accept a config file in an ancestor directory only when that ancestor holds a .git or a .devcontainer entry.

Scenario: config file in an ancestor with no project marker

  • WHEN A config.yaml sits in the parent of the base directory, and that parent holds no .git and no .devcontainer.
  • THEN _find_project_config_path returns None.

Requirement: Install-tools status names no tool of the host

The mark-only dry-run test SHALL run install-tools status with a PATH that names no installed tool.

Scenario: status with an empty PATH

  • WHEN The test sets PATH to an empty directory under tmp_path and runs devops --dry-run install-tools status.
  • THEN The command exits 0 and its output holds not installed.

Requirement: Network-profile case starts no tool of the host

The direct-executor network-profile case SHALL resolve its tool from a stand-in under tmp_path.

Scenario: kube-api request with a stand-in tool

  • WHEN The case puts a popeye stand-in first on PATH under tmp_path, and runs the request with the kube-api network profile.
  • THEN The run raises SandboxError that names kube-api, and the stand-in writes no marker file.

Requirement: The four modules pass on a host with the interfering state

The four test modules SHALL pass and skip no test on a host that holds /tmp/config.yaml and the installed tools.

Scenario: the four modules on the host that failed

  • WHEN uv run devops ci test runs the four modules on the devcontainer host.
  • THEN The run reports no failure and no error, and no test is skipped.

Existing Tool Considered

No alternatives check has run. No open, maintained tool or library covers this need: the change edits four test modules and one config search of this repository. It builds no code to keep, so the cost is the edit alone: about 25,000 tokens and under one hour of elapsed time, with no code that later needs fixes.

Key questions

  • Which case of the SSH test depends on the checkout directory name, and how does it become hermetic?: The fallback case does. With no .devcontainer in the directory, get_ssh_key_prefix names the key after the process working directory, which is the checkout. The test changes into /my_plain_repo before the assertion, as its other cases already change into their own directories. (best practice) [sources: tests/crypto/test_ssh.py, src/devops_cli/crypto/ssh_keys.py, docs/TESTING.md]
    • Chosen: change into the directory under test. The case with an unparseable .devcontainer already pins the directory-name fallback, and the test comment names the base name of the process working directory.
    • Rejected: change get_ssh_key_prefix to use the given directory. It changes the fallback that the case with an unparseable .devcontainer pins, and the issue is a test fix.
    • Rejected: skip the case when the name differs. docs/TESTING.md forbids a skip on host state.
  • Why does the project config search reach /tmp/config.yaml, and what bound stops it?: The search walks up from the base directory to the filesystem root and accepts any config.yaml. It stops at an ancestor that holds no project marker: the base directory itself, or an ancestor with .git or .devcontainer, gives a config. (best practice) [sources: tests/telemetry/test_config_env_overrides.py, tests/crypto/test_ssh.py, src/devops_cli/config/settings.py, docs/TESTING.md]
    • Chosen: accept an ancestor config only at a project marker. It keeps the ancestor lookup that the .devcontainer/config.yaml case needs, and it ignores a stray file in the system temporary directory.
    • Rejected: stop the walk at the system temporary directory. It hard-codes a host path, while a project marker is a property of the project.
    • Rejected: set HOME to tmp_path in the test. The run's home is no ancestor of tmp_path, so the walk still reaches the temporary directory.
  • How does the install-tools status case keep the host's installed tools out?: It sets PATH to an empty directory under tmp_path, so the lookup of each tool finds no tool and the status says not installed. (best practice) [sources: tests/commands/test_devcontainer_install_tools_dry_run.py, src/devops_cli/commands/install_tools.py, docs/TESTING.md]
    • Chosen: set PATH to an empty directory under tmp_path. It is the test's own environment, and a dry run starts no program.
    • Rejected: patch shutil.which. Patching a stdlib object is the fallback for an edge with no seam, and PATH reaches the lookup already.
    • Rejected: accept either status text. It removes the contract that the test checks.
  • Which direct-executor case reaches a tool under the user home directory and a network edge?: The network-profile case does. It names the tool popeye and no stand-in, so the direct executor resolves the host's copy, which then reaches the Kubernetes API. [sources: tests/ai/test_tool_executor_contract.py]
  • How does the network-profile case keep the host's tool out?: It puts a popeye stand-in first on PATH under tmp_path, as the scenario fixture puts the helm stand-in, and it asserts that the stand-in wrote no marker. The refusal then names kube-api on every host. (best practice) [sources: tests/ai/test_tool_executor_contract.py, docs/TESTING.md]
    • Chosen: put a stand-in for the tool first on PATH. The scenario fixture already does this for helm, and the case then resolves no tool of the host.
    • Rejected: expect the missing-tool error. The case must hold the refusal of the network profile, which is the behaviour under test.
    • Rejected: mark the case bwrap. The direct executor runs without the sandbox, and the mark hides the case from the default run.
  • May a test skip when the host has the interfering state?: No. A test must not skip on host state: a case that cannot run the same on every host is a contract case or a row of devops ci. (best practice) [sources: docs/TESTING.md, docs/adr/0007-tests-double-only-process-edges.md]
    • Chosen: no skip on host state. docs/TESTING.md states it, and a skip hides the defect from the gate.
    • Rejected: skip when the state is present. It leaves the four modules untested on the host that failed.

Fit

Fits in one pull request.

Suspected block

None.

Dependencies

None.

Activity

  1. added
    type/bugDefect, regression, or incorrect runtime behavior
    scope/ciGitHub Actions workflows, pre-commit hooks, CI gates, and runners
    status/newStatus New: the item is not ready yet
    value/highValue High: the delivery of the item matters much
    on Nov 5, 2026
  2. dan-petty commented on Nov 5, 2026

    @dan-petty
    OwnerAuthor

    Intake placed this item to the backlog: intake puts every new item in the backlog. A person plans the milestones.

    • Type: type/bug. The fix prefix of the title gives the type.
    • Scope: scope/ci. The (ci) scope of the title gives the scope.
    • Priority: P1-High. The CI gate fails on a clean worktree of origin/main, blocking all merges.
    • Value: High. A red CI gate blocks all development work on the repository.
    • Effort: Medium. Four distinct test fixes plus one error case, each with a different root cause.
    • Related: fix(tests): a devcontainer dry-run test reads the tools of the host #3025, which asks for a different change. Both fix the devcontainer dry-run test reading host tools, but the candidate also fixes three other tests and one error case.
  3. added
    priority/p1-highPriority P1: high-impact work for the next planned milestone
    on Nov 5, 2026
  4. added
    status/readyStatus Ready: problem, acceptance criteria and an answer to each key question
    and removed
    status/newStatus New: the item is not ready yet
    on Nov 5, 2026
  5. devops-cli-service commented on Nov 5, 2026

    @devops-cli-service
    Contributor

    Status set to Ready at 502219a (see proposed design in issue body).

  6. dan-petty commented on Nov 5, 2026

    @dan-petty
    OwnerAuthor

    Evidence from a build of issue #2919 on this host.

    A worktree of origin/main at 57f280c fails the same four tests, and the same one error case, when it runs uv run pytest on the four test files. The messages match the messages in this issue.

    The build branch of #2919 adds no change to these tests. It fails the same way in the full gate uv run devops ci, and the other 49 checks of that gate pass. This comment only adds evidence. No code was changed for these tests.

  7. dan-petty commented on Nov 5, 2026

    @dan-petty
    OwnerAuthor

    Review of #3495 on branch fix/3495-service-counters-zero-series-repo-label. The full uv run devops ci run fails the same four tests and one case that this issue lists.

    The same five tests also fail with the source of origin/main in the same checkout. So this branch does not cause them. This issue covers them, and no new issue is filed for them.

  8. dan-petty commented on Nov 5, 2026

    @dan-petty
    OwnerAuthor

    The host-state failures reproduce on branch feat/3039-verifier-reads-repo-two-rounds. The branch head is 268d01d, based on origin/main d254410. The full run of devops ci gave 4 failed tests and 1 error. The same tests fail:

    • tests/crypto/test_ssh.py::test_get_ssh_key_prefix_resolution
    • tests/telemetry/test_config_env_overrides.py::test_find_project_config_path_skips_directory
    • tests/commands/test_devcontainer_install_tools_dry_run.py::test_mark_only_commands_under_global_dry_run
    • tests/ai/test_tool_executor_contract.py::test_the_direct_executor_refuses_a_network_profile_before_it_starts_the_tool[direct] (failed, and errored at teardown)

    No other test fails on the branch.

  9. dan-petty commented on Nov 5, 2026

    @dan-petty
    OwnerAuthor

    Evidence from the build of #3430 on branch perf/3430-read-the-default-tree-once-per-run, based on origin/main 66a8a63.

    The full uv run devops ci run gave 4 failed tests and 1 error. The tests are the same as in this issue:

    • tests/crypto/test_ssh.py::test_get_ssh_key_prefix_resolution
    • tests/telemetry/test_config_env_overrides.py::test_find_project_config_path_skips_directory
    • tests/commands/test_devcontainer_install_tools_dry_run.py::test_mark_only_commands_under_global_dry_run
    • tests/ai/test_tool_executor_contract.py::test_the_direct_executor_refuses_a_network_profile_before_it_starts_the_tool[direct] (failed, and errored at teardown)

    The same four tests fail with the same messages in a clean worktree of origin/main 66a8a63. The other 23 rows of the gate pass, and the row coverage fails only because the row test fails. The total coverage is 94.52%.

    The pytest step took 6m 9s, over the 5 minute budget. The slowest test was tests/commands/test_ai_local_dry_run.py::test_local_ai_commands_run_cleanly, at 45.79s.

    This comment adds evidence. No code was changed for these tests.

  10. dan-petty commented on Nov 5, 2026

    @dan-petty
    OwnerAuthor

    The full gate of the pull request for #2613 (#3639) reproduces these failures on a clean origin/main worktree: 4 failed, 183 passed, 1 error in the four test files and the contract file. No new failure comes from that branch.

  11. dan-petty commented on Nov 5, 2026

    @dan-petty
    OwnerAuthor

    The same 4 failures and 1 error appear at main commit 66a8a63 in a devcontainer, with no change applied. They are the four tests that the issue names and the teardown error of the Popeye case. The full gate of the branch for #3235 shows them as its only failures.

  12. dan-petty commented on Nov 5, 2026

    @dan-petty
    OwnerAuthor

    The same four tests and the same error fail again on 2026-11-05 on the branch fix/3656-hosted-gateway-priced-local (#3663), in a full devops ci --check run: 4 failed, 18809 passed, 2 skipped, 1 error. With the change of that branch stashed, the same four tests give 4 failed, 1 passed, 1 error on origin/main at f6d57ec. The causes are the ones in this issue.

  13. dan-petty commented on Nov 5, 2026

    @dan-petty
    OwnerAuthor

    The full gate run for PR #3664 (a worktree nested in the main checkout, 2026-11-05) hit the same four failures and one teardown error: test_get_ssh_key_prefix_resolution, test_find_project_config_path_skips_directory (a file /tmp/config.yaml on the host), test_mark_only_commands_under_global_dry_run (devcontainer installed on the host) and test_the_direct_executor_refuses_a_network_profile_before_it_starts_the_tool[direct] (the popeye binary of the host). The GitHub runners passed all checks of that pull request.

  14. dan-petty commented on Nov 5, 2026

    @dan-petty
    OwnerAuthor

    The same four failures and one error occur again on origin/main at f6d57ecd, in a clean worktree, with the stray file /tmp/config.yaml and the tools of the user data directory on this host (seen on the branch of PR #3669). Issues #3590, #3593, #3594 and #3597 report the same failures. One of them should stay open and the others should close as duplicates.

  15. dan-petty commented on Nov 5, 2026

    @dan-petty
    OwnerAuthor

    Evidence from the build of #2569 on branch fix/2569-mask-keeps-json-quote, based on origin/main f7320cf.

    The full uv run devops ci run on this host gave 4 failed tests and 1 error. The tests are the same as in this issue:

    • tests/crypto/test_ssh.py::test_get_ssh_key_prefix_resolution
    • tests/telemetry/test_config_env_overrides.py::test_find_project_config_path_skips_directory
    • tests/commands/test_devcontainer_install_tools_dry_run.py::test_mark_only_commands_under_global_dry_run
    • tests/ai/test_tool_executor_contract.py::test_the_direct_executor_refuses_a_network_profile_before_it_starts_the_tool[direct] (one failure and one teardown error)

    The branch changes only the secret masking of src/devops_cli/security/sanitizer.py and its tests, so it does not cause them. The other rows of the gate passed. The cause is host state: the file /tmp/config.yaml has ssh.key_prefix: devops_cli, the devcontainer tool and popeye 0.22.1 are installed under the home directory. The test run took 8m 51s on a host with a load of about 24.

    This issue covers them, so no new issue is filed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    effort/mediumEffort Mediumpriority/p1-highPriority P1: high-impact work for the next planned milestonescope/ciGitHub Actions workflows, pre-commit hooks, CI gates, and runnerssource/agentFiled by an agentstatus/readyStatus Ready: problem, acceptance criteria and an answer to each key questiontype/bugDefect, regression, or incorrect runtime behaviorvalue/highValue High: the delivery of the item matters much

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions