Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9daf6bd
Update chart script for replicated agent-server images
aivong-openhands May 9, 2026
4e6d01d
feat: read runtime image tag from sandbox spec service file
aivong-openhands May 10, 2026
b4efd04
test: strengthen sandbox spec error message assertion
aivong-openhands May 10, 2026
3554389
feat: add --skip-version-check flag to force image tag updates
aivong-openhands May 11, 2026
b8c664e
fix: update runtime image patterns from runtime to agent-server
aivong-openhands May 11, 2026
67ff07b
PLTF-2045: improve test quality for update_openhands_charts
aivong-openhands May 11, 2026
5d2455e
fix: use correct agent-server image tag format (X.Y.Z-python)
aivong-openhands May 11, 2026
91cc0c6
test: move get_deploy_config mock response helpers to conftest
aivong-openhands May 11, 2026
8c7dc2e
test: pass count property names explicitly in parametrize
aivong-openhands May 11, 2026
b0f3675
test: split deploy_config success test into type and value checks
aivong-openhands May 11, 2026
b1ab18f
refactor: unify error_if_missing handling in update_tag_in_content
aivong-openhands May 11, 2026
9512f63
test: compare list counts to pre-update values instead of literals
aivong-openhands May 11, 2026
b002063
test: reuse mock_main_early_exit fixture in TestSkipVersionCheck
aivong-openhands May 11, 2026
852a574
test: extract stub fixtures for resolve_openhands_version and process…
aivong-openhands May 11, 2026
6af855e
test: parametrize main() output message format check
aivong-openhands May 11, 2026
2321166
refactor: use shared make_workflow_response factory in TestGetDeployC…
aivong-openhands May 11, 2026
9514e07
test: drop dedicated test classes for conftest assertion helpers
aivong-openhands May 11, 2026
479059c
test: parametrize ref-format check for cloud_tag_exists
aivong-openhands May 11, 2026
8e6e0b9
test: parametrize is_unchanged assertions in idempotency tests
aivong-openhands May 11, 2026
499f624
test: cover workflow orchestration and tighten existing tests
aivong-openhands May 11, 2026
8196276
ci: add PR check for update_openhands_charts tests
openhands-agent May 11, 2026
1f43e1a
ci: run all scripts tests in PR check
openhands-agent May 11, 2026
dc94f9e
PLTF-2502: upgrade laminar helm chart to address noisy postgres error…
aivong-openhands May 11, 2026
d04735e
Wire up Custom/Local LLM provider in Replicated installer (#626)
ak684 May 13, 2026
696e98b
restrict plugin marketplace source to supported URL schemes (#608)
jlav May 13, 2026
054b2c8
Fix support bundle errors and warnings, add runtime pod telemetry (#628)
jlav May 13, 2026
03bf6cc
PLTF-2504: upgrade laminar helm chart to 0.1.11 (rabbitmq+quickwit ha…
aivong-openhands May 13, 2026
ee1d4b2
bump chart version (#629)
aivong-openhands May 13, 2026
638770f
fix: update replicated/openhands.yaml in chart-bump script
aivong-openhands May 15, 2026
822064e
Merge remote-tracking branch 'origin/main' into av/update-openhands-c…
openhands-agent May 21, 2026
09532bd
Merge branch 'main' into av/update-openhands-charts-for-agent-server-…
aivong-openhands May 22, 2026
ff3e522
Merge branch 'main' into av/update-openhands-charts-for-agent-server-…
aivong-openhands May 26, 2026
62076d0
Merge branch 'main' into av/update-openhands-charts-for-agent-server-…
aivong-openhands May 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions scripts/update_openhands_charts/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ def sample_openhands_values_full():

runtime:
image:
repository: ghcr.io/openhands/runtime
repository: ghcr.io/openhands/agent-server
tag: cloud-1.0.0-nikolaik
runAsRoot: true

Expand All @@ -351,7 +351,7 @@ def sample_openhands_values_full():
count: 1
configs:
- name: default
image: "ghcr.io/openhands/runtime:cloud-1.0.0-nikolaik"
image: "ghcr.io/openhands/agent-server:cloud-1.0.0-nikolaik"
working_dir: "/openhands/code/"
"""

Expand All @@ -366,15 +366,15 @@ def sample_openhands_values_minimal():

runtime:
image:
repository: ghcr.io/openhands/runtime
repository: ghcr.io/openhands/agent-server
tag: cloud-1.0.0-nikolaik

runtime-api:
enabled: true
warmRuntimes:
configs:
- name: default
image: "ghcr.io/openhands/runtime:cloud-1.0.0-nikolaik"
image: "ghcr.io/openhands/agent-server:cloud-1.0.0-nikolaik"
"""


Expand All @@ -398,12 +398,40 @@ def sample_runtime_api_values():
count: 0
configs:
- name: default
image: "ghcr.io/openhands/runtime:cloud-1.0.0-nikolaik"
image: "ghcr.io/openhands/agent-server:cloud-1.0.0-nikolaik"
working_dir: "/openhands/code/"
environment: {}
"""


@pytest.fixture
def sample_replicated_openhands_wrapper_values():
"""Sample replicated openhands wrapper YAML with agent-server image references."""
return """\
spec:
values:
runtime:
image:
repository: 'images.r9.all-hands.dev/proxy/{{repl LicenseFieldValue "appSlug"}}/ghcr.io/openhands/agent-server'
tag: '1.19.0-python'
warmRuntimes:
configs:
- name: default
image: 'images.r9.all-hands.dev/proxy/{{repl LicenseFieldValue "appSlug"}}/ghcr.io/openhands/agent-server:1.19.0-python'
helmChart:
values:
runtime:
image:
repository: '{{repl LocalRegistryHost }}/{{repl LocalRegistryNamespace }}/agent-server'
tag: '1.19.0-python'
warmRuntimes:
configs:
- name: default
image: '{{repl LocalRegistryHost }}/{{repl LocalRegistryNamespace }}/agent-server:1.19.0-python'
"""



# =============================================================================
# GitHub API mock fixtures
# =============================================================================
Expand Down
6 changes: 6 additions & 0 deletions scripts/update_openhands_charts/setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[mutmut]
paths_to_mutate=update_openhands_charts.py
also_copy=
conftest.py
test_update_openhands_charts.py
tests_dir=test_update_openhands_charts.py
188 changes: 177 additions & 11 deletions scripts/update_openhands_charts/test_update_openhands_charts.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@
get_current_app_version,
get_deploy_config,
get_latest_cloud_tag,
get_runtime_image_tag_from_sandbox_spec,
get_short_sha,
main,
parse_args,
update_openhands_chart,
update_openhands_values,
update_runtime_api_chart,
Expand Down Expand Up @@ -567,6 +567,85 @@ def test_returns_correct_value_for_key(self, make_temp_yaml_file, key, expected)
# =============================================================================


class TestGetRuntimeImageTagFromSandboxSpec:
"""Tests for get_runtime_image_tag_from_sandbox_spec function.

Fetches sandbox_spec_service.py from the OpenHands repo at a specific cloud
tag and extracts the AGENT_SERVER_IMAGE tag.
"""

VALID_SANDBOX_SPEC_CONTENT = """\
AGENT_SERVER_IMAGE = 'ghcr.io/openhands/agent-server:1.19.1-python'

def get_agent_server_image():
return AGENT_SERVER_IMAGE
"""

def test_returns_image_tag_from_sandbox_spec(self, monkeypatch, make_workflow_response):
"""Test that a valid sandbox spec returns the agent-server image tag."""
response = make_workflow_response(self.VALID_SANDBOX_SPEC_CONTENT)
monkeypatch.setattr(
"update_openhands_charts.requests.get",
Mock(return_value=response)
)

result = get_runtime_image_tag_from_sandbox_spec("token", "owner/repo", ref="cloud-1.26.1")

assert result == "1.19.1-python"

def test_constructs_correct_url_with_ref(self, monkeypatch, make_workflow_response):
"""Test URL includes sandbox_spec_service.py path and ref parameter."""
mock_get = Mock(return_value=make_workflow_response(self.VALID_SANDBOX_SPEC_CONTENT))
monkeypatch.setattr("update_openhands_charts.requests.get", mock_get)

get_runtime_image_tag_from_sandbox_spec("token", "owner/repo", ref="cloud-1.26.1")

called_url = mock_get.call_args[0][0]
assert "openhands/app_server/sandbox/sandbox_spec_service.py" in called_url
assert "?ref=cloud-1.26.1" in called_url

def test_includes_authorization_header(self, monkeypatch, make_workflow_response):
"""Test that the Authorization header carries the bearer token."""
mock_get = Mock(return_value=make_workflow_response(self.VALID_SANDBOX_SPEC_CONTENT))
monkeypatch.setattr("update_openhands_charts.requests.get", mock_get)

get_runtime_image_tag_from_sandbox_spec("my-secret-token", "owner/repo", ref="cloud-1.26.1")

called_headers = mock_get.call_args[1]["headers"]
assert called_headers["Authorization"] == "Bearer my-secret-token"

def test_returns_none_and_prints_error_on_http_failure(self, monkeypatch, capsys):
"""Test graceful handling when the GitHub API request fails."""
mock_response = Mock()
mock_response.raise_for_status.side_effect = Exception("HTTP 404: Not Found")
monkeypatch.setattr(
"update_openhands_charts.requests.get",
Mock(return_value=mock_response)
)

result = get_runtime_image_tag_from_sandbox_spec("token", "owner/repo", ref="cloud-1.26.1")

assert result is None
assert "Error fetching sandbox spec" in capsys.readouterr().out

def test_returns_none_and_prints_error_when_image_constant_missing(
self, monkeypatch, make_workflow_response, capsys
):
"""Test graceful handling when AGENT_SERVER_IMAGE constant is absent."""
response = make_workflow_response("# No image constant here\n")
monkeypatch.setattr(
"update_openhands_charts.requests.get",
Mock(return_value=response)
)

result = get_runtime_image_tag_from_sandbox_spec("token", "owner/repo", ref="cloud-1.26.1")

assert result is None
out = capsys.readouterr().out
assert "Error fetching sandbox spec" in out
assert "AGENT_SERVER_IMAGE" in out


class TestGetDeployConfig:
"""Tests for get_deploy_config function.

Expand All @@ -578,7 +657,6 @@ class TestGetDeployConfig:
VALID_WORKFLOW_YAML = """\
env:
RUNTIME_API_SHA: abc123def456
OPENHANDS_RUNTIME_IMAGE_TAG: "cloud-1.21.0-nikolaik"
OTHER_VAR: value
"""

Expand All @@ -603,7 +681,6 @@ def test_returns_deploy_config_on_success(self, monkeypatch, mock_successful_res
assert result is not None
assert isinstance(result, DeployConfig)
assert result.runtime_api_sha == "abc123def456"
assert result.openhands_runtime_image_tag == "cloud-1.21.0-nikolaik"

def test_constructs_correct_url_without_ref(self, monkeypatch, mock_successful_response):
"""Test that URL is constructed correctly without ref parameter."""
Expand Down Expand Up @@ -653,7 +730,6 @@ def test_returns_empty_string_when_env_key_missing(self, monkeypatch, make_workf

assert result is not None
assert result.runtime_api_sha == ""
assert result.openhands_runtime_image_tag == ""

def test_returns_empty_string_when_env_section_missing(self, monkeypatch, make_workflow_response):
"""Test that missing env section returns empty string.
Expand All @@ -672,7 +748,6 @@ def test_returns_empty_string_when_env_section_missing(self, monkeypatch, make_w

assert result is not None
assert result.runtime_api_sha == ""
assert result.openhands_runtime_image_tag == ""

# =========================================================================
# Parameterized error path tests
Expand Down Expand Up @@ -887,7 +962,7 @@ def test_update_warm_runtimes_tag_uses_runtime_image_tag(self, temp_values_file)
runtime_image_tag="cloud-1.1.0-nikolaik",
)

assert_file_contains(temp_values_file, 'image: "ghcr.io/openhands/runtime:cloud-1.1.0-nikolaik"')
assert_file_contains(temp_values_file, 'image: "ghcr.io/openhands/agent-server:cloud-1.1.0-nikolaik"')

def test_idempotent_when_reapplying_same_values(self, temp_values_file):
"""Test that reapplying identical values is idempotent.
Expand Down Expand Up @@ -963,14 +1038,14 @@ def test_reports_error_when_enterprise_server_tag_missing(self, make_temp_yaml_f

runtime:
image:
repository: ghcr.io/openhands/runtime
repository: ghcr.io/openhands/agent-server
tag: cloud-1.0.0-nikolaik

runtime-api:
warmRuntimes:
configs:
- name: default
image: "ghcr.io/openhands/runtime:cloud-1.0.0-nikolaik"
image: "ghcr.io/openhands/agent-server:cloud-1.0.0-nikolaik"
"""
temp_file = make_temp_yaml_file(values_content)

Expand Down Expand Up @@ -1000,7 +1075,7 @@ def test_reports_error_when_runtime_tag_missing(self, make_temp_yaml_file):
warmRuntimes:
configs:
- name: default
image: "ghcr.io/openhands/runtime:cloud-1.0.0-nikolaik"
image: "ghcr.io/openhands/agent-server:cloud-1.0.0-nikolaik"
"""
temp_file = make_temp_yaml_file(values_content)

Expand Down Expand Up @@ -1028,7 +1103,7 @@ def test_reports_error_when_warm_runtimes_tag_missing(self, make_temp_yaml_file)

runtime:
image:
repository: ghcr.io/openhands/runtime
repository: ghcr.io/openhands/agent-server
tag: cloud-1.0.0-nikolaik

runtime-api:
Expand Down Expand Up @@ -1073,6 +1148,58 @@ def test_collects_multiple_errors_when_multiple_patterns_missing(self, make_temp
assert result.has_error_containing("warmRuntimes")



class TestUpdateReplicatedOpenhandsWrapperValues:
"""Tests for replicated OpenHands wrapper agent-server image updates."""

@pytest.fixture
def temp_replicated_wrapper_file(self, make_temp_yaml_file, sample_replicated_openhands_wrapper_values):
"""Create a temporary replicated wrapper YAML file."""
return make_temp_yaml_file(sample_replicated_openhands_wrapper_values)

def test_updates_all_three_agent_server_tag_references(self, temp_replicated_wrapper_file):
"""Test that all replicated wrapper agent-server references use the new runtime image tag."""
result = update_openhands_values(
temp_replicated_wrapper_file,
openhands_version="cloud-1.19.1",
runtime_image_tag="1.19.1-python",
)

assert_file_contains(temp_replicated_wrapper_file, "tag: '1.19.1-python'")
assert_file_contains(
temp_replicated_wrapper_file,
"image: 'images.r9.all-hands.dev/proxy/{{repl LicenseFieldValue \"appSlug\"}}/ghcr.io/openhands/agent-server:1.19.1-python'",
)
assert_file_contains(
temp_replicated_wrapper_file,
"image: '{{repl LocalRegistryHost }}/{{repl LocalRegistryNamespace }}/agent-server:1.19.1-python'",
)
assert result.has_change_for("replicated runtime image tag")
assert result.has_change_for("replicated warmRuntimes image tag")
assert result.has_change_for("replicated local registry runtime image tag")
assert result.has_change_for("replicated local registry warmRuntimes image tag")

def test_is_idempotent_for_replicated_wrapper_references(self, temp_replicated_wrapper_file):
"""Test that reapplying identical replicated wrapper values records unchanged entries."""
update_openhands_values(
temp_replicated_wrapper_file,
openhands_version="cloud-1.19.1",
runtime_image_tag="1.19.1-python",
)

result = update_openhands_values(
temp_replicated_wrapper_file,
openhands_version="cloud-1.19.1",
runtime_image_tag="1.19.1-python",
)

assert result.has_changes is False
assert result.is_unchanged("replicated runtime image tag")
assert result.is_unchanged("replicated warmRuntimes image tag")
assert result.is_unchanged("replicated local registry runtime image tag")
assert result.is_unchanged("replicated local registry warmRuntimes image tag")


class TestConditionalChartVersionBump:
"""Tests for conditional chart version bumping across both chart types.

Expand Down Expand Up @@ -1324,7 +1451,7 @@ def test_update_warm_runtimes_image_uses_runtime_image_tag(self, temp_runtime_ap
)

# Should use runtime_image_tag from deploy config
assert_file_contains(temp_runtime_api_values_file, 'image: "ghcr.io/openhands/runtime:cloud-1.1.0-nikolaik"')
assert_file_contains(temp_runtime_api_values_file, 'image: "ghcr.io/openhands/agent-server:cloud-1.1.0-nikolaik"')

def test_idempotent_when_reapplying_same_values(self, temp_runtime_api_values_file):
"""Test that reapplying identical values is idempotent.
Expand Down Expand Up @@ -1390,6 +1517,45 @@ def test_returns_true_when_changes_made(self, temp_runtime_api_values_file):
assert result.has_changes is True


class TestSkipVersionCheck:
"""Tests for --skip-version-check flag behavior.

Without the flag, the script exits early when the chart version already
matches the latest cloud tag. With the flag, it continues past that check.
"""

MOCK_CLOUD_TAG = "cloud-1.20.0"

@pytest.fixture
def mock_versions_match(self, monkeypatch):
"""Mock environment where chart version already matches latest cloud tag."""
monkeypatch.setenv("GITHUB_TOKEN", "dummy-token")
monkeypatch.setattr(
"update_openhands_charts.get_latest_cloud_tag",
lambda token, repo: self.MOCK_CLOUD_TAG,
)
monkeypatch.setattr(
"update_openhands_charts.get_current_app_version",
lambda path: self.MOCK_CLOUD_TAG,
)
monkeypatch.setattr(
"update_openhands_charts.get_runtime_image_tag_from_sandbox_spec",
lambda token, repo, ref: None,
)

def test_exits_early_when_versions_match_without_flag(self, mock_versions_match, capsys):
"""Without --skip-version-check, exits with 'already up to date' message."""
main(dry_run=True)

assert "Charts are already up to date" in capsys.readouterr().out

def test_skips_up_to_date_check_when_flag_set(self, mock_versions_match, capsys):
"""With --skip-version-check, continues past version check even when versions match."""
main(dry_run=True, skip_version_check=True)

assert "Charts are already up to date" not in capsys.readouterr().out


class TestMainOutputMessages:
"""Tests for main() output message formatting."""

Expand Down
Loading
Loading