diff --git a/scripts/test-windows-clone-contract.ps1 b/scripts/test-windows-clone-contract.ps1 index 29f55929..887079c7 100644 --- a/scripts/test-windows-clone-contract.ps1 +++ b/scripts/test-windows-clone-contract.ps1 @@ -149,13 +149,36 @@ try { $remote = Join-Path $fixtureRoot "Workspace Remote/remote.git" $seed = Join-Path $fixtureRoot "Workspace Seed" - [System.IO.Directory]::CreateDirectory((Join-Path $seed "workspaces")) | Out-Null + [System.IO.Directory]::CreateDirectory($seed) | Out-Null Invoke-BoundedNative -FilePath "git" -Arguments @("init", "--bare", "--initial-branch=main", $remote) -Label "initialize Windows bare registry" | Out-Null Invoke-BoundedNative -FilePath "git" -Arguments @("-C", $seed, "init", "--initial-branch=main") -Label "initialize Windows registry seed" | Out-Null $workspaceFixture = Join-Path $spacedRepository "scripts/fixtures/workspace-registry-windows.yaml" - $workspaceDestination = Join-Path $seed "workspaces/task13-windows.yaml" + $workspaceDirectory = Join-Path $seed "task13-windows" + $workspaceDestination = Join-Path $workspaceDirectory "workspace.yaml" + $workspaceEvidence = Join-Path $workspaceDirectory "evidence" + $catalogDestination = Join-Path $seed "thoth-workspaces.yaml" + [System.IO.Directory]::CreateDirectory($workspaceDirectory) | Out-Null Copy-Item -LiteralPath $workspaceFixture -Destination $workspaceDestination - Invoke-BoundedNative -FilePath "git" -Arguments @("-C", $seed, "add", "workspaces/task13-windows.yaml") -Label "stage Windows registry seed" | Out-Null + Write-Utf8File (Join-Path $workspaceEvidence "guide.md") "guide v1`n" + Write-Utf8File $catalogDestination @" +schema_version: 1 +workspaces: + - id: task13-windows + name: Task 13 Windows +"@ + if (-not (Test-Path -LiteralPath $catalogDestination)) { + throw "the Windows root workspace catalog was not created" + } + if (-not (Test-Path -LiteralPath $workspaceDestination)) { + throw "the Windows nested workspace descriptor was not created" + } + if (-not (Test-Path -LiteralPath (Join-Path $workspaceEvidence "guide.md"))) { + throw "the Windows nested workspace evidence was not created" + } + if (Test-Path -LiteralPath (Join-Path $seed "workspaces/task13-windows.yaml")) { + throw "the Windows legacy flat descriptor path must not be used" + } + Invoke-BoundedNative -FilePath "git" -Arguments @("-C", $seed, "add", "thoth-workspaces.yaml", "task13-windows/workspace.yaml", "task13-windows/evidence/guide.md") -Label "stage Windows registry seed" | Out-Null Invoke-BoundedNative -FilePath "git" -Arguments @("-C", $seed, "-c", "user.name=Task 13 Windows", "-c", "user.email=task13-windows@example.invalid", "commit", "-m", "Seed Windows smoke") -Label "commit Windows registry seed" | Out-Null Invoke-BoundedNative -FilePath "git" -Arguments @("-C", $seed, "push", $remote, "HEAD:main") -Label "push Windows registry seed" | Out-Null diff --git a/scripts/unified-deployment-smoke.sh b/scripts/unified-deployment-smoke.sh index 02879a28..e7d794e1 100755 --- a/scripts/unified-deployment-smoke.sh +++ b/scripts/unified-deployment-smoke.sh @@ -473,22 +473,93 @@ EOF chmod 0600 "$TASK13_ENV_FILE" } -task13_seed_registry() { - mkdir -p "$TASK13_SEED/workspaces" - task13_run_logged "initialize bare workspace registry" \ - git init --bare --initial-branch=main "$TASK13_REMOTE" - task13_run_logged "initialize workspace seed" git -C "$TASK13_SEED" init --initial-branch=main - cp "$TASK13_ROOT/scripts/fixtures/workspace-registry-task13.yaml" \ - "$TASK13_SEED/workspaces/task13-smoke.yaml" - task13_run_logged "commit initial workspace" git -C "$TASK13_SEED" add workspaces/task13-smoke.yaml - task13_run_logged "commit initial workspace" git -C "$TASK13_SEED" \ - -c user.name='Task 13 Smoke' -c user.email='task13-smoke@example.invalid' \ - commit -m 'Seed Task 13 workspace' - task13_run_logged "push initial workspace" git -C "$TASK13_SEED" \ - push "$TASK13_REMOTE" "HEAD:$TASK13_BRANCH" +task13_workspace_metadata() { + local fixture="$1" + awk ' + /^workspace:[[:space:]]*$/ { in_workspace = 1; next } + in_workspace && /^[^[:space:]]/ { in_workspace = 0 } + in_workspace && /^ id:[[:space:]]*/ { + sub(/^ id:[[:space:]]*/, "") + id = $0 + next + } + in_workspace && /^ name:[[:space:]]*/ { + sub(/^ name:[[:space:]]*/, "") + name = $0 + next + } + in_workspace && /^ description:[[:space:]]*/ { + sub(/^ description:[[:space:]]*/, "") + description = $0 + next + } + END { + if (id == "" || name == "") exit 1 + print id + print name + if (description != "") print description + } + ' "$fixture" +} + +task13_write_catalog() { + local catalog_path="$1" id="$2" name="$3" description="${4:-}" + { + printf 'schema_version: 1\n' + printf 'workspaces:\n' + printf ' - id: %s\n' "$id" + printf ' name: %s\n' "$name" + if [[ -n "$description" ]]; then + printf ' description: %s\n' "$description" + fi + } >"$catalog_path" +} + +task13_replace_once() { + local target="$1" old="$2" new="$3" + python3 - "$target" "$old" "$new" <<'PY' +from pathlib import Path +import sys +path = Path(sys.argv[1]) +old = sys.argv[2] +new = sys.argv[3] +text = path.read_text() +count = text.count(old) +if count != 1: + raise SystemExit(f"expected exactly one occurrence of {old!r} in {path}, found {count}") +path.write_text(text.replace(old, new, 1)) +PY +} + +task13_commit_registry_change() { + local message="$1" + task13_run_logged "$message" git -C "$TASK13_SEED" add -A + task13_run_logged "$message" git -C "$TASK13_SEED" -c user.name='Task 13 Smoke' -c user.email='task13-smoke@example.invalid' commit -m "$message" + task13_run_logged "$message" git -C "$TASK13_SEED" push "$TASK13_REMOTE" "HEAD:$TASK13_BRANCH" chmod -R a+rX "$TASK13_REMOTE" } +task13_seed_registry() { + local fixture metadata + fixture="$TASK13_ROOT/scripts/fixtures/workspace-registry-task13.yaml" + mapfile -t metadata < <(task13_workspace_metadata "$fixture") + TASK13_WORKSPACE_ID="${metadata[0]}" + TASK13_WORKSPACE_NAME="${metadata[1]}" + TASK13_WORKSPACE_DESCRIPTION="${metadata[2]-}" + + mkdir -p "$TASK13_SEED" + task13_run_logged "initialize bare workspace registry" git init --bare --initial-branch=main "$TASK13_REMOTE" + task13_run_logged "initialize workspace seed" git -C "$TASK13_SEED" init --initial-branch=main + mkdir -p "$TASK13_SEED/$TASK13_WORKSPACE_ID" + cp "$fixture" "$TASK13_SEED/$TASK13_WORKSPACE_ID/workspace.yaml" + task13_write_catalog "$TASK13_SEED/thoth-workspaces.yaml" "$TASK13_WORKSPACE_ID" "$TASK13_WORKSPACE_NAME" "$TASK13_WORKSPACE_DESCRIPTION" + if grep -Fq 'type: filesystem' "$fixture"; then + mkdir -p "$TASK13_SEED/$TASK13_WORKSPACE_ID/evidence" + printf 'guide v1\n' >"$TASK13_SEED/$TASK13_WORKSPACE_ID/evidence/guide.md" + fi + task13_commit_registry_change 'Seed Task 13 workspace registry' +} + task13_build_thothctl() { local os arch mkdir -p "$TASK13_THOTHCTL_DIR" @@ -773,8 +844,9 @@ task13_prepare_persistence() { } task13_registry_lifecycle() { - local offline_status offline_head valid_head - printf '== Recreate offline and retain the validated registry snapshot ==\n' + local offline_status offline_head valid_head evidence_head repaired_head + printf '== Recreate offline and retain the validated registry snapshot == +' task13_write_environment /fixtures/offline.git task13_compose_logged "offline Compose recreation" up --detach --force-recreate --wait --wait-timeout 120 offline_status="$(task13_registry_status)" @@ -784,48 +856,78 @@ task13_registry_lifecycle() { task13_assert_sentinels [[ "$(task13_mount_fingerprint)" == "$TASK13_INITIAL_MOUNTS" ]] || task13_fail "offline recreation changed volume identity" - printf '== Pull a valid Git workspace update ==\n' - sed -i.bak 's/name: Task 13 Smoke/name: Task 13 Smoke Updated/' \ - "$TASK13_SEED/workspaces/task13-smoke.yaml" - rm "$TASK13_SEED/workspaces/task13-smoke.yaml.bak" - task13_run_logged "commit valid workspace update" git -C "$TASK13_SEED" add workspaces/task13-smoke.yaml - task13_run_logged "commit valid workspace update" git -C "$TASK13_SEED" \ - -c user.name='Task 13 Smoke' -c user.email='task13-smoke@example.invalid' \ - commit -m 'Update Task 13 workspace' - task13_run_logged "push valid workspace update" git -C "$TASK13_SEED" \ - push "$TASK13_REMOTE" "HEAD:$TASK13_BRANCH" - chmod -R a+rX "$TASK13_REMOTE" + printf '== Pull a valid catalog+descriptor metadata update == +' + task13_replace_once "$TASK13_SEED/$TASK13_WORKSPACE_ID/workspace.yaml" 'name: Task 13 Smoke' 'name: Task 13 Smoke Updated' + task13_replace_once "$TASK13_SEED/thoth-workspaces.yaml" 'name: Task 13 Smoke' 'name: Task 13 Smoke Updated' + task13_commit_registry_change 'Update Task 13 workspace metadata' task13_write_environment /fixtures/remote.git task13_compose_logged "online Compose recreation" up --detach --force-recreate --wait --wait-timeout 120 - task13_compose exec -T core curl --connect-timeout 3 --max-time 15 -fsS -X POST \ - http://127.0.0.1:8787/workspace-registry/pull >/dev/null - task13_compose exec -T core curl --connect-timeout 3 --max-time 10 -fsS \ - http://127.0.0.1:8787/workspaces \ - | grep -Fq 'Task 13 Smoke Updated' || task13_fail "valid Git update was not activated" + task13_compose exec -T core curl --connect-timeout 3 --max-time 15 -fsS -X POST http://127.0.0.1:8787/workspace-registry/pull >/dev/null + task13_compose exec -T core curl --connect-timeout 3 --max-time 10 -fsS http://127.0.0.1:8787/workspaces | grep -Fq 'Task 13 Smoke Updated' || task13_fail "valid Git update was not activated" valid_head="$(task13_active_registry_head)" - [[ "$valid_head" =~ ^[0-9a-f]{40}$ && "$valid_head" != "$TASK13_INITIAL_HEAD" ]] \ - || task13_fail "valid Git update did not advance the registry head" + [[ "$valid_head" =~ ^[0-9a-f]{40}$ && "$valid_head" != "$TASK13_INITIAL_HEAD" ]] || task13_fail "valid Git update did not advance the registry head" TASK13_INITIAL_HEAD="$valid_head" task13_assert_sentinels - printf '== Reject invalid Git content and retain the valid snapshot ==\n' - printf 'workspace: invalid\n' >"$TASK13_SEED/workspaces/task13-smoke.yaml" - task13_run_logged "commit invalid workspace update" git -C "$TASK13_SEED" add workspaces/task13-smoke.yaml - task13_run_logged "commit invalid workspace update" git -C "$TASK13_SEED" \ - -c user.name='Task 13 Smoke' -c user.email='task13-smoke@example.invalid' \ - commit -m 'Invalid Task 13 workspace fixture' - task13_run_logged "push invalid workspace update" git -C "$TASK13_SEED" \ - push "$TASK13_REMOTE" "HEAD:$TASK13_BRANCH" - chmod -R a+rX "$TASK13_REMOTE" - if task13_compose exec -T core curl --connect-timeout 3 --max-time 15 -fsS -X POST \ - http://127.0.0.1:8787/workspace-registry/pull >>"$TASK13_LOG" 2>&1; then - task13_fail "registry accepted invalid Git content" + printf '== Pull a content-only Git Evidence update == +' + printf 'guide v2\n' >"$TASK13_SEED/$TASK13_WORKSPACE_ID/evidence/guide.md" + task13_commit_registry_change 'Update Task 13 workspace evidence only' + task13_compose exec -T core curl --connect-timeout 3 --max-time 15 -fsS -X POST http://127.0.0.1:8787/workspace-registry/pull >/dev/null + evidence_head="$(task13_active_registry_head)" + [[ "$evidence_head" =~ ^[0-9a-f]{40}$ && "$evidence_head" != "$valid_head" ]] || task13_fail "content-only Git Evidence update did not advance the registry head" + TASK13_INITIAL_HEAD="$evidence_head" + task13_assert_sentinels + + printf '== Reject catalog/descriptor metadata mismatch and retain the valid snapshot == +' + task13_replace_once "$TASK13_SEED/thoth-workspaces.yaml" 'name: Task 13 Smoke Updated' 'name: Task 13 Smoke Drift' + task13_commit_registry_change 'Break Task 13 workspace metadata parity' + if task13_compose exec -T core curl --connect-timeout 3 --max-time 15 -fsS -X POST http://127.0.0.1:8787/workspace-registry/pull >>"$TASK13_LOG" 2>&1; then + task13_fail "registry accepted catalog/descriptor metadata mismatch" fi - [[ "$(task13_active_registry_head)" == "$TASK13_INITIAL_HEAD" ]] \ - || task13_fail "invalid Git content replaced the valid registry head" - task13_compose exec -T core curl --connect-timeout 3 --max-time 10 -fsS \ - http://127.0.0.1:8787/workspaces \ - | grep -Fq 'Task 13 Smoke Updated' || task13_fail "invalid Git content displaced the valid workspace" + [[ "$(task13_active_registry_head)" == "$TASK13_INITIAL_HEAD" ]] || task13_fail "metadata mismatch replaced the valid registry head" + task13_compose exec -T core curl --connect-timeout 3 --max-time 10 -fsS http://127.0.0.1:8787/workspaces | grep -Fq 'Task 13 Smoke Updated' || task13_fail "metadata mismatch displaced the valid workspace" + task13_replace_once "$TASK13_SEED/thoth-workspaces.yaml" 'name: Task 13 Smoke Drift' 'name: Task 13 Smoke Updated' + task13_commit_registry_change 'Restore Task 13 workspace metadata parity' + task13_compose exec -T core curl --connect-timeout 3 --max-time 15 -fsS -X POST http://127.0.0.1:8787/workspace-registry/pull >/dev/null + repaired_head="$(task13_active_registry_head)" + [[ "$repaired_head" =~ ^[0-9a-f]{40}$ && "$repaired_head" != "$TASK13_INITIAL_HEAD" ]] || task13_fail "metadata repair did not restore a fresh valid registry head" + TASK13_INITIAL_HEAD="$repaired_head" + + printf '== Reject orphan descriptor directories not listed in the catalog == +' + mkdir -p "$TASK13_SEED/orphan" + cp "$TASK13_SEED/$TASK13_WORKSPACE_ID/workspace.yaml" "$TASK13_SEED/orphan/workspace.yaml" + task13_replace_once "$TASK13_SEED/orphan/workspace.yaml" 'id: task13-smoke' 'id: orphan' + task13_replace_once "$TASK13_SEED/orphan/workspace.yaml" 'name: Task 13 Smoke Updated' 'name: Orphan Workspace' + task13_replace_once "$TASK13_SEED/orphan/workspace.yaml" 'collection: task13-smoke' 'collection: orphan' + task13_replace_once "$TASK13_SEED/orphan/workspace.yaml" 'uri: task13-smoke/evidence' 'uri: orphan/evidence' + mkdir -p "$TASK13_SEED/orphan/evidence" + printf 'orphan guide\n' >"$TASK13_SEED/orphan/evidence/guide.md" + task13_commit_registry_change 'Add orphan Task 13 workspace directory' + if task13_compose exec -T core curl --connect-timeout 3 --max-time 15 -fsS -X POST http://127.0.0.1:8787/workspace-registry/pull >>"$TASK13_LOG" 2>&1; then + task13_fail "registry accepted orphan Task 13 descriptor directory" + fi + [[ "$(task13_active_registry_head)" == "$TASK13_INITIAL_HEAD" ]] || task13_fail "orphan descriptor directory replaced the valid registry head" + task13_compose exec -T core curl --connect-timeout 3 --max-time 10 -fsS http://127.0.0.1:8787/workspaces | grep -Fq 'Task 13 Smoke Updated' || task13_fail "orphan descriptor displaced the valid workspace" + rm -rf "$TASK13_SEED/orphan" + task13_commit_registry_change 'Remove orphan Task 13 workspace directory' + task13_compose exec -T core curl --connect-timeout 3 --max-time 15 -fsS -X POST http://127.0.0.1:8787/workspace-registry/pull >/dev/null + TASK13_INITIAL_HEAD="$(task13_active_registry_head)" + + printf '== Reject the retired flat workspace layout and retain the valid snapshot == +' + mkdir -p "$TASK13_SEED/workspaces" "$TASK13_SEED/workspace-content/$TASK13_WORKSPACE_ID/evidence" + cp "$TASK13_ROOT/scripts/fixtures/workspace-registry-task13.yaml" "$TASK13_SEED/workspaces/$TASK13_WORKSPACE_ID.yaml" + printf 'legacy guide\n' >"$TASK13_SEED/workspace-content/$TASK13_WORKSPACE_ID/evidence/guide.md" + task13_commit_registry_change 'Reintroduce retired flat Task 13 workspace layout' + if task13_compose exec -T core curl --connect-timeout 3 --max-time 15 -fsS -X POST http://127.0.0.1:8787/workspace-registry/pull >>"$TASK13_LOG" 2>&1; then + task13_fail "registry accepted the retired flat Task 13 workspace layout" + fi + [[ "$(task13_active_registry_head)" == "$TASK13_INITIAL_HEAD" ]] || task13_fail "retired flat workspace layout replaced the valid registry head" + task13_compose exec -T core curl --connect-timeout 3 --max-time 10 -fsS http://127.0.0.1:8787/workspaces | grep -Fq 'Task 13 Smoke Updated' || task13_fail "retired flat workspace layout displaced the valid workspace" task13_assert_sentinels } @@ -1541,6 +1643,49 @@ task13_self_test_case() { esac } +task13_fixtures_only() { + local tmp descriptor_path catalog_path initial_head updated_head + tmp="$(mktemp -d "${TMPDIR:-/tmp}/thothii-task13-fixtures.XXXXXX")" + trap 'rm -rf "$tmp"' RETURN + TASK13_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" + TASK13_TMP="$tmp" + TASK13_REMOTE="$tmp/remote.git" + TASK13_SEED="$tmp/seed" + TASK13_BRANCH="task13-smoke" + TASK13_LOG="$tmp/task13.log" + TASK13_FAILURE_LOGGED=0 + : >"$TASK13_LOG" + task13_seed_registry + + descriptor_path="$TASK13_SEED/$TASK13_WORKSPACE_ID/workspace.yaml" + catalog_path="$TASK13_SEED/thoth-workspaces.yaml" + [[ -f "$catalog_path" ]] || task13_fail "missing Task 13 root workspace catalog" + [[ -f "$descriptor_path" ]] || task13_fail "missing Task 13 nested workspace descriptor" + [[ -f "$TASK13_SEED/$TASK13_WORKSPACE_ID/evidence/guide.md" ]] || task13_fail "missing Task 13 nested workspace evidence" + [[ ! -e "$TASK13_SEED/workspaces/$TASK13_WORKSPACE_ID.yaml" ]] || task13_fail "legacy Task 13 flat descriptor path exists" + [[ ! -e "$TASK13_SEED/workspace-content/$TASK13_WORKSPACE_ID" ]] || task13_fail "legacy Task 13 flat evidence path exists" + + initial_head="$(git -C "$TASK13_SEED" rev-parse HEAD)" + task13_replace_once "$descriptor_path" 'name: Task 13 Smoke' 'name: Task 13 Smoke Updated' + task13_replace_once "$catalog_path" 'name: Task 13 Smoke' 'name: Task 13 Smoke Updated' + task13_commit_registry_change 'Update Task 13 workspace metadata' + updated_head="$(git -C "$TASK13_SEED" rev-parse HEAD)" + [[ "$updated_head" != "$initial_head" ]] || task13_fail "Task 13 metadata update did not advance Git head" + + printf 'guide v2\n' >"$TASK13_SEED/$TASK13_WORKSPACE_ID/evidence/guide.md" + task13_commit_registry_change 'Update Task 13 workspace evidence only' + [[ "$(git -C "$TASK13_SEED" rev-parse HEAD)" != "$updated_head" ]] || task13_fail "Task 13 evidence-only commit did not advance Git head" + + mkdir -p "$TASK13_SEED/workspaces" "$TASK13_SEED/workspace-content/$TASK13_WORKSPACE_ID/evidence" + cp "$TASK13_ROOT/scripts/fixtures/workspace-registry-task13.yaml" "$TASK13_SEED/workspaces/$TASK13_WORKSPACE_ID.yaml" + printf 'legacy guide\n' >"$TASK13_SEED/workspace-content/$TASK13_WORKSPACE_ID/evidence/guide.md" + [[ -f "$TASK13_SEED/workspaces/$TASK13_WORKSPACE_ID.yaml" ]] || task13_fail "missing Task 13 legacy flat descriptor fixture" + [[ -f "$TASK13_SEED/workspace-content/$TASK13_WORKSPACE_ID/evidence/guide.md" ]] || task13_fail "missing Task 13 legacy flat evidence fixture" + + printf 'Task 13 fixture contract passed. +' +} + task13_initialize() { umask 077 TASK13_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" @@ -1639,6 +1784,8 @@ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then elif [[ "${1:-}" == "--self-test-case" ]]; then [[ -n "${2:-}" ]] || task13_fail "--self-test-case requires a case name" task13_self_test_case "$2" + elif [[ "${1:-}" == "--fixtures-only" ]]; then + task13_fixtures_only else task13_supervise "$TASK13_SMOKE_TIMEOUT" "unified deployment smoke" task13_smoke_main full fi diff --git a/scripts/workspace-registry-smoke.sh b/scripts/workspace-registry-smoke.sh index 4de7a334..26136177 100755 --- a/scripts/workspace-registry-smoke.sh +++ b/scripts/workspace-registry-smoke.sh @@ -243,6 +243,157 @@ workspace_registry_smoke_self_test_cleanup_failure_path() { echo "workspace registry smoke cleanup failure-path self-test passed" } +workspace_registry_smoke_metadata() { + local fixture="$1" + awk ' + /^workspace:[[:space:]]*$/ { in_workspace = 1; next } + in_workspace && /^[^[:space:]]/ { in_workspace = 0 } + in_workspace && /^ id:[[:space:]]*/ { + sub(/^ id:[[:space:]]*/, "") + id = $0 + next + } + in_workspace && /^ name:[[:space:]]*/ { + sub(/^ name:[[:space:]]*/, "") + name = $0 + next + } + in_workspace && /^ description:[[:space:]]*/ { + sub(/^ description:[[:space:]]*/, "") + description = $0 + next + } + END { + if (id == "" || name == "") exit 1 + print id + print name + if (description != "") print description + } + ' "$fixture" +} + +workspace_registry_smoke_write_catalog() { + local catalog_path="$1" id="$2" name="$3" description="${4:-}" + { + printf 'schema_version: 1 +' + printf 'workspaces: +' + printf ' - id: %s +' "$id" + printf ' name: %s +' "$name" + if [[ -n "$description" ]]; then + printf ' description: %s +' "$description" + fi + } >"$catalog_path" +} + +workspace_registry_smoke_replace_once() { + local target="$1" old="$2" new="$3" + python3 - "$target" "$old" "$new" <<'PY' +from pathlib import Path +import sys +path = Path(sys.argv[1]) +old = sys.argv[2] +new = sys.argv[3] +text = path.read_text() +count = text.count(old) +if count != 1: + raise SystemExit(f"expected exactly one occurrence of {old!r} in {path}, found {count}") +path.write_text(text.replace(old, new, 1)) +PY +} + +workspace_registry_smoke_seed_fixture() { + local fixture="$1" metadata + mapfile -t metadata < <(workspace_registry_smoke_metadata "$fixture") + workspace_registry_smoke_id="${metadata[0]}" + workspace_registry_smoke_name="${metadata[1]}" + workspace_registry_smoke_description="${metadata[2]-}" + + mkdir -p "$seed/$workspace_registry_smoke_id" + cp "$fixture" "$seed/$workspace_registry_smoke_id/workspace.yaml" + workspace_registry_smoke_write_catalog "$seed/thoth-workspaces.yaml" "$workspace_registry_smoke_id" "$workspace_registry_smoke_name" "$workspace_registry_smoke_description" + if grep -Fq 'type: filesystem' "$fixture"; then + mkdir -p "$seed/$workspace_registry_smoke_id/evidence" + printf 'guide v1\n' >"$seed/$workspace_registry_smoke_id/evidence/guide.md" + fi +} + +workspace_registry_smoke_commit() { + local message="$1" + git -C "$seed" add -A >/dev/null + git -C "$seed" -c user.name='Workspace Registry Smoke' -c user.email='workspace-registry-smoke@example.invalid' commit -m "$message" >/dev/null + git -C "$seed" push ${2:-origin} "HEAD:$branch" >/dev/null +} + +workspace_registry_smoke_prepare_fixture() { + git init --bare --initial-branch=main "$remote" >/dev/null + git clone "$remote" "$seed" >/dev/null + git -C "$seed" checkout -b "$branch" >/dev/null + workspace_registry_smoke_seed_fixture "$root/scripts/fixtures/workspace-registry-smoke.yaml" + workspace_registry_smoke_commit 'Seed workspace registry smoke' +} + +workspace_registry_smoke_registry_head() { + printf '%s' "$1" | sed -n 's/.*"head":"\([0-9a-f]*\)".*/\1/p' +} + +workspace_registry_smoke_reset_seed() { + local commit="$1" + git -C "$seed" reset --hard "$commit" >/dev/null + git -C "$seed" push --force origin "HEAD:$branch" >/dev/null +} + +workspace_registry_smoke_fixtures_only() { + local descriptor_path catalog_path initial_head updated_head + workspace_registry_smoke_prepare_fixture + descriptor_path="$seed/$workspace_registry_smoke_id/workspace.yaml" + catalog_path="$seed/thoth-workspaces.yaml" + + [[ -f "$catalog_path" ]] || { echo 'missing root workspace catalog' >&2; return 1; } + [[ -f "$descriptor_path" ]] || { echo 'missing nested workspace descriptor' >&2; return 1; } + [[ -f "$seed/$workspace_registry_smoke_id/evidence/guide.md" ]] || { echo 'missing nested workspace evidence' >&2; return 1; } + [[ ! -e "$seed/workspaces/$workspace_registry_smoke_id.yaml" ]] || { echo 'legacy flat descriptor path was created' >&2; return 1; } + [[ ! -e "$seed/workspace-content/$workspace_registry_smoke_id" ]] || { echo 'legacy flat evidence path was created' >&2; return 1; } + grep -Fq 'schema_version: 1' "$catalog_path" + grep -Fq 'name: Local' "$catalog_path" + grep -Fq 'description: Isolated workspace registry smoke fixture.' "$catalog_path" + + initial_head="$(git -C "$seed" rev-parse HEAD)" + workspace_registry_smoke_replace_once "$descriptor_path" 'name: Local' 'name: Local Updated' + workspace_registry_smoke_replace_once "$catalog_path" 'name: Local' 'name: Local Updated' + workspace_registry_smoke_commit 'Update workspace registry smoke metadata' + updated_head="$(git -C "$seed" rev-parse HEAD)" + [[ "$updated_head" != "$initial_head" ]] || { echo 'metadata co-commit did not advance Git head' >&2; return 1; } + grep -Fq 'name: Local Updated' "$descriptor_path" + grep -Fq 'name: Local Updated' "$catalog_path" + + printf 'guide v2\n' >"$seed/$workspace_registry_smoke_id/evidence/guide.md" + workspace_registry_smoke_commit 'Update workspace registry smoke evidence only' + [[ "$(git -C "$seed" rev-parse HEAD)" != "$updated_head" ]] || { echo 'evidence-only commit did not advance Git head' >&2; return 1; } + + mkdir -p "$seed/orphan" + cp "$descriptor_path" "$seed/orphan/workspace.yaml" + workspace_registry_smoke_replace_once "$seed/orphan/workspace.yaml" 'id: local' 'id: orphan' + workspace_registry_smoke_replace_once "$seed/orphan/workspace.yaml" 'name: Local Updated' 'name: Orphan Workspace' + workspace_registry_smoke_replace_once "$seed/orphan/workspace.yaml" 'collection: local' 'collection: orphan' + workspace_registry_smoke_replace_once "$seed/orphan/workspace.yaml" 'uri: local/evidence' 'uri: orphan/evidence' + mkdir -p "$seed/orphan/evidence" + printf 'orphan guide\n' >"$seed/orphan/evidence/guide.md" + [[ -f "$seed/orphan/workspace.yaml" ]] || { echo 'orphan descriptor was not created' >&2; return 1; } + + mkdir -p "$seed/workspaces" "$seed/workspace-content/$workspace_registry_smoke_id/evidence" + cp "$root/scripts/fixtures/workspace-registry-smoke.yaml" "$seed/workspaces/$workspace_registry_smoke_id.yaml" + printf 'legacy guide\n' >"$seed/workspace-content/$workspace_registry_smoke_id/evidence/guide.md" + [[ -f "$seed/workspaces/$workspace_registry_smoke_id.yaml" ]] || { echo 'legacy flat descriptor fixture missing' >&2; return 1; } + [[ -f "$seed/workspace-content/$workspace_registry_smoke_id/evidence/guide.md" ]] || { echo 'legacy flat evidence fixture missing' >&2; return 1; } + + echo 'workspace registry smoke fixture contract passed' +} + case "${WORKSPACE_REGISTRY_SMOKE_SELF_TEST:-}" in "") ;; image-cleanup-identity) @@ -265,6 +416,12 @@ case "${WORKSPACE_REGISTRY_SMOKE_SELF_TEST:-}" in ;; esac +if [[ "${1:-}" == "--fixtures-only" ]]; then + workspace_registry_smoke_fixtures_only + rm -rf "$tmp" + exit 0 +fi + trap cleanup EXIT trap 'exit 129' HUP trap 'exit 130' INT @@ -288,15 +445,7 @@ if [[ -n "${WORKSPACE_GIT_REMOTE:-}" ]]; then fi echo "== Seed isolated workspace registry ==" -git init --bare --initial-branch=main "$remote" >/dev/null -git clone "$remote" "$seed" >/dev/null -git -C "$seed" checkout -b "$branch" >/dev/null -mkdir -p "$seed/workspaces" -cp "$root/scripts/fixtures/workspace-registry-smoke.yaml" "$seed/workspaces/local.yaml" -git -C "$seed" add workspaces/local.yaml -git -C "$seed" -c user.name='Workspace Registry Smoke' -c user.email='workspace-registry-smoke@example.invalid' \ - commit -m 'Seed workspace registry smoke' >/dev/null -git -C "$seed" push origin "HEAD:$branch" >/dev/null +workspace_registry_smoke_prepare_fixture echo "== Build and start isolated Compose core ==" compose up -d --build @@ -310,35 +459,84 @@ core_remote="/fixtures/offline.git" compose up -d --force-recreate wait_for_core recreated_status="$(compose exec -T core curl -fsS http://127.0.0.1:8787/workspace-registry/status)" -initial_head="$(printf '%s' "$initial_status" | sed -n 's/.*"head":"\([0-9a-f]*\)".*/\1/p')" -recreated_head="$(printf '%s' "$recreated_status" | sed -n 's/.*"head":"\([0-9a-f]*\)".*/\1/p')" +initial_head="$(workspace_registry_smoke_registry_head "$initial_status")" +recreated_head="$(workspace_registry_smoke_registry_head "$recreated_status")" test -n "$initial_head" && test "$initial_head" = "$recreated_head" printf '%s' "$recreated_status" | grep -Fq '"degraded":true' compose exec -T core curl -fsS http://127.0.0.1:8787/workspaces | grep -Fq 'Local' -echo "== Pull a valid remote update ==" -sed -i.bak 's/name: Local/name: Local Updated/' "$seed/workspaces/local.yaml" -rm "$seed/workspaces/local.yaml.bak" -git -C "$seed" add workspaces/local.yaml -git -C "$seed" -c user.name='Workspace Registry Smoke' -c user.email='workspace-registry-smoke@example.invalid' \ - commit -m 'Update workspace registry smoke' >/dev/null -git -C "$seed" push origin "HEAD:$branch" >/dev/null +echo "== Pull a valid catalog+descriptor metadata update ==" +workspace_registry_smoke_replace_once "$seed/$workspace_registry_smoke_id/workspace.yaml" 'name: Local' 'name: Local Updated' +workspace_registry_smoke_replace_once "$seed/thoth-workspaces.yaml" 'name: Local' 'name: Local Updated' +workspace_registry_smoke_commit 'Update workspace registry smoke metadata' core_remote="/fixtures/remote.git" compose up -d --force-recreate wait_for_core -compose exec -T core curl -fsS -X POST http://127.0.0.1:8787/workspace-registry/pull | grep -Eq '"head":"[0-9a-f]{40}"' +valid_status="$(compose exec -T core curl -fsS -X POST http://127.0.0.1:8787/workspace-registry/pull)" +valid_head="$(workspace_registry_smoke_registry_head "$valid_status")" +[[ "$valid_head" =~ ^[0-9a-f]{40}$ && "$valid_head" != "$initial_head" ]] compose exec -T core curl -fsS http://127.0.0.1:8787/workspaces | grep -Fq 'Local Updated' +good_seed_commit="$(git -C "$seed" rev-parse HEAD)" -echo "== Reject invalid remote content and retain the last valid snapshot ==" -printf '%s\n' 'workspace: invalid' >"$seed/workspaces/local.yaml" -git -C "$seed" add workspaces/local.yaml -git -C "$seed" -c user.name='Workspace Registry Smoke' -c user.email='workspace-registry-smoke@example.invalid' \ - commit -m 'Invalid workspace registry smoke fixture' >/dev/null -git -C "$seed" push origin "HEAD:$branch" >/dev/null +echo "== Pull a content-only Evidence update ==" +printf 'guide v2\n' >"$seed/$workspace_registry_smoke_id/evidence/guide.md" +workspace_registry_smoke_commit 'Update workspace registry smoke evidence only' +evidence_status="$(compose exec -T core curl -fsS -X POST http://127.0.0.1:8787/workspace-registry/pull)" +evidence_head="$(workspace_registry_smoke_registry_head "$evidence_status")" +[[ "$evidence_head" =~ ^[0-9a-f]{40}$ && "$evidence_head" != "$valid_head" ]] +compose exec -T core curl -fsS http://127.0.0.1:8787/workspaces | grep -Fq 'Local Updated' +good_seed_commit="$(git -C "$seed" rev-parse HEAD)" + +echo "== Reject catalog/descriptor metadata mismatch and retain the last valid snapshot ==" +workspace_registry_smoke_replace_once "$seed/thoth-workspaces.yaml" 'name: Local Updated' 'name: Local Drift' +workspace_registry_smoke_commit 'Break workspace registry metadata parity' if compose exec -T core curl -fsS -X POST http://127.0.0.1:8787/workspace-registry/pull >/dev/null; then - echo "registry accepted invalid remote workspace content" >&2 + echo 'registry accepted catalog/descriptor metadata mismatch' >&2 exit 1 fi +mismatch_status="$(compose exec -T core curl -fsS http://127.0.0.1:8787/workspace-registry/status)" +[[ "$(workspace_registry_smoke_registry_head "$mismatch_status")" == "$evidence_head" ]] +compose exec -T core curl -fsS http://127.0.0.1:8787/workspaces | grep -Fq 'Local Updated' +workspace_registry_smoke_replace_once "$seed/thoth-workspaces.yaml" 'name: Local Drift' 'name: Local Updated' +workspace_registry_smoke_commit 'Restore workspace registry metadata parity' +restore_status="$(compose exec -T core curl -fsS -X POST http://127.0.0.1:8787/workspace-registry/pull)" +evidence_head="$(workspace_registry_smoke_registry_head "$restore_status")" +compose exec -T core curl -fsS http://127.0.0.1:8787/workspaces | grep -Fq 'Local Updated' + +echo "== Reject orphan descriptor directories not listed in the catalog ==" +mkdir -p "$seed/orphan" +cp "$seed/$workspace_registry_smoke_id/workspace.yaml" "$seed/orphan/workspace.yaml" +workspace_registry_smoke_replace_once "$seed/orphan/workspace.yaml" 'id: local' 'id: orphan' +workspace_registry_smoke_replace_once "$seed/orphan/workspace.yaml" 'name: Local Updated' 'name: Orphan Workspace' +workspace_registry_smoke_replace_once "$seed/orphan/workspace.yaml" 'collection: local' 'collection: orphan' +workspace_registry_smoke_replace_once "$seed/orphan/workspace.yaml" 'uri: local/evidence' 'uri: orphan/evidence' +mkdir -p "$seed/orphan/evidence" +printf 'orphan guide\n' >"$seed/orphan/evidence/guide.md" +workspace_registry_smoke_commit 'Add orphan workspace directory' +if compose exec -T core curl -fsS -X POST http://127.0.0.1:8787/workspace-registry/pull >/dev/null; then + echo 'registry accepted orphan descriptor directory' >&2 + exit 1 +fi +orphan_status="$(compose exec -T core curl -fsS http://127.0.0.1:8787/workspace-registry/status)" +[[ "$(workspace_registry_smoke_registry_head "$orphan_status")" == "$evidence_head" ]] +compose exec -T core curl -fsS http://127.0.0.1:8787/workspaces | grep -Fq 'Local Updated' +rm -rf "$seed/orphan" +workspace_registry_smoke_commit 'Remove orphan workspace directory' +restore_status="$(compose exec -T core curl -fsS -X POST http://127.0.0.1:8787/workspace-registry/pull)" +evidence_head="$(workspace_registry_smoke_registry_head "$restore_status")" +compose exec -T core curl -fsS http://127.0.0.1:8787/workspaces | grep -Fq 'Local Updated' + +echo "== Reject the retired flat workspace layout and retain the last valid snapshot ==" +mkdir -p "$seed/workspaces" "$seed/workspace-content/$workspace_registry_smoke_id/evidence" +cp "$root/scripts/fixtures/workspace-registry-smoke.yaml" "$seed/workspaces/$workspace_registry_smoke_id.yaml" +printf 'legacy guide\n' >"$seed/workspace-content/$workspace_registry_smoke_id/evidence/guide.md" +workspace_registry_smoke_commit 'Reintroduce retired flat workspace layout' +if compose exec -T core curl -fsS -X POST http://127.0.0.1:8787/workspace-registry/pull >/dev/null; then + echo 'registry accepted the retired flat workspace layout' >&2 + exit 1 +fi +legacy_status="$(compose exec -T core curl -fsS http://127.0.0.1:8787/workspace-registry/status)" +[[ "$(workspace_registry_smoke_registry_head "$legacy_status")" == "$evidence_head" ]] compose exec -T core curl -fsS http://127.0.0.1:8787/workspaces | grep -Fq 'Local Updated' echo "workspace registry smoke passed"