diff --git a/scripts/internal-semantic-smoke.sh b/scripts/internal-semantic-smoke.sh index 20e581d7..a50509da 100755 --- a/scripts/internal-semantic-smoke.sh +++ b/scripts/internal-semantic-smoke.sh @@ -7,6 +7,34 @@ root="$(cd "$(dirname "$0")/.." && pwd -P)" # shellcheck source=./unified-deployment-smoke.sh source "$root/scripts/unified-deployment-smoke.sh" +task13_write_offline_semantic_override() { + TASK13_OFFLINE_OVERRIDE="$TASK13_TMP/compose.task13.offline-semantic.yaml" + cat >"$TASK13_OFFLINE_OVERRIDE" <<'EOF' +networks: + thothii: + internal: true +EOF + chmod 0600 "$TASK13_OFFLINE_OVERRIDE" +} + +task13_offline_semantic_compose() { + docker compose \ + --project-name "$TASK13_PROJECT" \ + --project-directory "$TASK13_ROOT" \ + --env-file "$TASK13_ENV_FILE" \ + -f "$TASK13_ROOT/compose.yaml" \ + -f "$TASK13_ROOT/deploy/compose.local.yaml" \ + -f "$TASK13_OVERRIDE" \ + -f "$TASK13_OFFLINE_OVERRIDE" \ + "$@" +} + +task13_offline_semantic_compose_logged() { + local label="$1" + shift + task13_run_logged "$label" task13_offline_semantic_compose "$@" +} + task13_wait_internal_embedding_model() { printf '== Wait for the internal embedding model ==\n' for _attempt in $(seq 1 30); do @@ -42,9 +70,49 @@ PY task13_log_failure "internal embedding model readiness" } -task13_semantic_python_probe() { +task13_wait_internal_embedding_model_offline() { + printf '== Wait for the internal embedding model ==\n' + for _attempt in $(seq 1 30); do + if docker run --rm -i --pull never \ + --label "io.thothii.task13.run=$TASK13_RUN_ID" \ + --network "$TASK13_NETWORK" \ + --entrypoint /opt/venv/bin/python \ + "$TASK13_CORE_IMAGE" - <<'PY' >>"$TASK13_LOG" 2>&1 +import json +import urllib.request +import urllib.error + +with urllib.request.urlopen("http://embedding:11434/api/tags", timeout=10) as response: + payload = json.load(response) +models = [entry.get("name") for entry in payload.get("models", []) if isinstance(entry, dict)] +if "qwen3-embedding:0.6b" not in models: + raise SystemExit(1) +request = urllib.request.Request( + "http://embedding:11434/api/embed", + data=json.dumps({"model": "qwen3-embedding:0.6b", "input": ["warm semantic smoke"]}).encode("utf-8"), + headers={"content-type": "application/json"}, + method="POST", +) +try: + with urllib.request.urlopen(request, timeout=180) as response: + payload = json.load(response) +except urllib.error.URLError: + raise SystemExit(1) +embeddings = payload.get("embeddings") +raise SystemExit(0 if isinstance(embeddings, list) and len(embeddings) == 1 and len(embeddings[0]) == 1024 else 1) +PY + then + return 0 + fi + sleep 1 + done + task13_log_failure "offline internal embedding model readiness" +} + +task13_semantic_python_probe_with() { local mode="$1" - task13_compose exec -T core /opt/venv/bin/python - "$mode" <<'PY' + shift + "$@" "$mode" <<'PY' from __future__ import annotations import hashlib @@ -247,6 +315,20 @@ print(json.dumps({ PY } +task13_semantic_python_probe() { + local mode="$1" + task13_semantic_python_probe_with "$mode" task13_compose exec -T core /opt/venv/bin/python - +} + +task13_semantic_python_probe_offline() { + local mode="$1" + task13_semantic_python_probe_with "$mode" docker run --rm -i --pull never \ + --label "io.thothii.task13.run=$TASK13_RUN_ID" \ + --network "$TASK13_NETWORK" \ + --entrypoint /opt/venv/bin/python \ + "$TASK13_CORE_IMAGE" - +} + task13_semantic_seed_and_assert() { local output printf '== Ensure the semantic collection and seed schema/evidence/memory ==\n' @@ -257,14 +339,38 @@ task13_semantic_seed_and_assert() { grep -Fq '"memory"' <<<"$output" || task13_fail "memory semantic verification did not run" } +task13_assert_offline_semantic_isolation() { + local running + [[ "$(docker network inspect --format '{{.Internal}}' "$TASK13_NETWORK")" == true ]] \ + || task13_fail "offline semantic network still allows egress" + running="$(task13_offline_semantic_compose ps --services --status running | sort)" + [[ "$running" == $'embedding\nqdrant' ]] \ + || task13_fail "offline semantic recreation started non-semantic services" + for service in core frontend embedding-model-init; do + if docker ps -a --filter "label=com.docker.compose.project=$TASK13_PROJECT" \ + --filter "label=com.docker.compose.service=$service" --format '{{.ID}}' | grep -q .; then + task13_fail "offline semantic recreation invoked bootstrap service $service" + fi + done +} + task13_semantic_verify_persistence() { local output printf '== Restart offline and prove semantic points plus model cache persist ==\n' task13_write_environment /fixtures/offline.git - task13_compose_logged "offline semantic recreation" up --detach --force-recreate --wait --wait-timeout 120 - task13_wait_internal_embedding_model - task13_registry_status >>"$TASK13_LOG" 2>&1 || true - output="$(task13_semantic_python_probe verify)" + task13_write_offline_semantic_override + task13_remove_labeled_container "${TASK13_LLM_CONTAINER:-}" >>"$TASK13_LOG" 2>&1 \ + || task13_fail "offline semantic phase could not remove the temporary LLM fixture" + task13_compose_logged "offline semantic stop" down --remove-orphans --timeout 10 + task13_offline_semantic_compose_logged "offline semantic recreation" \ + up --detach --wait --wait-timeout 120 --pull never qdrant embedding + TASK13_NETWORK="$(docker network ls \ + --filter "label=com.docker.compose.project=$TASK13_PROJECT" \ + --filter 'label=com.docker.compose.network=thothii' --format '{{.Name}}')" + [[ -n "$TASK13_NETWORK" && "$TASK13_NETWORK" != *$'\n'* ]] || task13_fail "offline semantic network was not resolved" + task13_assert_offline_semantic_isolation + task13_wait_internal_embedding_model_offline + output="$(task13_semantic_python_probe_offline verify)" printf '%s\n' "$output" >>"$TASK13_LOG" grep -Fq '"schema"' <<<"$output" || task13_fail "schema semantic persistence did not verify" grep -Fq '"evidence"' <<<"$output" || task13_fail "evidence semantic persistence did not verify" diff --git a/scripts/unified-deployment-smoke.sh b/scripts/unified-deployment-smoke.sh index 96185b09..8c308375 100755 --- a/scripts/unified-deployment-smoke.sh +++ b/scripts/unified-deployment-smoke.sh @@ -1410,6 +1410,25 @@ task13_self_test_public_timeout_contract() { || task13_fail "direct internal semantic smoke invocation lacks an internal supervisor" } +task13_self_test_internal_semantic_offline_contract() { + local root script + root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" + script="$root/scripts/internal-semantic-smoke.sh" + grep -Fq 'task13_write_offline_semantic_override' "$script" \ + || task13_fail "internal semantic smoke lacks a dedicated offline override" + grep -Fq 'task13_offline_semantic_compose_logged "offline semantic recreation" \' "$script" \ + || task13_fail "offline semantic recreation must use the isolated offline compose wrapper" + grep -Fq 'up --detach --wait --wait-timeout 120 --pull never qdrant embedding' "$script" \ + || task13_fail "offline semantic recreation must start only qdrant and embedding with --pull never" + grep -Eq 'down --remove-orphans --timeout 10$' "$script" \ + || task13_fail "offline semantic phase must stop the stack before isolated recreation" + grep -Fq 'internal: true' "$script" \ + || task13_fail "offline semantic override must disable network egress" + if grep -Eq 'offline semantic recreation.*embedding-model-init|offline semantic recreation.*core|offline semantic recreation.*frontend' "$script"; then + task13_fail "offline semantic recreation must exclude bootstrap and non-semantic services" + fi +} + task13_self_test_windows_release_contract() { local root script workflow root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" @@ -1519,6 +1538,7 @@ task13_self_test() { task13_self_test_timeout_process_group task13_self_test_nested_timeout_process_group task13_self_test_public_timeout_contract + task13_self_test_internal_semantic_offline_contract task13_self_test_windows_release_contract task13_self_test_server_release_contract task13_self_test_server_auth_hop_contract @@ -1535,6 +1555,7 @@ task13_self_test_case() { timeout-group) task13_self_test_timeout_process_group ;; timeout-nested) task13_self_test_nested_timeout_process_group ;; timeout-public) task13_self_test_public_timeout_contract ;; + semantic-offline) task13_self_test_internal_semantic_offline_contract ;; windows) task13_self_test_windows_release_contract ;; server) task13_self_test_server_release_contract ;; server-auth) task13_self_test_server_auth_hop_contract ;;