381 lines
13 KiB
Python
Raw Normal View History

2026-08-10 00:23:20 +02:00
# retoor <retoor@molodetz.nl>
import json
Route container proxies through the leg that is actually reachable The workspace editor hung for 60s and then 504'd. Three independent faults were stacked behind that one symptom. Reachability: editor_target delegated to proxy_target, which returns CONTAINER_PROXY_HOST plus the published host port and never falls back to the container. From inside the app container that address crosses docker0 into the host INPUT chain, whose policy is DROP with an allow-list that does not include the published port range, so the packet was dropped and the request hung rather than being refused. Measured from the app container: container_ip:8443 answers 302, gateway:20006 is dropped. One shared reachable_target now prefers the direct container leg and falls back to the published port, and editor_target uses tunnel_target as services/containers/CLAUDE.md already required. The same defect affected /p/{slug} ingress and every tunnel, since all three resolved through proxy_target. The recorded measurement that motivated the old order (container_ip times out, gateway connects) no longer holds: make docker-attach puts the app on the instances' bridge network, which is what makes the direct leg work. Duplicate response headers: the forwarding core relayed the upstream Date and Server alongside the ones the serving layer generates, so every proxied response carried two of each. Both are singleton headers and duplicating them is malformed HTTP. Serialization: WorkspaceViewOut declared flag_reason and three sibling strings as str, so a NULL column made the workspace page 500 for JSON clients. Documents the two public hostnames and the devplace.net SSH tunnel, so a future session does not conclude the site is down after pointing curl --resolve at an address the hostname does not resolve to, and adds the layered procedure for diagnosing a production failure. Verified on production with Playwright over both hostnames: the code-server login renders and the workbench loads. Suite: 3345 passed.
2026-08-11 20:03:15 +02:00
import shlex
2026-08-10 00:23:20 +02:00
import pytest
from devplacepy import config
from devplacepy.database import get_table, init_db, set_setting
from devplacepy.services.containers.backend.base import (
WORKSPACE_MOUNT,
WORKSPACE_STATE_MOUNT,
)
from devplacepy.services.containers.workspace import editor, quota
OWNER = "editor-owner"
OTHER = "editor-other"
@pytest.fixture(autouse=True)
def _editor_db(tmp_path, monkeypatch):
init_db()
monkeypatch.setattr(config, "WORKSPACE_STATE_DIR", tmp_path / "state")
yield
get_table(editor.PREFS_TABLE).delete()
get_table(quota.RULES_TABLE).delete()
for key, default in editor.DEFAULTS.items():
setting = editor.SETTING_KEYS[key]
if isinstance(default, bool):
set_setting(setting, "1" if default else "0")
else:
set_setting(setting, str(default))
def _instance(uid: str = "ws-editor") -> dict:
return {"uid": uid, "editor_port": editor.EDITOR_DEFAULT_PORT, "is_workspace": 1}
def _prefs(**values) -> dict:
return editor.save_prefs(OWNER, values)
def test_resolve_uses_site_defaults():
profile = editor.resolve(OWNER)
assert profile.theme == editor.DEFAULTS["theme"]
assert profile.font_size == editor.DEFAULTS["font_size"]
assert profile.zoom_level == editor.DEFAULTS["zoom_level"]
assert profile.boot_agent == editor.DEFAULTS["boot_agent"]
assert profile.trust_all is True
def test_resolve_reads_a_changed_site_setting():
set_setting("workspace_editor_font_size", "22")
assert editor.resolve(OWNER).font_size == 22
def test_resolve_prefers_the_user_row():
_prefs(font_size=20, theme="devplace-light")
profile = editor.resolve(OWNER)
assert profile.font_size == 20
assert profile.theme == "devplace-light"
def test_a_user_row_does_not_leak_to_another_user():
_prefs(font_size=20)
assert editor.resolve(OTHER).font_size == editor.DEFAULTS["font_size"]
def test_zero_and_empty_inherit_but_zoom_zero_does_not():
set_setting("workspace_editor_font_size", "22")
set_setting("workspace_editor_zoom_level", "3")
_prefs(font_size=0, theme="", zoom_level=0)
profile = editor.resolve(OWNER)
assert profile.font_size == 22
assert profile.theme == editor.DEFAULTS["theme"]
assert profile.zoom_level == 0
def test_the_zoom_sentinel_inherits():
set_setting("workspace_editor_zoom_level", "3")
_prefs(zoom_level=editor.INHERIT_ZOOM)
assert editor.resolve(OWNER).zoom_level == 3
def test_the_boot_shell_sentinel_inherits_but_zero_does_not():
_prefs(boot_shell=editor.INHERIT_FLAG)
assert editor.resolve(OWNER).boot_shell is True
_prefs(boot_shell=0)
assert editor.resolve(OWNER).boot_shell is False
def test_out_of_range_stored_values_are_clamped():
_prefs(font_size=999, zoom_level=99, window_width=1)
profile = editor.resolve(OWNER)
assert profile.font_size == editor.BOUNDS["font_size"][1]
assert profile.zoom_level == editor.BOUNDS["zoom_level"][1]
assert profile.window_width == editor.BOUNDS["window_width"][0]
def test_an_unknown_choice_falls_back_to_the_default():
_prefs(theme="hot-pink", layout="chaos", window_mode="teleport")
profile = editor.resolve(OWNER)
assert profile.theme == editor.DEFAULTS["theme"]
assert profile.layout == editor.DEFAULTS["layout"]
assert profile.window_mode == editor.DEFAULTS["window_mode"]
def test_the_container_size_comes_from_the_quota_resolver():
profile = editor.resolve(OWNER, {"workspace_cpu_millicores": 3000})
assert profile.cpu_millicores == 3000
assert profile.cpu_cores() == 3.0
assert profile.cpu_limit() == "3"
assert profile.mem_limit() == quota.format_memory(profile.memory_mb)
def test_source_map_reports_where_each_value_came_from():
_prefs(font_size=20)
sources = editor.source_map(OWNER)
assert sources["font_size"] == editor.SOURCE_USER
assert sources["theme"] == editor.SOURCE_SITE
assert sources["trust_all"] == editor.SOURCE_SITE
def test_reset_removes_the_row_and_restores_the_defaults():
_prefs(font_size=20)
assert editor.reset_prefs(OWNER, OWNER) is True
assert editor.resolve(OWNER).font_size == editor.DEFAULTS["font_size"]
assert editor.source_map(OWNER)["font_size"] == editor.SOURCE_SITE
def test_reset_on_a_user_with_no_row_is_a_no_op():
assert editor.reset_prefs(OWNER, OWNER) is False
def test_saving_twice_updates_one_row():
_prefs(font_size=20)
_prefs(font_size=21)
rows = list(get_table(editor.PREFS_TABLE).find(owner_id=OWNER, deleted_at=None))
assert len(rows) == 1
assert rows[0]["font_size"] == 21
def test_merge_managed_seeds_an_absent_key():
merged, managed = editor.merge_managed({}, {}, {"editor.fontSize": 14})
assert merged["editor.fontSize"] == 14
assert managed == {"editor.fontSize": 14}
def test_merge_managed_updates_a_value_we_still_own():
merged, _ = editor.merge_managed(
{"editor.fontSize": 14}, {"editor.fontSize": 14}, {"editor.fontSize": 18}
)
assert merged["editor.fontSize"] == 18
def test_merge_managed_respects_a_member_edit_forever():
current = {"editor.fontSize": 30}
managed = {"editor.fontSize": 14}
for desired in (16, 18, 20):
current, managed = editor.merge_managed(
current, managed, {"editor.fontSize": desired}
)
assert current["editor.fontSize"] == 30
def test_merge_managed_keeps_unrelated_member_keys():
merged, _ = editor.merge_managed(
{"files.autoSave": "on"}, {}, {"editor.fontSize": 14}
)
assert merged["files.autoSave"] == "on"
def test_merge_managed_is_idempotent():
desired = editor.settings_for(editor.resolve(OWNER))
first, managed = editor.merge_managed({}, {}, desired)
second, _ = editor.merge_managed(first, managed, desired)
assert first == second
def test_settings_disable_workspace_trust_when_trust_all():
settings = editor.settings_for(editor.resolve(OWNER))
assert settings["security.workspace.trust.enabled"] is False
assert settings["security.workspace.trust.startupPrompt"] == "never"
assert settings["task.allowAutomaticTasks"] == "on"
def test_settings_leave_trust_alone_when_the_kill_switch_is_off():
set_setting("workspace_editor_trust_all", "0")
settings = editor.settings_for(editor.resolve(OWNER))
assert "security.workspace.trust.enabled" not in settings
def test_settings_carry_the_devplace_terminal_profile():
settings = editor.settings_for(editor.resolve(OWNER))
profiles = settings["terminal.integrated.profiles.linux"]
assert profiles["DevPlace Code"]["path"] == "/usr/bin/dpc"
assert settings["terminal.integrated.defaultProfile.linux"] == "bash"
def test_settings_apply_the_layout_preset():
_prefs(layout="zen")
settings = editor.settings_for(editor.resolve(OWNER))
assert settings["workbench.activityBar.location"] == "hidden"
assert settings["editor.minimap.enabled"] is False
def test_the_system_theme_is_left_to_the_member():
_prefs(theme="system")
assert "workbench.colorTheme" not in editor.settings_for(editor.resolve(OWNER))
def test_seed_state_writes_the_whole_tree():
instance = _instance()
profile = editor.resolve(OWNER)
assert editor.seed_state(instance, profile) is True
root = editor.state_dir(instance)
settings = json.loads((root / "data" / "User" / "settings.json").read_text())
managed = json.loads((root / "data" / "User" / editor.MANAGED_FILE).read_text())
payload = json.loads((root / editor.PROFILE_FILE).read_text())
assert settings["editor.fontSize"] == profile.font_size
assert managed["editor.fontSize"] == profile.font_size
assert payload["editor"]["theme"] == profile.theme
assert payload["app_name"] == editor.APP_NAME
def test_seed_state_never_clobbers_a_member_edit():
instance = _instance()
editor.seed_state(instance, editor.resolve(OWNER))
settings_path = editor.state_dir(instance) / "data" / "User" / "settings.json"
stored = json.loads(settings_path.read_text())
stored["editor.fontSize"] = 30
stored["files.autoSave"] = "on"
settings_path.write_text(json.dumps(stored))
set_setting("workspace_editor_font_size", "18")
set_setting("workspace_editor_terminal_font_size", "20")
editor.seed_state(instance, editor.resolve(OWNER))
written = json.loads(settings_path.read_text())
assert written["editor.fontSize"] == 30
assert written["files.autoSave"] == "on"
assert written["terminal.integrated.fontSize"] == 20
def test_seed_state_replaces_an_unparseable_settings_file():
instance = _instance()
user_dir = editor.state_dir(instance) / "data" / "User"
user_dir.mkdir(parents=True, exist_ok=True)
(user_dir / "settings.json").write_text("{not json at all")
assert editor.seed_state(instance, editor.resolve(OWNER)) is True
assert json.loads((user_dir / "settings.json").read_text())["editor.fontSize"]
def test_seed_state_fails_soft_on_an_unwritable_directory(monkeypatch):
def explode(*args, **kwargs):
raise OSError("read-only file system")
monkeypatch.setattr("pathlib.Path.mkdir", explode)
assert editor.seed_state(_instance(), editor.resolve(OWNER)) is False
def test_argv_carries_the_devplace_brand():
instance = _instance()
command = editor.argv(instance, editor.resolve(OWNER))
assert command[0] == "code-server"
assert command[command.index("--app-name") + 1] == editor.APP_NAME
assert "--disable-getting-started-override" in command
assert "--disable-telemetry" in command
def test_argv_disables_workspace_trust_when_trust_all():
command = editor.argv(_instance(), editor.resolve(OWNER))
assert "--disable-workspace-trust" in command
def test_argv_keeps_workspace_trust_when_the_kill_switch_is_off():
set_setting("workspace_editor_trust_all", "0")
command = editor.argv(_instance(), editor.resolve(OWNER))
assert "--disable-workspace-trust" not in command
def test_argv_keeps_password_auth():
command = editor.argv(_instance(), editor.resolve(OWNER))
assert command[command.index("--auth") + 1] == "password"
assert "none" not in command
def test_argv_paths_sit_under_the_state_mount():
command = editor.argv(_instance(), editor.resolve(OWNER))
assert command[command.index("--user-data-dir") + 1].startswith(
WORKSPACE_STATE_MOUNT
)
assert command[command.index("--extensions-dir") + 1].startswith(
WORKSPACE_STATE_MOUNT
)
assert command[-1] == WORKSPACE_MOUNT
def test_argv_binds_the_instance_editor_port():
instance = _instance()
instance["editor_port"] = 9443
command = editor.argv(instance, editor.resolve(OWNER))
assert command[command.index("--bind-addr") + 1] == "0.0.0.0:9443"
def test_every_optional_flag_is_declared():
command = editor.argv(_instance(), editor.resolve(OWNER))
for flag in editor.OPTIONAL_FLAGS:
assert flag in command
Route container proxies through the leg that is actually reachable The workspace editor hung for 60s and then 504'd. Three independent faults were stacked behind that one symptom. Reachability: editor_target delegated to proxy_target, which returns CONTAINER_PROXY_HOST plus the published host port and never falls back to the container. From inside the app container that address crosses docker0 into the host INPUT chain, whose policy is DROP with an allow-list that does not include the published port range, so the packet was dropped and the request hung rather than being refused. Measured from the app container: container_ip:8443 answers 302, gateway:20006 is dropped. One shared reachable_target now prefers the direct container leg and falls back to the published port, and editor_target uses tunnel_target as services/containers/CLAUDE.md already required. The same defect affected /p/{slug} ingress and every tunnel, since all three resolved through proxy_target. The recorded measurement that motivated the old order (container_ip times out, gateway connects) no longer holds: make docker-attach puts the app on the instances' bridge network, which is what makes the direct leg work. Duplicate response headers: the forwarding core relayed the upstream Date and Server alongside the ones the serving layer generates, so every proxied response carried two of each. Both are singleton headers and duplicating them is malformed HTTP. Serialization: WorkspaceViewOut declared flag_reason and three sibling strings as str, so a NULL column made the workspace page 500 for JSON clients. Documents the two public hostnames and the devplace.net SSH tunnel, so a future session does not conclude the site is down after pointing curl --resolve at an address the hostname does not resolve to, and adds the layered procedure for diagnosing a production failure. Verified on production with Playwright over both hostnames: the code-server login renders and the workbench loads. Suite: 3345 passed.
2026-08-11 20:03:15 +02:00
def test_wrap_with_env_export_runs_the_original_command_through_a_shell():
command = editor.argv(_instance(), editor.resolve(OWNER))
wrapped = editor.wrap_with_env_export(command)
assert wrapped[0] == "/bin/sh"
assert wrapped[1] == "-c"
script = wrapped[2]
assert script.rstrip().endswith(shlex.join(command))
def test_wrap_with_env_export_writes_only_devplace_prefixed_vars():
script = editor.wrap_with_env_export(["true"])[2]
assert "python3 -c" in script
assert editor.ENV_EXPORT_FILE in script
assert "DEVPLACE_" in script
assert "startswith" in script
2026-08-10 00:23:20 +02:00
def test_env_for_exports_the_profile_to_the_container():
env = editor.env_for(editor.resolve(OWNER))
assert env["DEVPLACE_EDITOR_APP_NAME"] == editor.APP_NAME
assert env["DEVPLACE_EDITOR_PROFILE"].endswith(editor.PROFILE_FILE)
assert env["DEVPLACE_EDITOR_BOOT_AGENT"] == editor.DEFAULTS["boot_agent"]
assert env["DEVPLACE_EDITOR_TRUST_ALL"] == "1"
assert all(isinstance(value, str) for value in env.values())
def test_restart_is_not_required_before_the_first_boot():
instance = _instance()
assert editor.restart_required(instance, editor.resolve(OWNER)) is False
def test_restart_is_not_required_when_nothing_changed():
instance = _instance()
profile = editor.resolve(OWNER)
editor.seed_state(instance, profile)
assert editor.restart_required(instance, profile) is False
def test_restart_is_required_after_a_preference_change():
instance = _instance()
editor.seed_state(instance, editor.resolve(OWNER))
_prefs(font_size=27)
assert editor.restart_required(instance, editor.resolve(OWNER)) is True
def test_view_carries_the_profile_and_its_sources():
_prefs(theme="devplace-light")
payload = editor.view(OWNER)
assert payload["theme"] == "devplace-light"
assert payload["sources"]["theme"] == editor.SOURCE_USER
assert payload["cpu_cores"] == editor.resolve(OWNER).cpu_cores()
def test_settings_suppress_a_foreign_ai_assistant():
settings = editor.settings_for(editor.resolve(OWNER))
assert settings["chat.disableAIFeatures"] is True
assert settings["workbench.secondarySideBar.defaultVisibility"] == "hidden"
def test_settings_open_straight_into_the_workspace():
assert editor.settings_for(editor.resolve(OWNER))["workbench.startupEditor"] == "none"
def test_the_agent_working_files_never_sync_into_the_project():
from devplacepy.project_files import SYNC_SKIP_NAMES
assert ".dpc" in SYNC_SKIP_NAMES
assert "dpc.log" in SYNC_SKIP_NAMES
assert ".devplace" in SYNC_SKIP_NAMES