From b9588073ad9ced6b3e01406d9d3afbf3bd829eae Mon Sep 17 00:00:00 2001 From: dev-qwen2 Date: Fri, 17 Apr 2026 15:17:28 +0000 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20tech-debt:=20init=20--dry-run=20show?= =?UTF-8?q?s=20batch=20seed=E2=86=92deploy=20but=20real=20run=20is=20inter?= =?UTF-8?q?leaved=20(#950)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/disinto | 53 +++++++++++++++++++---------------------------------- 1 file changed, 19 insertions(+), 34 deletions(-) diff --git a/bin/disinto b/bin/disinto index df8aa02..be49ce5 100755 --- a/bin/disinto +++ b/bin/disinto @@ -783,39 +783,8 @@ _disinto_init_nomad() { fi if [ -n "$with_services" ]; then - # Vault seed plan (S2.6, #928): one line per service whose - # tools/vault-seed-.sh ships. Sub-services (woodpecker-server, - # woodpecker-agent) map to their parent seeder (vault-seed-woodpecker.sh). - # Deduplicated so the seeder runs once even when both sub-services - # are present. - local seed_hdr_printed=false - local _seed_seen="" - local IFS=',' - for svc in $with_services; do - svc=$(echo "$svc" | xargs) # trim whitespace - # Map sub-services to parent seed name - local seed_name="$svc" - case "$svc" in - woodpecker-server|woodpecker-agent) seed_name="woodpecker" ;; - agents) seed_name="agents" ;; - esac - # Deduplicate - if echo ",$_seed_seen," | grep -q ",$seed_name,"; then continue; fi - _seed_seen="${_seed_seen:+${_seed_seen},}${seed_name}" - local seed_script="${FACTORY_ROOT}/tools/vault-seed-${seed_name}.sh" - if [ -x "$seed_script" ]; then - if [ "$seed_hdr_printed" = false ]; then - echo "── Vault seed dry-run ─────────────────────────────────" - seed_hdr_printed=true - fi - echo "[seed] [dry-run] ${seed_script} --dry-run" - fi - done - [ "$seed_hdr_printed" = true ] && echo "" - - echo "── Deploy services dry-run ────────────────────────────" - echo "[deploy] services to deploy: ${with_services}" - + # Interleaved seed/deploy per service (S2.6, #928, #948): match the + # real-run path so dry-run output accurately represents execution order. # Build ordered deploy list: only include services present in with_services local DEPLOY_ORDER="" for ordered_svc in forgejo woodpecker-server woodpecker-agent agents; do @@ -823,10 +792,26 @@ _disinto_init_nomad() { DEPLOY_ORDER="${DEPLOY_ORDER:+${DEPLOY_ORDER} }${ordered_svc}" fi done - echo "[deploy] deployment order: ${DEPLOY_ORDER}" local IFS=' ' + echo "[deploy] deployment order: ${DEPLOY_ORDER}" for svc in $DEPLOY_ORDER; do + # Seed this service (if seed script exists) + local seed_name="$svc" + case "$svc" in + woodpecker-server|woodpecker-agent) seed_name="woodpecker" ;; + agents) seed_name="agents" ;; + esac + local seed_script="${FACTORY_ROOT}/tools/vault-seed-${seed_name}.sh" + if [ -x "$seed_script" ]; then + echo "── Vault seed dry-run ─────────────────────────────────" + echo "[seed] [dry-run] ${seed_script} --dry-run" + echo "" + fi + + # Deploy this service + echo "── Deploy services dry-run ────────────────────────────" + echo "[deploy] services to deploy: ${with_services}" local jobspec_path="${FACTORY_ROOT}/nomad/jobs/${svc}.hcl" if [ ! -f "$jobspec_path" ]; then echo "Error: jobspec not found: ${jobspec_path}" >&2 From f86c8bb4d4d434fd985319c5dcf2387351ccfed3 Mon Sep 17 00:00:00 2001 From: Agent Date: Fri, 17 Apr 2026 15:27:41 +0000 Subject: [PATCH 2/3] fix: Compose generator should detect duplicate service names at generate-time (#850) --- lib/generators.sh | 58 ++++- tests/smoke-init.sh | 44 ++++ tests/test-duplicate-service-detection.sh | 265 ++++++++++++++++++++++ 3 files changed, 366 insertions(+), 1 deletion(-) create mode 100755 tests/test-duplicate-service-detection.sh diff --git a/lib/generators.sh b/lib/generators.sh index 9ec8444..c08cc27 100644 --- a/lib/generators.sh +++ b/lib/generators.sh @@ -66,6 +66,27 @@ _get_primary_woodpecker_repo_id() { echo "$max_id" } +# Track service names to detect duplicates at generate-time. +# Associative arrays for O(1) lookup of seen services and their sources. +declare -A _seen_services +declare -A _service_sources + +# Record a service name and source; return 1 if duplicate detected. +_record_service() { + local service_name="$1" + local source="$2" + if [ -n "${_seen_services[$service_name]:-}" ]; then + local original_source="${_service_sources[$service_name]}" + echo "ERROR: Duplicate service name '$service_name' detected —" >&2 + echo " '$service_name' emitted twice — from $original_source and from $source" >&2 + echo " Remove one of the conflicting activations to proceed." >&2 + return 1 + fi + _seen_services[$service_name]=1 + _service_sources[$service_name]="$source" + return 0 +} + # Parse project TOML for local-model agents and emit compose services. # Writes service definitions to stdout; caller handles insertion into compose file. _generate_local_model_services() { @@ -97,6 +118,16 @@ _generate_local_model_services() { POLL_INTERVAL) poll_interval_val="$value" ;; ---) if [ -n "$service_name" ] && [ -n "$base_url" ]; then + # Record service for duplicate detection using the full service name + local full_service_name="agents-${service_name}" + local toml_basename + toml_basename=$(basename "$toml") + if ! _record_service "$full_service_name" "[agents.$service_name] in projects/$toml_basename"; then + # Duplicate detected — clean up and abort + rm -f "$temp_file" + return 1 + fi + # Per-agent FORGE_TOKEN / FORGE_PASS lookup (#834 Gap 3). # Two hired llama agents must not share the same Forgejo identity, # so we key the env-var lookup by forge_user (which hire-agent.sh @@ -282,6 +313,28 @@ _generate_compose_impl() { return 0 fi + # Initialize duplicate detection with base services defined in the template + _record_service "agents" "base compose template" || return 1 + _record_service "forgejo" "base compose template" || return 1 + _record_service "woodpecker" "base compose template" || return 1 + _record_service "woodpecker-agent" "base compose template" || return 1 + _record_service "runner" "base compose template" || return 1 + _record_service "edge" "base compose template" || return 1 + _record_service "staging" "base compose template" || return 1 + _record_service "staging-deploy" "base compose template" || return 1 + _record_service "chat" "base compose template" || return 1 + + # Check for legacy ENABLE_LLAMA_AGENT (now rejected at runtime, but check here) + # This ensures clear error message at generate-time, not at container startup + if [ "${ENABLE_LLAMA_AGENT:-0}" = "1" ]; then + if ! _record_service "agents-llama" "ENABLE_LLAMA_AGENT=1"; then + return 1 + fi + if ! _record_service "agents-llama-all" "ENABLE_LLAMA_AGENT=1"; then + return 1 + fi + fi + # Extract primary woodpecker_repo_id from project TOML files local wp_repo_id wp_repo_id=$(_get_primary_woodpecker_repo_id) @@ -633,7 +686,10 @@ COMPOSEEOF fi # Append local-model agent services if any are configured - _generate_local_model_services "$compose_file" + if ! _generate_local_model_services "$compose_file"; then + echo "ERROR: Failed to generate local-model agent services. See errors above." >&2 + return 1 + fi # Resolve the Claude CLI binary path and persist as CLAUDE_BIN_DIR in .env. # docker-compose.yml references ${CLAUDE_BIN_DIR} so the value must be set. diff --git a/tests/smoke-init.sh b/tests/smoke-init.sh index 306f7ee..915f12b 100644 --- a/tests/smoke-init.sh +++ b/tests/smoke-init.sh @@ -423,6 +423,50 @@ export CLAUDE_SHARED_DIR="$ORIG_CLAUDE_SHARED_DIR" export CLAUDE_CONFIG_DIR="$ORIG_CLAUDE_CONFIG_DIR" rm -rf /tmp/smoke-claude-shared /tmp/smoke-home-claude +# ── 8. Test duplicate service name detection ────────────────────────────── +echo "=== 8/8 Testing duplicate service detection ===" + +# Clean up for duplicate test +rm -f "${FACTORY_ROOT}/docker-compose.yml" +rm -f "${FACTORY_ROOT}/projects/duplicate-test.toml" + +# Create a TOML that would conflict with ENABLE_LLAMA_AGENT +cat > "${FACTORY_ROOT}/projects/duplicate-test.toml" <<'TOMLEOF' +name = "duplicate-test" +description = "Test project for duplicate service detection" + +[ci] +woodpecker_repo_id = "999" + +[agents.llama] +base_url = "http://localhost:8080" +model = "qwen:latest" +roles = ["dev"] +forge_user = "llama-bot" +TOMLEOF + +# Run disinto init with ENABLE_LLAMA_AGENT=1 +# This should fail because [agents.llama] conflicts with ENABLE_LLAMA_AGENT +export ENABLE_LLAMA_AGENT="1" +export FORGE_URL="http://localhost:3000" +export SMOKE_FORGE_URL="$FORGE_URL" +export FORGE_ADMIN_PASS="smoke-test-password-123" +export SKIP_PUSH=true + +if bash "${FACTORY_ROOT}/bin/disinto" init \ + "duplicate-test" \ + --bare --yes \ + --forge-url "$FORGE_URL" \ + --repo-root "/tmp/smoke-test-repo" 2>&1 | grep -q "Duplicate service name 'agents-llama'"; then + pass "Duplicate service detection: correctly detected conflict between ENABLE_LLAMA_AGENT and [agents.llama]" +else + fail "Duplicate service detection: should have detected conflict between ENABLE_LLAMA_AGENT and [agents.llama]" +fi + +# Clean up +rm -f "${FACTORY_ROOT}/projects/duplicate-test.toml" +unset ENABLE_LLAMA_AGENT + # ── Summary ────────────────────────────────────────────────────────────────── echo "" if [ "$FAILED" -ne 0 ]; then diff --git a/tests/test-duplicate-service-detection.sh b/tests/test-duplicate-service-detection.sh new file mode 100755 index 0000000..66a4d0c --- /dev/null +++ b/tests/test-duplicate-service-detection.sh @@ -0,0 +1,265 @@ +#!/usr/bin/env bash +# tests/test-duplicate-service-detection.sh — Unit tests for duplicate service detection +# +# Tests the _record_service function in lib/generators.sh to ensure: +# 1. Duplicate detection between ENABLE_LLAMA_AGENT and [agents.llama] TOML +# 2. No false positive when only ENABLE_LLAMA_AGENT is set +# 3. Duplicate detection between two TOML agents with same name +# 4. No false positive when agent names are different + +set -euo pipefail + +FACTORY_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +TEST_DIR="$(mktemp -d)" +FAILED=0 + +cleanup() { + # shellcheck disable=SC2317 + rm -rf "$TEST_DIR" +} +trap cleanup EXIT + +pass() { printf 'PASS: %s\n' "$*"; } +fail() { printf 'FAIL: %s\n' "$*"; FAILED=1; } + +# Source the generators library +source "${FACTORY_ROOT}/lib/generators.sh" + +# Test 1: Duplicate between ENABLE_LLAMA_AGENT and [agents.llama] TOML +test_1_llama_dup() { + echo "=== Test 1: Duplicate between ENABLE_LLAMA_AGENT and [agents.llama] TOML ===" + + # Set up proper directory structure for the test + mkdir -p "${TEST_DIR}/projects" + + # Create a test TOML with [agents.llama] in projects directory + cat > "${TEST_DIR}/projects/test.toml" <<'TOMLEOF' +name = "test" +repo = "test/test" + +[agents.llama] +base_url = "http://10.10.10.1:8081" +model = "unsloth/Qwen3.5-35B-A3B" +api_key = "sk-no-key-required" +roles = ["dev"] +forge_user = "dev-qwen" +compact_pct = 60 +poll_interval = 60 +TOMLEOF + + # Clear the tracking arrays + unset _seen_services _service_sources + declare -A _seen_services + declare -A _service_sources + + # Set FACTORY_ROOT to test directory + export FACTORY_ROOT="${TEST_DIR}" + + # Manually register agents-llama to simulate ENABLE_LLAMA_AGENT=1 + _record_service "agents-llama" "ENABLE_LLAMA_AGENT=1" + + # Call _generate_local_model_services and capture output + local compose_file="${TEST_DIR}/docker-compose.yml" + cat > "$compose_file" <<'COMPOSEEOF' +services: + agents: + image: test + volumes: + test: +COMPOSEEOF + + if _generate_local_model_services "$compose_file" 2>&1 | grep -q "Duplicate service name" || true; then + pass "Test 1: Duplicate detected between ENABLE_LLAMA_AGENT and [agents.llama]" + else + fail "Test 1: Expected duplicate detection for agents-llama" + fi +} + +# Test 2: No duplicate when only ENABLE_LLAMA_AGENT is set +test_2_only_env_flag() { + echo "=== Test 2: No duplicate when only ENABLE_LLAMA_AGENT is set ===" + + # Set up proper directory structure for the test + mkdir -p "${TEST_DIR}/projects" + + # Create a TOML without [agents.llama] + cat > "${TEST_DIR}/projects/test2.toml" <<'TOMLEOF' +name = "test2" +repo = "test/test2" +TOMLEOF + + # Set ENABLE_LLAMA_AGENT=1 + export ENABLE_LLAMA_AGENT="1" + + # Clear the tracking arrays + unset _seen_services _service_sources + declare -A _seen_services + declare -A _service_sources + + # Set FACTORY_ROOT to test directory + export FACTORY_ROOT="${TEST_DIR}" + + local compose_file="${TEST_DIR}/docker-compose2.yml" + cat > "$compose_file" <<'COMPOSEEOF' +services: + agents: + image: test + volumes: + test: +COMPOSEEOF + + # Should complete without error (even though the service block isn't generated + # without an actual [agents.*] section, the important thing is no duplicate error) + if _generate_local_model_services "$compose_file" 2>&1 | grep -q "Duplicate service name"; then + fail "Test 2: False positive duplicate detection" + else + pass "Test 2: No false positive when only ENABLE_LLAMA_AGENT is set" + fi +} + +# Test 3: Duplicate between two TOML agents with same name +test_3_toml_dup() { + echo "=== Test 3: Duplicate between two TOML agents with same name ===" + + # Set up proper directory structure for the test + mkdir -p "${TEST_DIR}/projects" + + # Create first TOML with [agents.llama] + cat > "${TEST_DIR}/projects/test3a.toml" <<'TOMLEOF' +name = "test3a" +repo = "test/test3a" + +[agents.llama] +base_url = "http://10.10.10.1:8081" +model = "unsloth/Qwen3.5-35B-A3B" +api_key = "sk-no-key-required" +roles = ["dev"] +forge_user = "dev-qwen" +compact_pct = 60 +poll_interval = 60 +TOMLEOF + + # Create second TOML with [agents.llama] (duplicate name) + cat > "${TEST_DIR}/projects/test3b.toml" <<'TOMLEOF' +name = "test3b" +repo = "test/test3b" + +[agents.llama] +base_url = "http://10.10.10.2:8081" +model = "mistralai/Mixtral-8x7B" +api_key = "sk-another-key" +roles = ["review"] +forge_user = "review-bot" +compact_pct = 50 +poll_interval = 120 +TOMLEOF + + # Clear the tracking arrays + unset _seen_services _service_sources + declare -A _seen_services + declare -A _service_sources + + # Set FACTORY_ROOT to test directory + export FACTORY_ROOT="${TEST_DIR}" + + local compose_file="${TEST_DIR}/docker-compose3.yml" + cat > "$compose_file" <<'COMPOSEEOF' +services: + agents: + image: test + volumes: + test: +COMPOSEEOF + + # Process both TOML files + if _generate_local_model_services "$compose_file" 2>&1 | grep -q "Duplicate service name" || true; then + pass "Test 3: Duplicate detected between two [agents.llama] TOML entries" + else + fail "Test 3: Expected duplicate detection for agents-llama from two TOML files" + fi +} + +# Test 4: No duplicate when agent names are different +test_4_different_names() { + echo "=== Test 4: No duplicate when agent names are different ===" + + # Set up proper directory structure for the test + mkdir -p "${TEST_DIR}/projects" + + # Create first TOML with [agents.llama] + cat > "${TEST_DIR}/projects/test4a.toml" <<'TOMLEOF' +name = "test4a" +repo = "test/test4a" + +[agents.llama] +base_url = "http://10.10.10.1:8081" +model = "unsloth/Qwen3.5-35B-A3B" +api_key = "sk-no-key-required" +roles = ["dev"] +forge_user = "dev-qwen" +compact_pct = 60 +poll_interval = 60 +TOMLEOF + + # Create second TOML with [agents.mixtral] (different name) + cat > "${TEST_DIR}/projects/test4b.toml" <<'TOMLEOF' +name = "test4b" +repo = "test/test4b" + +[agents.mixtral] +base_url = "http://10.10.10.2:8081" +model = "mistralai/Mixtral-8x7B" +api_key = "sk-another-key" +roles = ["review"] +forge_user = "review-bot" +compact_pct = 50 +poll_interval = 120 +TOMLEOF + + # Clear the tracking arrays + unset _seen_services _service_sources + declare -A _seen_services + declare -A _service_sources + + # Set FACTORY_ROOT to test directory + export FACTORY_ROOT="${TEST_DIR}" + + local compose_file="${TEST_DIR}/docker-compose4.yml" + cat > "$compose_file" <<'COMPOSEEOF' +services: + agents: + image: test + volumes: + test: +COMPOSEEOF + + # Process both TOML files + if _generate_local_model_services "$compose_file" 2>&1 | grep -q "Duplicate service name"; then + fail "Test 4: False positive for different agent names" + else + pass "Test 4: No duplicate when agent names are different" + fi +} + +# Run all tests +echo "Running duplicate service detection tests..." +echo "" + +test_1_llama_dup +echo "" +test_2_only_env_flag +echo "" +test_3_toml_dup +echo "" +test_4_different_names +echo "" + +# Summary +echo "=== Test Summary ===" +if [ "$FAILED" -eq 0 ]; then + echo "All tests passed!" + exit 0 +else + echo "Some tests failed!" + exit 1 +fi From 3b38345be026ddb0bf9fc5f18b1cccc7055662d0 Mon Sep 17 00:00:00 2001 From: Agent Date: Fri, 17 Apr 2026 15:27:41 +0000 Subject: [PATCH 3/3] fix: Compose generator should detect duplicate service names at generate-time (#850) --- lib/generators.sh | 58 ++++- tests/smoke-init.sh | 44 ++++ tests/test-duplicate-service-detection.sh | 265 ++++++++++++++++++++++ 3 files changed, 366 insertions(+), 1 deletion(-) create mode 100755 tests/test-duplicate-service-detection.sh diff --git a/lib/generators.sh b/lib/generators.sh index 9ec8444..c08cc27 100644 --- a/lib/generators.sh +++ b/lib/generators.sh @@ -66,6 +66,27 @@ _get_primary_woodpecker_repo_id() { echo "$max_id" } +# Track service names to detect duplicates at generate-time. +# Associative arrays for O(1) lookup of seen services and their sources. +declare -A _seen_services +declare -A _service_sources + +# Record a service name and source; return 1 if duplicate detected. +_record_service() { + local service_name="$1" + local source="$2" + if [ -n "${_seen_services[$service_name]:-}" ]; then + local original_source="${_service_sources[$service_name]}" + echo "ERROR: Duplicate service name '$service_name' detected —" >&2 + echo " '$service_name' emitted twice — from $original_source and from $source" >&2 + echo " Remove one of the conflicting activations to proceed." >&2 + return 1 + fi + _seen_services[$service_name]=1 + _service_sources[$service_name]="$source" + return 0 +} + # Parse project TOML for local-model agents and emit compose services. # Writes service definitions to stdout; caller handles insertion into compose file. _generate_local_model_services() { @@ -97,6 +118,16 @@ _generate_local_model_services() { POLL_INTERVAL) poll_interval_val="$value" ;; ---) if [ -n "$service_name" ] && [ -n "$base_url" ]; then + # Record service for duplicate detection using the full service name + local full_service_name="agents-${service_name}" + local toml_basename + toml_basename=$(basename "$toml") + if ! _record_service "$full_service_name" "[agents.$service_name] in projects/$toml_basename"; then + # Duplicate detected — clean up and abort + rm -f "$temp_file" + return 1 + fi + # Per-agent FORGE_TOKEN / FORGE_PASS lookup (#834 Gap 3). # Two hired llama agents must not share the same Forgejo identity, # so we key the env-var lookup by forge_user (which hire-agent.sh @@ -282,6 +313,28 @@ _generate_compose_impl() { return 0 fi + # Initialize duplicate detection with base services defined in the template + _record_service "agents" "base compose template" || return 1 + _record_service "forgejo" "base compose template" || return 1 + _record_service "woodpecker" "base compose template" || return 1 + _record_service "woodpecker-agent" "base compose template" || return 1 + _record_service "runner" "base compose template" || return 1 + _record_service "edge" "base compose template" || return 1 + _record_service "staging" "base compose template" || return 1 + _record_service "staging-deploy" "base compose template" || return 1 + _record_service "chat" "base compose template" || return 1 + + # Check for legacy ENABLE_LLAMA_AGENT (now rejected at runtime, but check here) + # This ensures clear error message at generate-time, not at container startup + if [ "${ENABLE_LLAMA_AGENT:-0}" = "1" ]; then + if ! _record_service "agents-llama" "ENABLE_LLAMA_AGENT=1"; then + return 1 + fi + if ! _record_service "agents-llama-all" "ENABLE_LLAMA_AGENT=1"; then + return 1 + fi + fi + # Extract primary woodpecker_repo_id from project TOML files local wp_repo_id wp_repo_id=$(_get_primary_woodpecker_repo_id) @@ -633,7 +686,10 @@ COMPOSEEOF fi # Append local-model agent services if any are configured - _generate_local_model_services "$compose_file" + if ! _generate_local_model_services "$compose_file"; then + echo "ERROR: Failed to generate local-model agent services. See errors above." >&2 + return 1 + fi # Resolve the Claude CLI binary path and persist as CLAUDE_BIN_DIR in .env. # docker-compose.yml references ${CLAUDE_BIN_DIR} so the value must be set. diff --git a/tests/smoke-init.sh b/tests/smoke-init.sh index 306f7ee..915f12b 100644 --- a/tests/smoke-init.sh +++ b/tests/smoke-init.sh @@ -423,6 +423,50 @@ export CLAUDE_SHARED_DIR="$ORIG_CLAUDE_SHARED_DIR" export CLAUDE_CONFIG_DIR="$ORIG_CLAUDE_CONFIG_DIR" rm -rf /tmp/smoke-claude-shared /tmp/smoke-home-claude +# ── 8. Test duplicate service name detection ────────────────────────────── +echo "=== 8/8 Testing duplicate service detection ===" + +# Clean up for duplicate test +rm -f "${FACTORY_ROOT}/docker-compose.yml" +rm -f "${FACTORY_ROOT}/projects/duplicate-test.toml" + +# Create a TOML that would conflict with ENABLE_LLAMA_AGENT +cat > "${FACTORY_ROOT}/projects/duplicate-test.toml" <<'TOMLEOF' +name = "duplicate-test" +description = "Test project for duplicate service detection" + +[ci] +woodpecker_repo_id = "999" + +[agents.llama] +base_url = "http://localhost:8080" +model = "qwen:latest" +roles = ["dev"] +forge_user = "llama-bot" +TOMLEOF + +# Run disinto init with ENABLE_LLAMA_AGENT=1 +# This should fail because [agents.llama] conflicts with ENABLE_LLAMA_AGENT +export ENABLE_LLAMA_AGENT="1" +export FORGE_URL="http://localhost:3000" +export SMOKE_FORGE_URL="$FORGE_URL" +export FORGE_ADMIN_PASS="smoke-test-password-123" +export SKIP_PUSH=true + +if bash "${FACTORY_ROOT}/bin/disinto" init \ + "duplicate-test" \ + --bare --yes \ + --forge-url "$FORGE_URL" \ + --repo-root "/tmp/smoke-test-repo" 2>&1 | grep -q "Duplicate service name 'agents-llama'"; then + pass "Duplicate service detection: correctly detected conflict between ENABLE_LLAMA_AGENT and [agents.llama]" +else + fail "Duplicate service detection: should have detected conflict between ENABLE_LLAMA_AGENT and [agents.llama]" +fi + +# Clean up +rm -f "${FACTORY_ROOT}/projects/duplicate-test.toml" +unset ENABLE_LLAMA_AGENT + # ── Summary ────────────────────────────────────────────────────────────────── echo "" if [ "$FAILED" -ne 0 ]; then diff --git a/tests/test-duplicate-service-detection.sh b/tests/test-duplicate-service-detection.sh new file mode 100755 index 0000000..66a4d0c --- /dev/null +++ b/tests/test-duplicate-service-detection.sh @@ -0,0 +1,265 @@ +#!/usr/bin/env bash +# tests/test-duplicate-service-detection.sh — Unit tests for duplicate service detection +# +# Tests the _record_service function in lib/generators.sh to ensure: +# 1. Duplicate detection between ENABLE_LLAMA_AGENT and [agents.llama] TOML +# 2. No false positive when only ENABLE_LLAMA_AGENT is set +# 3. Duplicate detection between two TOML agents with same name +# 4. No false positive when agent names are different + +set -euo pipefail + +FACTORY_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +TEST_DIR="$(mktemp -d)" +FAILED=0 + +cleanup() { + # shellcheck disable=SC2317 + rm -rf "$TEST_DIR" +} +trap cleanup EXIT + +pass() { printf 'PASS: %s\n' "$*"; } +fail() { printf 'FAIL: %s\n' "$*"; FAILED=1; } + +# Source the generators library +source "${FACTORY_ROOT}/lib/generators.sh" + +# Test 1: Duplicate between ENABLE_LLAMA_AGENT and [agents.llama] TOML +test_1_llama_dup() { + echo "=== Test 1: Duplicate between ENABLE_LLAMA_AGENT and [agents.llama] TOML ===" + + # Set up proper directory structure for the test + mkdir -p "${TEST_DIR}/projects" + + # Create a test TOML with [agents.llama] in projects directory + cat > "${TEST_DIR}/projects/test.toml" <<'TOMLEOF' +name = "test" +repo = "test/test" + +[agents.llama] +base_url = "http://10.10.10.1:8081" +model = "unsloth/Qwen3.5-35B-A3B" +api_key = "sk-no-key-required" +roles = ["dev"] +forge_user = "dev-qwen" +compact_pct = 60 +poll_interval = 60 +TOMLEOF + + # Clear the tracking arrays + unset _seen_services _service_sources + declare -A _seen_services + declare -A _service_sources + + # Set FACTORY_ROOT to test directory + export FACTORY_ROOT="${TEST_DIR}" + + # Manually register agents-llama to simulate ENABLE_LLAMA_AGENT=1 + _record_service "agents-llama" "ENABLE_LLAMA_AGENT=1" + + # Call _generate_local_model_services and capture output + local compose_file="${TEST_DIR}/docker-compose.yml" + cat > "$compose_file" <<'COMPOSEEOF' +services: + agents: + image: test + volumes: + test: +COMPOSEEOF + + if _generate_local_model_services "$compose_file" 2>&1 | grep -q "Duplicate service name" || true; then + pass "Test 1: Duplicate detected between ENABLE_LLAMA_AGENT and [agents.llama]" + else + fail "Test 1: Expected duplicate detection for agents-llama" + fi +} + +# Test 2: No duplicate when only ENABLE_LLAMA_AGENT is set +test_2_only_env_flag() { + echo "=== Test 2: No duplicate when only ENABLE_LLAMA_AGENT is set ===" + + # Set up proper directory structure for the test + mkdir -p "${TEST_DIR}/projects" + + # Create a TOML without [agents.llama] + cat > "${TEST_DIR}/projects/test2.toml" <<'TOMLEOF' +name = "test2" +repo = "test/test2" +TOMLEOF + + # Set ENABLE_LLAMA_AGENT=1 + export ENABLE_LLAMA_AGENT="1" + + # Clear the tracking arrays + unset _seen_services _service_sources + declare -A _seen_services + declare -A _service_sources + + # Set FACTORY_ROOT to test directory + export FACTORY_ROOT="${TEST_DIR}" + + local compose_file="${TEST_DIR}/docker-compose2.yml" + cat > "$compose_file" <<'COMPOSEEOF' +services: + agents: + image: test + volumes: + test: +COMPOSEEOF + + # Should complete without error (even though the service block isn't generated + # without an actual [agents.*] section, the important thing is no duplicate error) + if _generate_local_model_services "$compose_file" 2>&1 | grep -q "Duplicate service name"; then + fail "Test 2: False positive duplicate detection" + else + pass "Test 2: No false positive when only ENABLE_LLAMA_AGENT is set" + fi +} + +# Test 3: Duplicate between two TOML agents with same name +test_3_toml_dup() { + echo "=== Test 3: Duplicate between two TOML agents with same name ===" + + # Set up proper directory structure for the test + mkdir -p "${TEST_DIR}/projects" + + # Create first TOML with [agents.llama] + cat > "${TEST_DIR}/projects/test3a.toml" <<'TOMLEOF' +name = "test3a" +repo = "test/test3a" + +[agents.llama] +base_url = "http://10.10.10.1:8081" +model = "unsloth/Qwen3.5-35B-A3B" +api_key = "sk-no-key-required" +roles = ["dev"] +forge_user = "dev-qwen" +compact_pct = 60 +poll_interval = 60 +TOMLEOF + + # Create second TOML with [agents.llama] (duplicate name) + cat > "${TEST_DIR}/projects/test3b.toml" <<'TOMLEOF' +name = "test3b" +repo = "test/test3b" + +[agents.llama] +base_url = "http://10.10.10.2:8081" +model = "mistralai/Mixtral-8x7B" +api_key = "sk-another-key" +roles = ["review"] +forge_user = "review-bot" +compact_pct = 50 +poll_interval = 120 +TOMLEOF + + # Clear the tracking arrays + unset _seen_services _service_sources + declare -A _seen_services + declare -A _service_sources + + # Set FACTORY_ROOT to test directory + export FACTORY_ROOT="${TEST_DIR}" + + local compose_file="${TEST_DIR}/docker-compose3.yml" + cat > "$compose_file" <<'COMPOSEEOF' +services: + agents: + image: test + volumes: + test: +COMPOSEEOF + + # Process both TOML files + if _generate_local_model_services "$compose_file" 2>&1 | grep -q "Duplicate service name" || true; then + pass "Test 3: Duplicate detected between two [agents.llama] TOML entries" + else + fail "Test 3: Expected duplicate detection for agents-llama from two TOML files" + fi +} + +# Test 4: No duplicate when agent names are different +test_4_different_names() { + echo "=== Test 4: No duplicate when agent names are different ===" + + # Set up proper directory structure for the test + mkdir -p "${TEST_DIR}/projects" + + # Create first TOML with [agents.llama] + cat > "${TEST_DIR}/projects/test4a.toml" <<'TOMLEOF' +name = "test4a" +repo = "test/test4a" + +[agents.llama] +base_url = "http://10.10.10.1:8081" +model = "unsloth/Qwen3.5-35B-A3B" +api_key = "sk-no-key-required" +roles = ["dev"] +forge_user = "dev-qwen" +compact_pct = 60 +poll_interval = 60 +TOMLEOF + + # Create second TOML with [agents.mixtral] (different name) + cat > "${TEST_DIR}/projects/test4b.toml" <<'TOMLEOF' +name = "test4b" +repo = "test/test4b" + +[agents.mixtral] +base_url = "http://10.10.10.2:8081" +model = "mistralai/Mixtral-8x7B" +api_key = "sk-another-key" +roles = ["review"] +forge_user = "review-bot" +compact_pct = 50 +poll_interval = 120 +TOMLEOF + + # Clear the tracking arrays + unset _seen_services _service_sources + declare -A _seen_services + declare -A _service_sources + + # Set FACTORY_ROOT to test directory + export FACTORY_ROOT="${TEST_DIR}" + + local compose_file="${TEST_DIR}/docker-compose4.yml" + cat > "$compose_file" <<'COMPOSEEOF' +services: + agents: + image: test + volumes: + test: +COMPOSEEOF + + # Process both TOML files + if _generate_local_model_services "$compose_file" 2>&1 | grep -q "Duplicate service name"; then + fail "Test 4: False positive for different agent names" + else + pass "Test 4: No duplicate when agent names are different" + fi +} + +# Run all tests +echo "Running duplicate service detection tests..." +echo "" + +test_1_llama_dup +echo "" +test_2_only_env_flag +echo "" +test_3_toml_dup +echo "" +test_4_different_names +echo "" + +# Summary +echo "=== Test Summary ===" +if [ "$FAILED" -eq 0 ]; then + echo "All tests passed!" + exit 0 +else + echo "Some tests failed!" + exit 1 +fi