#!/usr/bin/env bash # Generate Task 13 fixtures and validate rendered bindings with the production workspace resolver. set -euo pipefail profile="${1:-}" [[ "$profile" == local || "$profile" == server ]] || { echo "usage: $0 local|server" >&2 exit 2 } root="$(cd "$(dirname "$0")/.." && pwd -P)" tmp_parent="${TMPDIR:-/tmp}" tmp_parent="${tmp_parent%/}" fixture="$(mktemp -d "$tmp_parent/thoth-task13-runtime-$profile.XXXXXX")" trap 'rm -rf "$fixture"' EXIT HUP INT TERM # shellcheck source=./unified-deployment-smoke.sh source "$root/scripts/unified-deployment-smoke.sh" 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" TASK13_BRANCH=main TASK13_ENV_FILE="$fixture/operator.env" TASK13_OVERRIDE="$fixture/compose.task13.yaml" TASK13_LOG="$fixture/task13.log" : >"$TASK13_LOG" TASK13_INSTALLATION="$fixture/thothii-installation.yaml" TASK13_PI_AUTH="$fixture/pi-auth.json" TASK13_SECRETS="$fixture/thothii.secrets" TASK13_AUTH_ROOT="$fixture/auth" TASK13_AUTH_RUNTIME_ROOT="$fixture/auth-runtime" TASK13_AUTH_PASSWORD_FILE="$fixture/local-auth-password" TASK13_AUTH_ADMIN=task13-admin TASK13_AUTH_PASSWORD="fixture-auth-password-$profile" TASK13_OIDC_CLIENT_SECRET="fixture-oidc-client-$profile" TASK13_AUTHENTIK_API_TOKEN="fixture-authentik-token-$profile" TASK13_FRONTEND_PORT=18080 TASK13_SESSION_RUNTIME_PASSWORD="$fixture/runtime-password" TASK13_PI_MODELS="$fixture/models.json" 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" cp "$root/scripts/fixtures/workspace-registry-task13.yaml" "$workspace" if [[ "$profile" == local ]]; then task13_write_fixture_files node - "$TASK13_PI_MODELS" <<'NODE' const fs = require("fs"); const models = JSON.parse(fs.readFileSync(process.argv[2], "utf8")); const model = models.providers?.["local-qwen"]?.models?.find( (candidate) => candidate?.id === "task13-smoke", ); if (!model || JSON.stringify(model.input) !== JSON.stringify(["text"])) { throw new Error("Task 13 provider smoke model must declare text input support"); } const expectedCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }; if (JSON.stringify(model.cost) !== JSON.stringify(expectedCost)) { throw new Error("Task 13 provider smoke model must declare complete zero-cost metadata"); } NODE task13_write_environment /fixtures/remote.git compose_files=(-f "$root/compose.yaml" -f "$root/deploy/compose.local.yaml" -f "$TASK13_OVERRIDE") else TASK13_SERVER_DATA="$fixture/Server Data" TASK13_SERVER_PI_STATE="$fixture/Server Pi State" TASK13_SERVER_REGISTRY="$fixture/Server Registry" TASK13_SERVER_WORKSPACE_CONFIG="$fixture/server-sessions.yaml" TASK13_SESSION_RUNTIME_PASSWORD="$fixture/session-runtime-password" TASK13_SESSION_MIGRATOR_PASSWORD_FILE="$fixture/session-migrator-password" TASK13_SESSION_CA="$fixture/session-ca.pem" TASK13_SESSION_PASSWORD="fixture-private-token-$profile" TASK13_SESSION_MIGRATOR_PASSWORD="fixture-migrator-token-$profile" task13_write_server_fixture_files original_task13_run_logged="$(declare -f task13_run_logged)" ownership_calls="$fixture/server-auth-ownership.calls" task13_run_logged() { printf '%s\n' "$*" >>"$ownership_calls" } task13_prepare_server_auth_roots grep -Fxq -- \ "prepare server authentication canonical root sudo -n -- install -d -o 0 -g 0 -m 0700 -- $TASK13_AUTH_ROOT" \ "$ownership_calls" || { echo "server auth fixture does not prepare a root-owned private canonical directory" >&2 exit 1 } grep -Fxq -- \ "prepare server authentication runtime root sudo -n -- install -d -o 10001 -g 10001 -m 0700 -- $TASK13_AUTH_RUNTIME_ROOT" \ "$ownership_calls" || { echo "server auth fixture does not prepare the private runtime projection directory" >&2 exit 1 } unset -f task13_run_logged eval "$original_task13_run_logged" compose_files=( -f "$root/compose.yaml" -f "$root/deploy/compose.server.yaml" -f "$root/deploy/compose.session-server.yaml.example" -f "$TASK13_OVERRIDE" -f "$root/deploy/compose.auth-runtime-projection.yaml" ) 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 maintenance_rendered="$fixture/maintenance-rendered.json" docker compose --project-name "$TASK13_PROJECT" --project-directory "$root" \ --env-file "$TASK13_ENV_FILE" "${compose_files[@]}" --profile workspace-maintenance \ config --format json >"$maintenance_rendered" node - "$maintenance_rendered" <<'NODE' const config = JSON.parse(require("fs").readFileSync(process.argv[2], "utf8")); const core = config.services?.core; const maintenance = config.services?.["workspace-maintenance"]; if (!core || !maintenance || maintenance.image !== core.image) { throw new Error("workspace-maintenance must use the isolated core image"); } NODE 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" ]] || { echo "backend dependencies are required for the Task 13 runtime fixture contract" >&2 exit 2 } "${checker[@]}" "$rendered" "$workspace" "$profile" "$TASK13_SECRETS" "$TASK13_SESSION_RUNTIME_PASSWORD" for mutation in \ wrong-service \ wrong-value \ wrong-secret-mount \ wrong-qdrant-service \ wrong-embedding-service \ external-semantic-urls; do mutated="$fixture/$mutation.json" node - "$rendered" "$mutated" "$mutation" "$profile" <<'NODE' const fs = require("fs"); const [source, destination, mutation, profile] = process.argv.slice(2); const config = JSON.parse(fs.readFileSync(source, "utf8")); if (mutation === "wrong-service") { const name = "THT_WS_TASK13_SMOKE_DWH_HOST"; config.services.frontend.environment ||= {}; config.services.frontend.environment[name] = config.services.core.environment[name]; delete config.services.core.environment[name]; } else if (mutation === "wrong-value") { config.services.core.environment.THT_WS_TASK13_SMOKE_DWH_HOST = "wrong.task13.invalid"; } else if (mutation === "wrong-secret-mount") { if (profile === "local") { const mount = config.services.core.volumes.find((item) => item.target === "/run/secrets"); mount.source = "wrong-application-secrets"; } else { config.services.core.secrets[0].target = "wrong.secrets"; } } else if (mutation === "wrong-qdrant-service") { config.services.core.environment.THT_INTERNAL_QDRANT_URL = "http://vector:6333"; } else if (mutation === "wrong-embedding-service") { config.services.core.environment.THT_INTERNAL_EMBEDDING_URL = "http://ollama:11434"; } else if (mutation === "external-semantic-urls") { config.services.core.environment.THT_INTERNAL_QDRANT_URL = "https://qdrant.example.test"; config.services.core.environment.THT_INTERNAL_EMBEDDING_URL = "https://embedding.example.test"; } fs.writeFileSync(destination, JSON.stringify(config)); NODE if "${checker[@]}" "$mutated" "$workspace" "$profile" \ "$TASK13_SECRETS" "$TASK13_SESSION_RUNTIME_PASSWORD" \ >"$fixture/$mutation.out" 2>"$fixture/$mutation.err"; then echo "runtime fixture checker accepted mutation: $mutation" >&2 exit 1 fi done for mutation in collection-reuse dimension-change; do mutated="$fixture/$mutation.yaml" node - "$root/backend/package.json" "$workspace" "$mutated" "$mutation" <<'NODE' const fs = require("fs"); const { createRequire } = require("module"); const requireFromBackend = createRequire(process.argv[2]); const yaml = requireFromBackend("yaml"); const [source, destination, mutation] = process.argv.slice(3); const workspace = yaml.parse(fs.readFileSync(source, "utf8")); if (mutation === "collection-reuse") { workspace.semantic_index.vector_store.collection = "shared-semantic"; } else if (mutation === "dimension-change") { workspace.semantic_index.vector_store.dimensions = 1536; workspace.semantic_index.embedding.dimensions = 1536; } fs.writeFileSync(destination, yaml.stringify(workspace)); NODE if "${checker[@]}" "$rendered" "$mutated" "$profile" \ "$TASK13_SECRETS" "$TASK13_SESSION_RUNTIME_PASSWORD" \ >"$fixture/$mutation.out" 2>"$fixture/$mutation.err"; then echo "runtime fixture checker accepted workspace mutation: $mutation" >&2 exit 1 fi done echo "Task 13 $profile rendered runtime fixture contract passed."