dev-bubble 0.7.20__tar.gz → 0.7.21__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. {dev_bubble-0.7.20/dev_bubble.egg-info → dev_bubble-0.7.21}/PKG-INFO +1 -1
  2. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/__init__.py +1 -1
  3. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/auth_proxy.py +10 -6
  4. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/config.py +9 -0
  5. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/runtime/incus.py +18 -10
  6. {dev_bubble-0.7.20 → dev_bubble-0.7.21/dev_bubble.egg-info}/PKG-INFO +1 -1
  7. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/dev_bubble.egg-info/SOURCES.txt +1 -0
  8. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/conftest.py +8 -0
  9. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_auth_proxy.py +56 -1
  10. dev_bubble-0.7.21/tests/test_get_info_remote.py +138 -0
  11. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/.claude/CLAUDE.md +0 -0
  12. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/.github/workflows/ci.yml +0 -0
  13. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/.github/workflows/publish.yml +0 -0
  14. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/.gitignore +0 -0
  15. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/CHANGELOG.md +0 -0
  16. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/LICENSE +0 -0
  17. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/README.md +0 -0
  18. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/SPEC.md +0 -0
  19. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/__main__.py +0 -0
  20. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/ai.py +0 -0
  21. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/automation.py +0 -0
  22. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/clean.py +0 -0
  23. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/cli.py +0 -0
  24. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/clone.py +0 -0
  25. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/cloud.py +0 -0
  26. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/cloud_types.py +0 -0
  27. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/commands/__init__.py +0 -0
  28. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/commands/cloud_cmd.py +0 -0
  29. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/commands/completion.py +0 -0
  30. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/commands/doctor.py +0 -0
  31. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/commands/images.py +0 -0
  32. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/commands/infrastructure.py +0 -0
  33. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/commands/internal.py +0 -0
  34. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/commands/lifecycle.py +0 -0
  35. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/commands/list_cmd.py +0 -0
  36. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/commands/relay_cmd.py +0 -0
  37. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/commands/remote_cmd.py +0 -0
  38. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/commands/security_cmd.py +0 -0
  39. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/commands/settings.py +0 -0
  40. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/commands/status_cmd.py +0 -0
  41. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/container_helpers.py +0 -0
  42. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/data/skill.md +0 -0
  43. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/default_repos.json +0 -0
  44. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/finalization.py +0 -0
  45. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/git_store.py +0 -0
  46. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/github_token.py +0 -0
  47. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/graphql_validator.py +0 -0
  48. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/hooks/__init__.py +0 -0
  49. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/hooks/lean.py +0 -0
  50. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/hooks/python.py +0 -0
  51. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/image_management.py +0 -0
  52. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/images/__init__.py +0 -0
  53. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/images/builder.py +0 -0
  54. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/images/scripts/base.sh +0 -0
  55. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/images/scripts/cloud-init.sh +0 -0
  56. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/images/scripts/lean-toolchain.sh +0 -0
  57. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/images/scripts/lean.sh +0 -0
  58. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/images/scripts/python.sh +0 -0
  59. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/images/scripts/tools/claude.sh +0 -0
  60. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/images/scripts/tools/codex.sh +0 -0
  61. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/images/scripts/tools/elan.sh +0 -0
  62. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/images/scripts/tools/emacs.sh +0 -0
  63. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/images/scripts/tools/gh.sh +0 -0
  64. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/images/scripts/tools/neovim.sh +0 -0
  65. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/images/scripts/tools/pi.sh +0 -0
  66. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/images/scripts/tools/pins.json +0 -0
  67. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/images/scripts/tools/uv.sh +0 -0
  68. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/images/scripts/tools/vscode.sh +0 -0
  69. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/incus_bridge.py +0 -0
  70. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/lean.py +0 -0
  71. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/lifecycle.py +0 -0
  72. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/naming.py +0 -0
  73. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/network.py +0 -0
  74. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/notices.py +0 -0
  75. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/output.py +0 -0
  76. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/provisioning.py +0 -0
  77. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/relay.py +0 -0
  78. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/remote.py +0 -0
  79. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/repo_registry.py +0 -0
  80. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/runtime/__init__.py +0 -0
  81. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/runtime/base.py +0 -0
  82. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/runtime/colima.py +0 -0
  83. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/security.py +0 -0
  84. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/setup.py +0 -0
  85. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/skill.py +0 -0
  86. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/spinner.py +0 -0
  87. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/target.py +0 -0
  88. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/token_store.py +0 -0
  89. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/tools.py +0 -0
  90. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/tunnel.py +0 -0
  91. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/bubble/vscode.py +0 -0
  92. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/config/com.bubble.git-update.plist +0 -0
  93. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/config/com.bubble.image-refresh.plist +0 -0
  94. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/config/com.bubble.relay-daemon.plist +0 -0
  95. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/conftest.py +0 -0
  96. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/dev_bubble.egg-info/dependency_links.txt +0 -0
  97. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/dev_bubble.egg-info/entry_points.txt +0 -0
  98. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/dev_bubble.egg-info/requires.txt +0 -0
  99. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/dev_bubble.egg-info/top_level.txt +0 -0
  100. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/pyproject.toml +0 -0
  101. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/scratch/forkproxy-repro.sh +0 -0
  102. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/setup.cfg +0 -0
  103. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_ai.py +0 -0
  104. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_authorized_keys.py +0 -0
  105. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_branch_no_target.py +0 -0
  106. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_bridge_listener.py +0 -0
  107. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_build_lock.py +0 -0
  108. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_claude_projects_symlink.py +0 -0
  109. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_cloud.py +0 -0
  110. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_colima.py +0 -0
  111. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_completion.py +0 -0
  112. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_config.py +0 -0
  113. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_customize.py +0 -0
  114. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_editor.py +0 -0
  115. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_ephemeral.py +0 -0
  116. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_git_store.py +0 -0
  117. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_github_security_override.py +0 -0
  118. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_github_token.py +0 -0
  119. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_graphql_validator.py +0 -0
  120. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_hooks.py +0 -0
  121. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_integration.py +0 -0
  122. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_internal.py +0 -0
  123. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_lifecycle.py +0 -0
  124. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_list_columns.py +0 -0
  125. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_list_remote.py +0 -0
  126. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_mounts.py +0 -0
  127. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_multi_target.py +0 -0
  128. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_naming.py +0 -0
  129. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_network.py +0 -0
  130. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_notices.py +0 -0
  131. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_reattach_network.py +0 -0
  132. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_relay.py +0 -0
  133. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_remote.py +0 -0
  134. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_repo_registry.py +0 -0
  135. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_security.py +0 -0
  136. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_skill.py +0 -0
  137. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_spinner.py +0 -0
  138. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_status.py +0 -0
  139. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_systemd_path.py +0 -0
  140. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_target.py +0 -0
  141. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_token_no_argv_leak.py +0 -0
  142. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_tools.py +0 -0
  143. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_tunnel.py +0 -0
  144. {dev_bubble-0.7.20 → dev_bubble-0.7.21}/tests/test_vscode.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dev-bubble
3
- Version: 0.7.20
3
+ Version: 0.7.21
4
4
  Summary: Containerized development environments powered by Incus
5
5
  Author-email: Kim Morrison <kim@tqft.net>
6
6
  License-Expression: Apache-2.0
@@ -1,3 +1,3 @@
1
1
  """bubble: Containerized development environments."""
2
2
 
3
- __version__ = "0.7.20"
3
+ __version__ = "0.7.21"
@@ -76,14 +76,18 @@ from urllib.request import (
76
76
  build_opener,
77
77
  )
78
78
 
79
- from .config import DATA_DIR
79
+ from .config import AUTH_PROXY_DIR
80
80
  from .token_store import RateLimiter as _RateLimiter
81
81
  from .token_store import RateWindow, TokenStore, setup_file_logging
82
82
 
83
- AUTH_PROXY_PORT_FILE = DATA_DIR / "auth-proxy.port"
84
- AUTH_PROXY_ENDPOINT_FILE = DATA_DIR / "auth-proxy.endpoint"
85
- AUTH_PROXY_LOG = DATA_DIR / "auth-proxy.log"
86
- AUTH_PROXY_TOKENS = DATA_DIR / "auth-tokens.json"
83
+ # The daemon is a host singleton that always uses ~/.bubble (see
84
+ # AUTH_PROXY_DIR in config.py). Both the daemon and callers running under a
85
+ # custom BUBBLE_HOME must resolve these files against that fixed location so
86
+ # they agree on where the endpoint and per-container tokens live.
87
+ AUTH_PROXY_PORT_FILE = AUTH_PROXY_DIR / "auth-proxy.port"
88
+ AUTH_PROXY_ENDPOINT_FILE = AUTH_PROXY_DIR / "auth-proxy.endpoint"
89
+ AUTH_PROXY_LOG = AUTH_PROXY_DIR / "auth-proxy.log"
90
+ AUTH_PROXY_TOKENS = AUTH_PROXY_DIR / "auth-tokens.json"
87
91
 
88
92
  # Default port (configurable via config.toml)
89
93
  DEFAULT_PORT = 7654
@@ -1574,7 +1578,7 @@ def run_daemon(port: int = 0):
1574
1578
  port: Port to listen on. 0 means use config or default.
1575
1579
  """
1576
1580
  _setup_logging()
1577
- DATA_DIR.mkdir(parents=True, exist_ok=True)
1581
+ AUTH_PROXY_DIR.mkdir(parents=True, exist_ok=True)
1578
1582
 
1579
1583
  if not port:
1580
1584
  from .config import load_config
@@ -20,6 +20,15 @@ import tomli_w
20
20
  # Override with BUBBLE_HOME environment variable
21
21
  DATA_DIR = Path(os.environ.get("BUBBLE_HOME", Path.home() / ".bubble"))
22
22
  CONFIG_FILE = DATA_DIR / "config.toml"
23
+
24
+ # The auth-proxy daemon is a host singleton, installed via launchd/systemd
25
+ # with no BUBBLE_HOME in its environment, so it always runs against the
26
+ # default ~/.bubble and writes its endpoint/port/token/log files there.
27
+ # Callers (e.g. `bubble open`) may run under a custom BUBBLE_HOME, but must
28
+ # resolve those daemon files against the daemon's fixed location rather than
29
+ # their own DATA_DIR — otherwise they look for an endpoint the daemon never
30
+ # wrote and write tokens the daemon never reads. See issue #304.
31
+ AUTH_PROXY_DIR = Path.home() / ".bubble"
23
32
  REGISTRY_FILE = DATA_DIR / "registry.json"
24
33
  GIT_DIR = DATA_DIR / "git"
25
34
  REPOS_FILE = DATA_DIR / "repos.json"
@@ -199,17 +199,25 @@ class IncusRuntime(ContainerRuntime):
199
199
  def _get_info(self, name: str) -> ContainerInfo:
200
200
  """Get info for a single container.
201
201
 
202
- Filters the full listing in Python rather than passing
203
- ``<remote>:<name>`` as an incus list filter: on recent incus
204
- versions ``incus list <remote>:<name>`` returns nothing (the
205
- ``remote:name`` form isn't treated as a name filter), whereas
206
- ``incus list <remote>:`` reliably scopes to the remote. This is
207
- the same workaround the no-name path in ``list_containers``
208
- already relies on.
202
+ Passes the remote scope and the name filter as *separate*
203
+ arguments (``incus list <remote>: name=<name>``). Concatenating
204
+ them into one token (``incus list <remote>:<name>``) breaks on
205
+ recent incus versions: a ``list`` argument that doesn't end in
206
+ ``:`` is treated as a name filter on the *default* remote, so it
207
+ matches nothing on a non-default remote (e.g. ``bubble-colima``
208
+ on macOS). The bare ``<remote>:`` token reliably scopes the list
209
+ to the remote, the same way the no-name path in
210
+ ``list_containers`` already relies on.
211
+
212
+ ``name=<name>`` is matched as a substring on some incus versions,
213
+ so we still confirm an exact name match before returning.
209
214
  """
210
- for info in self.list_containers(fast=False):
211
- if info.name == name:
212
- return info
215
+ scope = [self._q("")] if self._remote else []
216
+ data = self._run_json(["list", *scope, f"name={name}"])
217
+ if isinstance(data, list):
218
+ for c in data:
219
+ if c.get("name") == name:
220
+ return self._parse_container(c)
213
221
  raise RuntimeError(f"Container '{name}' not found")
214
222
 
215
223
  def list_containers(self, fast: bool = True) -> list[ContainerInfo]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dev-bubble
3
- Version: 0.7.20
3
+ Version: 0.7.21
4
4
  Summary: Containerized development environments powered by Incus
5
5
  Author-email: Kim Morrison <kim@tqft.net>
6
6
  License-Expression: Apache-2.0
@@ -110,6 +110,7 @@ tests/test_config.py
110
110
  tests/test_customize.py
111
111
  tests/test_editor.py
112
112
  tests/test_ephemeral.py
113
+ tests/test_get_info_remote.py
113
114
  tests/test_git_store.py
114
115
  tests/test_github_security_override.py
115
116
  tests/test_github_token.py
@@ -156,7 +156,15 @@ def tmp_data_dir(tmp_path, monkeypatch):
156
156
  try:
157
157
  import bubble.auth_proxy as auth_proxy_mod
158
158
 
159
+ # These files are pinned to the fixed singleton ~/.bubble
160
+ # (AUTH_PROXY_DIR), not DATA_DIR, so they don't follow BUBBLE_HOME
161
+ # (issue #304). Patch all four explicitly to keep tests off the
162
+ # real ~/.bubble.
159
163
  monkeypatch.setattr(auth_proxy_mod, "AUTH_PROXY_PORT_FILE", data_dir / "auth-proxy.port")
164
+ monkeypatch.setattr(
165
+ auth_proxy_mod, "AUTH_PROXY_ENDPOINT_FILE", data_dir / "auth-proxy.endpoint"
166
+ )
167
+ monkeypatch.setattr(auth_proxy_mod, "AUTH_PROXY_LOG", data_dir / "auth-proxy.log")
160
168
  monkeypatch.setattr(auth_proxy_mod, "AUTH_PROXY_TOKENS", data_dir / "auth-tokens.json")
161
169
  except ImportError:
162
170
  pass
@@ -24,6 +24,48 @@ from bubble.auth_proxy import (
24
24
  validate_path,
25
25
  )
26
26
 
27
+ # ---------------------------------------------------------------------------
28
+ # Daemon file location (issue #304)
29
+ # ---------------------------------------------------------------------------
30
+
31
+
32
+ class TestDaemonFileLocation:
33
+ """The auth-proxy daemon is a host singleton pinned to ~/.bubble.
34
+
35
+ Its endpoint/port/token/log files must resolve against that fixed
36
+ location even when the caller runs under a custom BUBBLE_HOME, or the
37
+ caller looks for an endpoint the daemon never wrote and writes tokens
38
+ the daemon never reads (issue #304).
39
+ """
40
+
41
+ def test_files_ignore_bubble_home(self, tmp_path, monkeypatch):
42
+ import importlib
43
+ from pathlib import Path
44
+
45
+ import bubble.auth_proxy
46
+ import bubble.config
47
+
48
+ fixed = Path.home() / ".bubble"
49
+ try:
50
+ monkeypatch.setenv("BUBBLE_HOME", str(tmp_path))
51
+ importlib.reload(bubble.config)
52
+ importlib.reload(bubble.auth_proxy)
53
+
54
+ # DATA_DIR follows BUBBLE_HOME ...
55
+ assert bubble.config.DATA_DIR == tmp_path
56
+ # ... but the daemon's files stay at the fixed singleton location.
57
+ assert bubble.config.AUTH_PROXY_DIR == fixed
58
+ assert bubble.auth_proxy.AUTH_PROXY_ENDPOINT_FILE == fixed / "auth-proxy.endpoint"
59
+ assert bubble.auth_proxy.AUTH_PROXY_PORT_FILE == fixed / "auth-proxy.port"
60
+ assert bubble.auth_proxy.AUTH_PROXY_TOKENS == fixed / "auth-tokens.json"
61
+ assert bubble.auth_proxy.AUTH_PROXY_LOG == fixed / "auth-proxy.log"
62
+ finally:
63
+ # Restore default module state for subsequent tests.
64
+ monkeypatch.delenv("BUBBLE_HOME", raising=False)
65
+ importlib.reload(bubble.config)
66
+ importlib.reload(bubble.auth_proxy)
67
+
68
+
27
69
  # ---------------------------------------------------------------------------
28
70
  # Token management
29
71
  # ---------------------------------------------------------------------------
@@ -1669,7 +1711,14 @@ class TestApiProxyIntegration:
1669
1711
 
1670
1712
  @pytest.fixture
1671
1713
  def auth_proxy_env(tmp_path, monkeypatch):
1672
- """Set BUBBLE_HOME to tmp_path and reload auth_proxy module."""
1714
+ """Isolate the auth_proxy daemon's files into tmp_path.
1715
+
1716
+ The daemon's endpoint/port/token/log files are pinned to the fixed
1717
+ singleton location ~/.bubble (AUTH_PROXY_DIR), independent of
1718
+ BUBBLE_HOME (see issue #304), so setting BUBBLE_HOME alone would leave
1719
+ these tests writing the real ~/.bubble/auth-tokens.json. Monkeypatch
1720
+ the module constants directly to keep the tests hermetic.
1721
+ """
1673
1722
  import importlib
1674
1723
 
1675
1724
  import bubble.auth_proxy
@@ -1678,4 +1727,10 @@ def auth_proxy_env(tmp_path, monkeypatch):
1678
1727
  monkeypatch.setenv("BUBBLE_HOME", str(tmp_path))
1679
1728
  importlib.reload(bubble.config)
1680
1729
  importlib.reload(bubble.auth_proxy)
1730
+ monkeypatch.setattr(bubble.auth_proxy, "AUTH_PROXY_PORT_FILE", tmp_path / "auth-proxy.port")
1731
+ monkeypatch.setattr(
1732
+ bubble.auth_proxy, "AUTH_PROXY_ENDPOINT_FILE", tmp_path / "auth-proxy.endpoint"
1733
+ )
1734
+ monkeypatch.setattr(bubble.auth_proxy, "AUTH_PROXY_LOG", tmp_path / "auth-proxy.log")
1735
+ monkeypatch.setattr(bubble.auth_proxy, "AUTH_PROXY_TOKENS", tmp_path / "auth-tokens.json")
1681
1736
  return tmp_path
@@ -0,0 +1,138 @@
1
+ """Regression test for issue #300: container lookup on a non-default remote.
2
+
3
+ On macOS/Colima, ``IncusRuntime`` is constructed with
4
+ ``remote="bubble-colima"``. Recent incus clients treat a single
5
+ concatenated ``list`` token (``incus list <remote>:<name>``) as a name
6
+ filter on the *default* remote, so it matches nothing on a non-default
7
+ remote and ``_get_info`` raised "Container not found" even when the
8
+ container was running (breaking the base-image rebuild path).
9
+
10
+ ``_get_info`` must instead pass the remote scope and the name filter as
11
+ *separate* arguments (``incus list <remote>: name=<name>``).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import subprocess
18
+
19
+ import pytest
20
+
21
+ from bubble.runtime.incus import IncusRuntime
22
+
23
+
24
+ def _fake_subprocess(records, containers):
25
+ """A ``_run_subprocess`` stand-in that records argv and serves JSON.
26
+
27
+ The fake models incus's ``name=`` filtering so tests exercise the real
28
+ behavior ``_get_info`` depends on, not just the argv shape: a ``list``
29
+ with a ``name=<value>`` token returns *containers* whose names contain
30
+ ``<value>`` as a substring, mirroring incus versions where ``name=``
31
+ over-matches.
32
+ """
33
+
34
+ def run(self, cmd, *, capture=True):
35
+ records.append(cmd)
36
+ name_filter = next((a[len("name=") :] for a in cmd if a.startswith("name=")), None)
37
+ if name_filter is not None:
38
+ result = [c for c in containers if name_filter in c["name"]]
39
+ else:
40
+ result = containers
41
+ return subprocess.CompletedProcess(cmd, 0, stdout=json.dumps(result), stderr="")
42
+
43
+ return run
44
+
45
+
46
+ def _container(name, status="Running"):
47
+ return {"name": name, "status": status, "state": {}}
48
+
49
+
50
+ def test_get_info_uses_separate_remote_and_filter_tokens(monkeypatch):
51
+ records: list[list[str]] = []
52
+ monkeypatch.setattr(
53
+ IncusRuntime,
54
+ "_run_subprocess",
55
+ _fake_subprocess(records, [_container("base-builder")]),
56
+ )
57
+ rt = IncusRuntime(remote="bubble-colima")
58
+ info = rt._get_info("base-builder")
59
+ assert info.name == "base-builder"
60
+ # Exactly one incus invocation, with the remote scope and name filter as
61
+ # *separate* argv tokens (never the broken "bubble-colima:base-builder").
62
+ assert len(records) == 1
63
+ cmd = records[0]
64
+ assert cmd == ["incus", "list", "bubble-colima:", "name=base-builder", "--format=json"]
65
+ assert "bubble-colima:base-builder" not in cmd
66
+
67
+
68
+ def test_get_info_no_remote_omits_scope_token(monkeypatch):
69
+ records: list[list[str]] = []
70
+ monkeypatch.setattr(
71
+ IncusRuntime,
72
+ "_run_subprocess",
73
+ _fake_subprocess(records, [_container("foo")]),
74
+ )
75
+ rt = IncusRuntime()
76
+ info = rt._get_info("foo")
77
+ assert info.name == "foo"
78
+ assert records[0] == ["incus", "list", "name=foo", "--format=json"]
79
+
80
+
81
+ def test_get_info_exact_match_among_substring_matches(monkeypatch):
82
+ # Some incus versions match `name=base` as a substring, returning both
83
+ # "base" and "base-builder"; _get_info must return the exact match.
84
+ records: list[list[str]] = []
85
+ monkeypatch.setattr(
86
+ IncusRuntime,
87
+ "_run_subprocess",
88
+ _fake_subprocess(records, [_container("base-builder"), _container("base")]),
89
+ )
90
+ rt = IncusRuntime(remote="bubble-colima")
91
+ info = rt._get_info("base")
92
+ assert info.name == "base"
93
+
94
+
95
+ def test_get_info_not_found_raises(monkeypatch):
96
+ records: list[list[str]] = []
97
+ monkeypatch.setattr(
98
+ IncusRuntime,
99
+ "_run_subprocess",
100
+ _fake_subprocess(records, []),
101
+ )
102
+ rt = IncusRuntime(remote="bubble-colima")
103
+ with pytest.raises(RuntimeError, match="not found"):
104
+ rt._get_info("missing")
105
+
106
+
107
+ def test_get_info_superstring_only_is_not_a_match(monkeypatch):
108
+ # Asking for "base-build" when only "base-builder" exists must NOT match.
109
+ records: list[list[str]] = []
110
+ monkeypatch.setattr(
111
+ IncusRuntime,
112
+ "_run_subprocess",
113
+ _fake_subprocess(records, [_container("base-builder")]),
114
+ )
115
+ rt = IncusRuntime(remote="bubble-colima")
116
+ with pytest.raises(RuntimeError, match="not found"):
117
+ rt._get_info("base-build")
118
+
119
+
120
+ def test_launch_then_lookup_on_remote(monkeypatch):
121
+ """Reproduces issue #300 end-to-end: launch base-builder on the
122
+ bubble-colima remote, then look it up via the separate-token filter."""
123
+ records: list[list[str]] = []
124
+ monkeypatch.setattr(
125
+ IncusRuntime,
126
+ "_run_subprocess",
127
+ _fake_subprocess(records, [_container("base-builder")]),
128
+ )
129
+ rt = IncusRuntime(remote="bubble-colima")
130
+ info = rt.launch("base-builder", "base")
131
+ assert info.name == "base-builder"
132
+ launch_cmd, list_cmd = records
133
+ # launch uses remote:name as a single resource identifier (correct).
134
+ assert launch_cmd == ["incus", "launch", "bubble-colima:base", "bubble-colima:base-builder"]
135
+ # the follow-up lookup must NOT concatenate remote and name.
136
+ assert "bubble-colima:base-builder" not in list_cmd
137
+ assert "bubble-colima:" in list_cmd
138
+ assert "name=base-builder" in list_cmd
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes