Repository navigation
Description
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 namemy_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.yamlon 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 notnot 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_repoand callsget_ssh_key_prefixon that directory. - THEN The call returns
my_plain_repoon a host whose checkout directory isdevops_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.yamlsits in the parent of the base directory, and that parent holds no.gitand no.devcontainer. - THEN
_find_project_config_pathreturns 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_pathand runsdevops --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
popeyestand-in first on PATH undertmp_path, and runs the request with thekube-apinetwork profile. - THEN The run raises
SandboxErrorthat nameskube-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 testruns 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
.devcontainerin the directory,get_ssh_key_prefixnames the key after the process working directory, which is the checkout. The test changes into/my_plain_repobefore 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
.devcontaineralready pins the directory-name fallback, and the test comment names the base name of the process working directory. - Rejected: change
get_ssh_key_prefixto use the given directory. It changes the fallback that the case with an unparseable.devcontainerpins, and the issue is a test fix. - Rejected: skip the case when the name differs. docs/TESTING.md forbids a skip on host state.
- Chosen: change into the directory under test. The case with an unparseable
- 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 anyconfig.yaml. It stops at an ancestor that holds no project marker: the base directory itself, or an ancestor with.gitor.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.yamlcase 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_pathin the test. The run's home is no ancestor oftmp_path, so the walk still reaches the temporary directory.
- Chosen: accept an ancestor config only at a project marker. It keeps the ancestor lookup that the
- 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 saysnot 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.
- Chosen: set PATH to an empty directory under
- 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
popeyeand 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
popeyestand-in first on PATH undertmp_path, as the scenario fixture puts thehelmstand-in, and it asserts that the stand-in wrote no marker. The refusal then nameskube-apion 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.
- Chosen: put a stand-in for the tool first on PATH. The scenario fixture already does this for
- 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
dan-petty commentedon Nov 5, 2026
Intake placed this item to the backlog: intake puts every new item in the backlog. A person plans the milestones.
fixprefix of the title gives the type.(ci)scope of the title gives the scope.The CI gate fails on a clean worktree of origin/main, blocking all merges.A red CI gate blocks all development work on the repository.Four distinct test fixes plus one error case, each with a different root cause.Both fix the devcontainer dry-run test reading host tools, but the candidate also fixes three other tests and one error case.devops-cli-service commentedon Nov 5, 2026
Status set to Ready at 502219a (see proposed design in issue body).
dan-petty commentedon Nov 5, 2026
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 pyteston 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.dan-petty commentedon Nov 5, 2026
Review of #3495 on branch fix/3495-service-counters-zero-series-repo-label. The full
uv run devops cirun 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.
dan-petty commentedon Nov 5, 2026
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 cigave 4 failed tests and 1 error. The same tests fail:tests/crypto/test_ssh.py::test_get_ssh_key_prefix_resolutiontests/telemetry/test_config_env_overrides.py::test_find_project_config_path_skips_directorytests/commands/test_devcontainer_install_tools_dry_run.py::test_mark_only_commands_under_global_dry_runtests/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.
dan-petty commentedon Nov 5, 2026
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 cirun 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_resolutiontests/telemetry/test_config_env_overrides.py::test_find_project_config_path_skips_directorytests/commands/test_devcontainer_install_tools_dry_run.py::test_mark_only_commands_under_global_dry_runtests/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
coveragefails only because the rowtestfails. 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.
dan-petty commentedon Nov 5, 2026
The full gate of the pull request for #2613 (#3639) reproduces these failures on a clean
origin/mainworktree:4 failed, 183 passed, 1 errorin the four test files and the contract file. No new failure comes from that branch.dan-petty commentedon Nov 5, 2026
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.
dan-petty commentedon Nov 5, 2026
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 fulldevops ci --checkrun:4 failed, 18809 passed, 2 skipped, 1 error. With the change of that branch stashed, the same four tests give4 failed, 1 passed, 1 erroronorigin/mainat f6d57ec. The causes are the ones in this issue.dan-petty commentedon Nov 5, 2026
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.
dan-petty commentedon Nov 5, 2026
The same four failures and one error occur again on
origin/mainatf6d57ecd, in a clean worktree, with the stray file/tmp/config.yamland 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.dan-petty commentedon Nov 5, 2026
Evidence from the build of #2569 on branch fix/2569-mask-keeps-json-quote, based on origin/main f7320cf.
The full
uv run devops cirun 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_resolutiontests/telemetry/test_config_env_overrides.py::test_find_project_config_path_skips_directorytests/commands/test_devcontainer_install_tools_dry_run.py::test_mark_only_commands_under_global_dry_runtests/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.pyand its tests, so it does not cause them. The other rows of the gate passed. The cause is host state: the file/tmp/config.yamlhasssh.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.