Skip to content

Commit a011481

Browse files
authored
Merge pull request #314 from thevibeworks/refactor/container-slug-naming
refactor(slug): redesign container naming format
2 parents fc4a95f + c320df6 commit a011481

1 file changed

Lines changed: 318 additions & 0 deletions

File tree

scripts/test-container-slug.sh

Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
6+
7+
PASS=0
8+
FAIL=0
9+
ERRORS=""
10+
11+
pass() { PASS=$((PASS + 1)); echo " ok: $1"; }
12+
fail() { FAIL=$((FAIL + 1)); ERRORS="${ERRORS}\n FAIL: $1"; echo " FAIL: $1" >&2; }
13+
14+
assert_eq() {
15+
local label="$1" expected="$2" actual="$3"
16+
if [ "$expected" = "$actual" ]; then
17+
pass "$label"
18+
else
19+
fail "$label: expected='$expected' actual='$actual'"
20+
fi
21+
}
22+
23+
assert_match() {
24+
local label="$1" pattern="$2" actual="$3"
25+
if [[ "$actual" =~ $pattern ]]; then
26+
pass "$label"
27+
else
28+
fail "$label: pattern='$pattern' actual='$actual'"
29+
fi
30+
}
31+
32+
assert_no_match() {
33+
local label="$1" pattern="$2" actual="$3"
34+
if [[ "$actual" =~ $pattern ]]; then
35+
fail "$label: should NOT match pattern='$pattern' actual='$actual'"
36+
else
37+
pass "$label"
38+
fi
39+
}
40+
41+
# ──────────────────────────────────────
42+
# Source the helper functions from deva.sh without running it.
43+
# We extract just the function definitions we need.
44+
# ──────────────────────────────────────
45+
source_helpers() {
46+
eval "$(sed -n '/^sanitize_slug_component()/,/^}/p' "$REPO_ROOT/deva.sh")"
47+
eval "$(sed -n '/^extract_auth_file_stem()/,/^}/p' "$REPO_ROOT/deva.sh")"
48+
eval "$(sed -n '/^short_hash()/,/^}/p' "$REPO_ROOT/deva.sh")"
49+
eval "$(sed -n '/^compute_shape_hash()/,/^}/p' "$REPO_ROOT/deva.sh")"
50+
eval "$(sed -n '/^generate_auth_tag()/,/^}/p' "$REPO_ROOT/deva.sh")"
51+
eval "$(sed -n '/^build_container_name()/,/^}/p' "$REPO_ROOT/deva.sh")"
52+
eval "$(sed -n '/^generate_container_slug_for_path()/,/^}/p' "$REPO_ROOT/deva.sh")"
53+
eval "$(sed -n '/^compute_slug_components_for_path()/,/^}/p' "$REPO_ROOT/deva.sh")"
54+
eval "$(sed -n '/^extract_agent_from_name()/,/^}/p' "$REPO_ROOT/deva.sh")"
55+
DEVA_CONTAINER_PREFIX="deva"
56+
}
57+
58+
source_helpers
59+
60+
# ──────────────────────────────────────
61+
echo "=== extract_auth_file_stem ==="
62+
# ──────────────────────────────────────
63+
64+
assert_eq "credentials.json suffix" \
65+
"fffff" \
66+
"$(extract_auth_file_stem "/home/user/.config/deva/claude/fffff.credentials.json")"
67+
68+
assert_eq "plain .json suffix" \
69+
"pianolaauth" \
70+
"$(extract_auth_file_stem "/home/user/.config/deva/codex/pianolaauth.json")"
71+
72+
assert_eq "no extension" \
73+
"mytoken" \
74+
"$(extract_auth_file_stem "/tmp/mytoken")"
75+
76+
assert_eq "dots in stem" \
77+
"my-auth-v2" \
78+
"$(extract_auth_file_stem "/tmp/my.auth.v2.credentials.json")"
79+
80+
assert_eq "long stem truncated to 20" \
81+
"$(printf '%s' "this-is-a-very-long-" | head -c 20)" \
82+
"$(extract_auth_file_stem "/tmp/this-is-a-very-long-credential-filename.credentials.json")"
83+
84+
# ──────────────────────────────────────
85+
echo "=== generate_auth_tag ==="
86+
# ──────────────────────────────────────
87+
88+
assert_eq "claude default" "auth-default" "$(generate_auth_tag claude claude)"
89+
assert_eq "codex default" "auth-default" "$(generate_auth_tag codex chatgpt)"
90+
assert_eq "gemini default" "auth-default" "$(generate_auth_tag gemini oauth)"
91+
assert_eq "gemini app-oauth default" "auth-default" "$(generate_auth_tag gemini gemini-app-oauth)"
92+
assert_eq "empty method" "auth-default" "$(generate_auth_tag claude "")"
93+
94+
assert_eq "claude api-key (no env)" "api-key" "$(generate_auth_tag claude api-key)"
95+
assert_eq "codex api-key (no env)" "api-key" "$(generate_auth_tag codex api-key)"
96+
97+
assert_eq "claude api-key with env" "api-key-abcd" \
98+
"$(ANTHROPIC_API_KEY="sk-ant-test1234abcd" generate_auth_tag claude api-key)"
99+
100+
assert_eq "codex api-key with env" "api-key-9876" \
101+
"$(OPENAI_API_KEY="sk-proj-xyzw9876" generate_auth_tag codex api-key)"
102+
103+
assert_eq "gemini api-key with env" "api-key-end5" \
104+
"$(GEMINI_API_KEY="AIza-short-key-end5" generate_auth_tag gemini api-key)"
105+
106+
assert_eq "credentials-file with path" \
107+
"auth-file-fffff" \
108+
"$(generate_auth_tag claude credentials-file "/home/u/.config/deva/claude/fffff.credentials.json")"
109+
110+
assert_eq "credentials-file with plain json" \
111+
"auth-file-pianolaauth" \
112+
"$(generate_auth_tag codex credentials-file "/home/u/.config/deva/codex/pianolaauth.json")"
113+
114+
assert_eq "credentials-file no path" "auth-file" "$(generate_auth_tag claude credentials-file)"
115+
116+
assert_eq "bedrock method" "bedrock" "$(generate_auth_tag claude bedrock)"
117+
assert_eq "vertex method" "vertex" "$(generate_auth_tag claude vertex)"
118+
assert_eq "copilot method" "copilot" "$(generate_auth_tag claude copilot)"
119+
assert_eq "oat method" "oat" "$(generate_auth_tag claude oat)"
120+
121+
assert_eq "env override" "env" "$(generate_auth_tag claude claude "" true)"
122+
123+
assert_eq "gemini-api-key method" "api-key" "$(generate_auth_tag gemini gemini-api-key)"
124+
assert_eq "gemini-api-key with GEMINI_API_KEY" "api-key-xyzw" \
125+
"$(GEMINI_API_KEY="AIza-test-xyzw" generate_auth_tag gemini gemini-api-key)"
126+
assert_eq "gemini-api-key GOOGLE_API_KEY fallback" "api-key-goog" \
127+
"$(GOOGLE_API_KEY="AIza-test-goog" generate_auth_tag gemini gemini-api-key)"
128+
129+
assert_eq "compute-adc method" "compute-adc" "$(generate_auth_tag gemini compute-adc)"
130+
131+
assert_eq "api-key 3 char key -> no suffix" "api-key" \
132+
"$(ANTHROPIC_API_KEY="abc" generate_auth_tag claude api-key)"
133+
assert_eq "api-key exactly 4 chars" "api-key-abcd" \
134+
"$(ANTHROPIC_API_KEY="abcd" generate_auth_tag claude api-key)"
135+
136+
assert_eq "sanitizes unknown method with special chars" "some-weird-method" \
137+
"$(generate_auth_tag claude "some.weird/method")"
138+
139+
# ──────────────────────────────────────
140+
echo "=== compute_shape_hash ==="
141+
# ──────────────────────────────────────
142+
143+
raw_hash=$(short_hash "test-input" 8)
144+
assert_match "short_hash produces hex" "^[a-f0-9]{8}$" "$raw_hash"
145+
146+
h1=$(compute_shape_hash "ghcr.io/thevibeworks/deva:latest" "" "")
147+
h2=$(compute_shape_hash "ghcr.io/thevibeworks/deva:latest" "" "")
148+
assert_eq "deterministic" "$h1" "$h2"
149+
150+
h3=$(compute_shape_hash "ghcr.io/thevibeworks/deva:nightly" "" "")
151+
assert_no_match "different image -> different hash" "^${h1}$" "$h3"
152+
153+
h4=$(compute_shape_hash "ghcr.io/thevibeworks/deva:latest" "/vol1:/dst1|" "")
154+
assert_no_match "volumes change hash" "^${h1}$" "$h4"
155+
156+
h5=$(compute_shape_hash "ghcr.io/thevibeworks/deva:latest" "" "/custom/config")
157+
assert_no_match "config changes hash" "^${h1}$" "$h5"
158+
159+
assert_match "hash is 8 chars hex" "^[a-f0-9]{8}$" "$h1"
160+
161+
# ──────────────────────────────────────
162+
echo "=== build_container_name ==="
163+
# ──────────────────────────────────────
164+
165+
name=$(build_container_name "deva" "claude" "auth-default" "lroolle-deploydock" "ab12cd34" "false" "")
166+
assert_eq "persistent default" \
167+
"deva--claude--auth-default--lroolle-deploydock..ab12cd34" "$name"
168+
169+
name=$(build_container_name "deva" "codex" "auth-file-pianolaauth" "lroolle-deploydock" "ab12cd34" "false" "")
170+
assert_eq "persistent with auth-file" \
171+
"deva--codex--auth-file-pianolaauth--lroolle-deploydock..ab12cd34" "$name"
172+
173+
name=$(build_container_name "deva" "claude" "api-key-abcd" "myrepo" "ff00ff00" "true" "12345")
174+
assert_eq "ephemeral with api-key" \
175+
"deva--claude--api-key-abcd--myrepo..ff00ff00--12345" "$name"
176+
177+
name=$(build_container_name "deva" "gemini" "vertex" "big-project" "11223344" "false" "")
178+
assert_eq "persistent vertex auth" \
179+
"deva--gemini--vertex--big-project..11223344" "$name"
180+
181+
# ──────────────────────────────────────
182+
echo "=== extract_agent_from_name ==="
183+
# ──────────────────────────────────────
184+
185+
assert_eq "new format claude" "claude" \
186+
"$(extract_agent_from_name "deva--claude--auth-default--myrepo..ab12cd34")"
187+
188+
assert_eq "new format codex" "codex" \
189+
"$(extract_agent_from_name "deva--codex--auth-file-foo--myrepo..ab12cd34")"
190+
191+
assert_eq "new format gemini" "gemini" \
192+
"$(extract_agent_from_name "deva--gemini--vertex--myrepo..ab12cd34")"
193+
194+
assert_eq "new format ephemeral" "claude" \
195+
"$(extract_agent_from_name "deva--claude--api-key-abcd--myrepo..ab12cd34--99999")"
196+
197+
assert_eq "legacy ephemeral" "claude" \
198+
"$(extract_agent_from_name "deva-myrepo..i47b207-claude-12345")"
199+
200+
assert_eq "legacy persistent (no agent)" "share" \
201+
"$(extract_agent_from_name "deva-myrepo..i47b207..va3797701")"
202+
203+
# ──────────────────────────────────────
204+
echo "=== container name format (regex structure) ==="
205+
# ──────────────────────────────────────
206+
207+
name=$(build_container_name "deva" "claude" "auth-default" "lroolle-deploydock" "ab12cd34" "false" "")
208+
assert_match "double-dash field separators" \
209+
"^deva--[a-z]+--[a-z0-9-]+--[a-zA-Z0-9-]+\.\.[a-f0-9]{8}$" "$name"
210+
211+
name=$(build_container_name "deva" "codex" "auth-file-test" "myrepo" "ab12cd34" "true" "999")
212+
assert_match "ephemeral PID suffix" \
213+
"--[0-9]+$" "$name"
214+
215+
# ──────────────────────────────────────
216+
echo "=== fallback regex matches both formats ==="
217+
# ──────────────────────────────────────
218+
219+
escaped_slug="lroolle-deploydock"
220+
rgx="^deva(--.*--${escaped_slug}|-${escaped_slug})(\\.\\.|-|$)"
221+
222+
old_name="deva-lroolle-deploydock..i47b207..va3797701"
223+
new_name="deva--claude--auth-default--lroolle-deploydock..ab12cd34"
224+
225+
assert_match "regex matches old format" "$rgx" "$old_name"
226+
assert_match "regex matches new format" "$rgx" "$new_name"
227+
assert_no_match "regex rejects unrelated" "$rgx" "deva-other-project..i47b207"
228+
229+
# ──────────────────────────────────────
230+
echo "=== integration: dry-run container name ==="
231+
# ──────────────────────────────────────
232+
233+
tmp_home="$(mktemp -d)"
234+
cleanup() { rm -rf "$tmp_home"; }
235+
trap cleanup EXIT
236+
237+
run_dry() {
238+
(
239+
cd "$REPO_ROOT"
240+
HOME="$tmp_home" \
241+
XDG_CONFIG_HOME="$tmp_home/.config" \
242+
XDG_CACHE_HOME="$tmp_home/.cache" \
243+
DEVA_NO_DOCKER=1 \
244+
./deva.sh "$@"
245+
) 2>&1
246+
}
247+
248+
extract_container_name() {
249+
local output="$1"
250+
echo "$output" | grep -oE 'Container name: [^ ]+' | head -1 | sed 's/Container name: //'
251+
}
252+
253+
out="$(run_dry claude --debug --dry-run || true)"
254+
cname="$(extract_container_name "$out")"
255+
if [ -n "$cname" ]; then
256+
assert_match "dry-run: new format structure" \
257+
"^deva--claude--auth-default--" "$cname"
258+
assert_match "dry-run: ends with ..hash" \
259+
'\.\.[a-f0-9]{8}$' "$cname"
260+
assert_no_match "dry-run: no old-style ..i prefix" \
261+
'\.\.i[a-f0-9]' "$cname"
262+
else
263+
fail "dry-run: could not extract container name from output"
264+
fi
265+
266+
out="$(run_dry codex --debug --dry-run || true)"
267+
cname="$(extract_container_name "$out")"
268+
if [ -n "$cname" ]; then
269+
assert_match "dry-run codex: agent in name" \
270+
"^deva--codex--" "$cname"
271+
else
272+
fail "dry-run codex: could not extract container name"
273+
fi
274+
275+
out="$(run_dry gemini --debug --dry-run || true)"
276+
cname="$(extract_container_name "$out")"
277+
if [ -n "$cname" ]; then
278+
assert_match "dry-run gemini: agent in name" \
279+
"^deva--gemini--" "$cname"
280+
else
281+
fail "dry-run gemini: could not extract container name"
282+
fi
283+
284+
# Ephemeral mode
285+
out="$(run_dry claude --rm --debug --dry-run || true)"
286+
cname="$(extract_container_name "$out")"
287+
if [ -n "$cname" ]; then
288+
assert_match "dry-run ephemeral: has PID suffix" \
289+
'--[0-9]+$' "$cname"
290+
assert_match "dry-run ephemeral: agent in name" \
291+
"^deva--claude--" "$cname"
292+
else
293+
fail "dry-run ephemeral: could not extract container name"
294+
fi
295+
296+
# Different agents same workspace -> different container names
297+
out_claude="$(run_dry claude --debug --dry-run || true)"
298+
out_codex="$(run_dry codex --debug --dry-run || true)"
299+
cname_claude="$(extract_container_name "$out_claude")"
300+
cname_codex="$(extract_container_name "$out_codex")"
301+
if [ -n "$cname_claude" ] && [ -n "$cname_codex" ]; then
302+
if [ "$cname_claude" != "$cname_codex" ]; then
303+
pass "different agents -> different container names"
304+
else
305+
fail "different agents -> different container names: both='$cname_claude'"
306+
fi
307+
else
308+
fail "could not extract container names for agent isolation test"
309+
fi
310+
311+
# ──────────────────────────────────────
312+
echo ""
313+
echo "=== Results: $PASS passed, $FAIL failed ==="
314+
if [ "$FAIL" -gt 0 ]; then
315+
echo -e "$ERRORS"
316+
exit 1
317+
fi
318+
echo "All tests passed."

0 commit comments

Comments
 (0)