fix: tighten internal semantic compose contracts
This commit is contained in:
@@ -52,3 +52,47 @@ Concerns:
|
||||
|
||||
- The model bootstrap waits for Ollama readiness and verifies cache state, but the first real cold-start will still take time to download `qwen3-embedding:0.6b`.
|
||||
- The GPU override requests generic Docker GPU capability only; actual GPU availability remains host/runtime dependent and intentionally stays opt-in.
|
||||
|
||||
## Fix round 1 / 5 — 2026-08-08
|
||||
|
||||
Rulings applied:
|
||||
|
||||
- Kept the Task 1 boundary intact: schema-v3 remains the only operational workspace descriptor shape.
|
||||
- Did not restore any external semantic fallback for schema-v2 live sessions.
|
||||
- Treated `PROJECT_STATE.md` as stale documentation for this point, not runtime truth.
|
||||
|
||||
Focused schema-v2 evidence:
|
||||
|
||||
- Re-ran the existing targeted registry test:
|
||||
- `cd backend && npx vitest run test/workspace-registry.test.ts -t "lists a schema v2 descriptor as migration_required and refuses to acquire it"`
|
||||
- Result: pass.
|
||||
- Evidence from that test:
|
||||
- schema-v2 descriptors list as `migration_required`
|
||||
- `acquireSessionRevision("psd-clinical")` rejects with `code: "workspace_invalid"`
|
||||
- Conclusion: schema-v2 acquisition remains blocked; no external semantic fallback was reintroduced.
|
||||
|
||||
Contract consistency fixes:
|
||||
|
||||
- Updated `harness/tests/test_local_compose_contract.py` to assert the mandatory internal semantic stack, fixed internal core semantic env, private-service topology, persistent volumes, and Ollama health/dependency contract.
|
||||
- Updated shell Compose contracts to require:
|
||||
- Ollama healthcheck on `embedding`
|
||||
- `embedding-model-init` dependency on `embedding: service_healthy`
|
||||
- Updated `scripts/unified-deployment-smoke.sh` rendered-contract helper to expect the mandatory internal semantic topology and internal semantic env names, and to reject retired external semantic bindings.
|
||||
- Updated `scripts/test-task13-runtime-fixtures.sh` to exercise `task13_assert_rendered_contract` for both local and server fixture renders.
|
||||
|
||||
Fix round 1 verification:
|
||||
|
||||
- RED before implementation:
|
||||
- `cd harness && .venv/bin/pytest tests/test_local_compose_contract.py -q` failed because `embedding` had no healthcheck.
|
||||
- `./scripts/test-default-compose.sh` failed because `embedding` had no healthcheck.
|
||||
- `./scripts/test-unified-compose.sh` failed because `embedding` had no healthcheck.
|
||||
- `./scripts/test-task13-runtime-fixtures.sh local` failed because `unified-deployment-smoke.sh` still expected `core,frontend`.
|
||||
- GREEN after implementation:
|
||||
- `./scripts/test-default-compose.sh`
|
||||
- `./scripts/test-unified-compose.sh`
|
||||
- `./scripts/test-internal-semantic-compose.sh`
|
||||
- `cd harness && .venv/bin/pytest tests/test_local_compose_contract.py -q`
|
||||
- `./scripts/test-task13-runtime-fixtures.sh local`
|
||||
- `./scripts/test-task13-runtime-fixtures.sh server`
|
||||
- `cd backend && npx vitest run test/workspace-registry.test.ts -t "lists a schema v2 descriptor as migration_required and refuses to acquire it"`
|
||||
- `docker compose --env-file deploy/env/local.env.example -f compose.yaml -f deploy/compose.local.yaml config --format json`
|
||||
|
||||
+12
-1
@@ -102,6 +102,17 @@ services:
|
||||
- "11434"
|
||||
volumes:
|
||||
- embedding-models:/root/.ollama
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD-SHELL
|
||||
- >
|
||||
/usr/bin/bash -lc "exec 3<>/dev/tcp/127.0.0.1/11434 &&
|
||||
printf 'GET /api/tags HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n' >&3 &&
|
||||
grep -q '200 OK' <&3"
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
start_period: 10s
|
||||
networks:
|
||||
- thothii
|
||||
|
||||
@@ -117,7 +128,7 @@ services:
|
||||
- ./docker/embedding-model-init.sh:/opt/thoth/embedding-model-init.sh:ro
|
||||
depends_on:
|
||||
embedding:
|
||||
condition: service_started
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- thothii
|
||||
|
||||
|
||||
@@ -8,15 +8,37 @@ def test_local_compose_uses_the_generic_external_endpoint_contract():
|
||||
compose = yaml.safe_load((root / "compose.yaml").read_text())
|
||||
local = yaml.safe_load((root / "deploy/compose.local.yaml").read_text())
|
||||
|
||||
assert set(compose["services"]) == {"core", "frontend"}
|
||||
assert set(compose["services"]) == {"core", "frontend", "qdrant", "embedding", "embedding-model-init"}
|
||||
assert local["services"]["core"]["environment"]["AUTH_MODE"] == "none"
|
||||
assert local["services"]["core"]["ports"] == ["127.0.0.1:${THOTH_CORE_HTTP_PORT:-8787}:8787"]
|
||||
assert local["services"]["frontend"]["ports"] == ["127.0.0.1:${THOTH_HTTP_PORT:-8080}:8080"]
|
||||
|
||||
environment = compose["services"]["core"]["environment"]
|
||||
for name in ("THT_DWH_REST_URL", "THT_VEC_REST_URL", "THT_OLLAMA_URL", "THT_LLM_URL"):
|
||||
for name in ("THT_DWH_REST_URL", "THT_LLM_URL"):
|
||||
assert name in environment
|
||||
assert {"settings", "pi-state", "workspace-registry", "sessions"} <= set(compose["volumes"])
|
||||
assert environment["THT_INTERNAL_QDRANT_URL"] == "http://qdrant:6333"
|
||||
assert environment["THT_INTERNAL_EMBEDDING_URL"] == "http://embedding:11434"
|
||||
assert environment["THT_INTERNAL_EMBEDDING_MODEL"] == "qwen3-embedding:0.6b"
|
||||
assert environment["THT_INTERNAL_EMBEDDING_DIMENSIONS"] == "1024"
|
||||
for name in ("THT_VEC_REST_URL", "THT_VEC_WRITE_REST_URL", "THT_OLLAMA_URL"):
|
||||
assert name not in environment
|
||||
|
||||
assert {"settings", "pi-state", "workspace-registry", "sessions", "qdrant-data", "embedding-models"} <= set(compose["volumes"])
|
||||
|
||||
qdrant = compose["services"]["qdrant"]
|
||||
assert qdrant["expose"] == ["6333"]
|
||||
assert "ports" not in qdrant
|
||||
assert "healthcheck" in qdrant
|
||||
|
||||
embedding = compose["services"]["embedding"]
|
||||
assert embedding["expose"] == ["11434"]
|
||||
assert "ports" not in embedding
|
||||
assert "healthcheck" in embedding
|
||||
|
||||
model_init = compose["services"]["embedding-model-init"]
|
||||
assert "ports" not in model_init
|
||||
assert model_init["depends_on"]["embedding"]["condition"] == "service_healthy"
|
||||
assert compose["services"]["core"]["depends_on"]["embedding-model-init"]["condition"] == "service_completed_successfully"
|
||||
|
||||
|
||||
def test_core_image_prepares_the_writable_pi_profile_before_mounting_config_files():
|
||||
|
||||
@@ -40,6 +40,8 @@ for (const service of [qdrant, embedding, modelInit]) {
|
||||
}
|
||||
if ((qdrant.expose || []).join(",") !== "6333") throw new Error("qdrant must expose only 6333");
|
||||
if ((embedding.expose || []).join(",") !== "11434") throw new Error("embedding must expose only 11434");
|
||||
if (!qdrant.healthcheck) throw new Error("qdrant must define a healthcheck");
|
||||
if (!embedding.healthcheck) throw new Error("embedding must define a healthcheck");
|
||||
if (qdrant.image !== "qdrant/qdrant:v1.18.2@sha256:75eab8c4ba42096724fdcfde8b4de0b5713d529dde32f285a1f86fdcb2c9e50c") {
|
||||
throw new Error("qdrant image must be pinned by version and digest");
|
||||
}
|
||||
@@ -72,6 +74,9 @@ if (depends.qdrant?.condition !== "service_healthy") {
|
||||
if (depends["embedding-model-init"]?.condition !== "service_completed_successfully") {
|
||||
throw new Error("core must wait for embedding-model-init success");
|
||||
}
|
||||
if (modelInit.depends_on?.embedding?.condition !== "service_healthy") {
|
||||
throw new Error("embedding-model-init must wait for embedding health");
|
||||
}
|
||||
if (JSON.stringify(embedding).includes('"devices"')) {
|
||||
throw new Error("base embedding service must stay CPU-only");
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ TASK13_ROOT="$root"
|
||||
TASK13_TMP="$fixture"
|
||||
TASK13_RUN_ID="fixture-$profile"
|
||||
TASK13_PROJECT="thothii-task13-$profile"
|
||||
TASK13_PROFILE="$profile"
|
||||
TASK13_CORE_IMAGE="task13-core-$profile:fixture"
|
||||
TASK13_FRONTEND_IMAGE="task13-frontend-$profile:fixture"
|
||||
TASK13_SECRET_VALUE="task13-runtime-secret-$profile"
|
||||
@@ -36,6 +37,7 @@ TASK13_PI_SETTINGS="$fixture/settings.json"
|
||||
TASK13_LLM_SERVER="$fixture/fake-llm.mjs"
|
||||
TASK13_LLM_CONTAINER="$TASK13_PROJECT-llm"
|
||||
TASK13_REMOTE="$fixture/remote.git"
|
||||
TASK13_CURRENT_IMAGE_OVERRIDE="$fixture/current-image.yaml"
|
||||
mkdir -p "$TASK13_REMOTE"
|
||||
|
||||
workspace="$fixture/task13-smoke.yaml"
|
||||
@@ -94,6 +96,7 @@ fi
|
||||
rendered="$fixture/rendered.json"
|
||||
docker compose --project-name "$TASK13_PROJECT" --project-directory "$root" \
|
||||
--env-file "$TASK13_ENV_FILE" "${compose_files[@]}" config --format json >"$rendered"
|
||||
task13_assert_rendered_contract
|
||||
tsx_loader="$root/backend/node_modules/tsx/dist/loader.mjs"
|
||||
checker=(node --import "$tsx_loader" "$root/scripts/task13-runtime-fixture-check.ts")
|
||||
[[ -f "$tsx_loader" ]] || {
|
||||
|
||||
@@ -75,12 +75,17 @@ for (const serviceName of ["qdrant", "embedding", "embedding-model-init"]) {
|
||||
}
|
||||
if ((config.services.qdrant.expose || []).join(",") !== "6333") throw new Error("qdrant must expose only 6333");
|
||||
if ((config.services.embedding.expose || []).join(",") !== "11434") throw new Error("embedding must expose only 11434");
|
||||
if (!config.services.qdrant.healthcheck) throw new Error("qdrant must define a healthcheck");
|
||||
if (!config.services.embedding.healthcheck) throw new Error("embedding must define a healthcheck");
|
||||
if (config.services.core.depends_on?.qdrant?.condition !== "service_healthy") {
|
||||
throw new Error("core must wait for qdrant health");
|
||||
}
|
||||
if (config.services.core.depends_on?.["embedding-model-init"]?.condition !== "service_completed_successfully") {
|
||||
throw new Error("core must wait for embedding-model-init success");
|
||||
}
|
||||
if (config.services["embedding-model-init"].depends_on?.embedding?.condition !== "service_healthy") {
|
||||
throw new Error("embedding-model-init must wait for embedding health");
|
||||
}
|
||||
if (JSON.stringify(config.services.embedding).includes('"devices"')) {
|
||||
throw new Error("base embedding service must stay CPU-only");
|
||||
}
|
||||
@@ -190,6 +195,10 @@ if (config.services.core.depends_on?.qdrant?.condition !== "service_healthy") {
|
||||
if (config.services.core.depends_on?.["embedding-model-init"]?.condition !== "service_completed_successfully") {
|
||||
throw new Error("core must wait for embedding-model-init success");
|
||||
}
|
||||
if (!config.services.embedding.healthcheck) throw new Error("embedding must define a healthcheck");
|
||||
if (config.services["embedding-model-init"].depends_on?.embedding?.condition !== "service_healthy") {
|
||||
throw new Error("embedding-model-init must wait for embedding health");
|
||||
}
|
||||
if (/docker\.sock|\/var\/run\/docker|docker[-_]?daemon/i.test(JSON.stringify(config.services))) {
|
||||
throw new Error("Compose must not mount the Docker socket or daemon");
|
||||
}
|
||||
|
||||
@@ -518,7 +518,8 @@ task13_build_thothctl() {
|
||||
task13_assert_rendered_contract() {
|
||||
local services rendered
|
||||
services="$(task13_compose config --services | sort)"
|
||||
[[ "$services" == $'core\nfrontend' ]] || task13_fail "rendered stack is not exactly core and frontend"
|
||||
[[ "$services" == $'core\nembedding\nembedding-model-init\nfrontend\nqdrant' ]] \
|
||||
|| task13_fail "rendered stack is not the mandatory internal semantic topology"
|
||||
rendered="$TASK13_TMP/rendered-compose.yaml"
|
||||
task13_compose config >"$rendered"
|
||||
if grep -Eqi 'docker\.sock|/var/run/docker' "$rendered"; then
|
||||
@@ -527,9 +528,14 @@ task13_assert_rendered_contract() {
|
||||
if grep -Fq "$TASK13_SECRET_VALUE" "$rendered"; then
|
||||
task13_fail "rendered Compose exposed the fixture secret"
|
||||
fi
|
||||
for endpoint in THT_DWH_REST_URL THT_VEC_REST_URL THT_OLLAMA_URL THT_LLM_URL; do
|
||||
for endpoint in THT_DWH_REST_URL THT_LLM_URL \
|
||||
THT_INTERNAL_QDRANT_URL THT_INTERNAL_EMBEDDING_URL \
|
||||
THT_INTERNAL_EMBEDDING_MODEL THT_INTERNAL_EMBEDDING_DIMENSIONS; do
|
||||
grep -Fq "$endpoint" "$rendered" || task13_fail "rendered Compose lacks $endpoint"
|
||||
done
|
||||
if grep -Eq 'THT_VEC_REST_URL|THT_VEC_WRITE_REST_URL|THT_OLLAMA_URL' "$rendered"; then
|
||||
task13_fail "rendered Compose still exposes retired external semantic bindings"
|
||||
fi
|
||||
}
|
||||
|
||||
task13_start_stack() {
|
||||
|
||||
Reference in New Issue
Block a user