refactor(cli): rename operator command to tht

This commit is contained in:
2026-08-15 21:56:40 +02:00
parent 460caa550c
commit aa8a2e9278
49 changed files with 303 additions and 261 deletions
+4 -4
View File
@@ -85,8 +85,8 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Run unified deployment smoke - name: Run unified deployment smoke
run: timeout --signal=TERM --kill-after=45s 32m bash scripts/unified-deployment-smoke.sh run: timeout --signal=TERM --kill-after=45s 32m bash scripts/unified-deployment-smoke.sh
- name: Run thothctl update smoke - name: Run tht update smoke
run: timeout --signal=TERM --kill-after=45s 32m bash scripts/thothctl-update-smoke.sh run: timeout --signal=TERM --kill-after=45s 32m bash scripts/tht-update-smoke.sh
- name: Run Linux server deployment smoke - name: Run Linux server deployment smoke
run: timeout --signal=TERM --kill-after=45s 32m bash scripts/server-deployment-smoke.sh run: timeout --signal=TERM --kill-after=45s 32m bash scripts/server-deployment-smoke.sh
@@ -114,7 +114,7 @@ jobs:
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version: "1.26.5" go-version: "1.26.5"
cache-dependency-path: tools/thothctl/go.sum cache-dependency-path: tools/tht/go.sum
- name: Verify Windows clone contract - name: Verify Windows clone contract
shell: pwsh shell: pwsh
run: ./scripts/test-windows-clone-contract.ps1 run: ./scripts/test-windows-clone-contract.ps1
@@ -133,7 +133,7 @@ jobs:
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version: "1.26.5" go-version: "1.26.5"
cache-dependency-path: tools/thothctl/go.sum cache-dependency-path: tools/tht/go.sum
- name: Run spaced-path Windows Docker release gate - name: Run spaced-path Windows Docker release gate
shell: pwsh shell: pwsh
run: ./scripts/test-windows-clone-contract.ps1 -DockerStartup run: ./scripts/test-windows-clone-contract.ps1 -DockerStartup
+1 -1
View File
@@ -8,7 +8,7 @@ Thoth/
# === Visual companion brainstorming artifacts (local-only) === # === Visual companion brainstorming artifacts (local-only) ===
.superpowers/ .superpowers/
.worktrees/ .worktrees/
.thothctl/ .tht/
# === Python === # === Python ===
__pycache__/ __pycache__/
@@ -1,16 +1,16 @@
FROM golang:1.26.5-bookworm@sha256:1ecb7edf62a0408027bd5729dfd6b1b8766e578e8df93995b225dfd0944eb651 AS build FROM golang:1.26.5-bookworm@sha256:1ecb7edf62a0408027bd5729dfd6b1b8766e578e8df93995b225dfd0944eb651 AS build
WORKDIR /src/tools/thothctl WORKDIR /src/tools/tht
COPY tools/thothctl/go.mod tools/thothctl/go.sum ./ COPY tools/tht/go.mod tools/tht/go.sum ./
RUN go mod download RUN go mod download
COPY tools/thothctl ./ COPY tools/tht ./
RUN mkdir -p /out \ RUN mkdir -p /out \
&& CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -ldflags='-s -w' -o /out/thothctl-windows-amd64.exe ./cmd/thothctl \ && CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -ldflags='-s -w' -o /out/tht-windows-amd64.exe ./cmd/tht \
&& CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -trimpath -ldflags='-s -w' -o /out/thothctl-darwin-amd64 ./cmd/thothctl \ && CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -trimpath -ldflags='-s -w' -o /out/tht-darwin-amd64 ./cmd/tht \
&& CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -trimpath -ldflags='-s -w' -o /out/thothctl-darwin-arm64 ./cmd/thothctl \ && CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -trimpath -ldflags='-s -w' -o /out/tht-darwin-arm64 ./cmd/tht \
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags='-s -w' -o /out/thothctl-linux-amd64 ./cmd/thothctl \ && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags='-s -w' -o /out/tht-linux-amd64 ./cmd/tht \
&& CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath -ldflags='-s -w' -o /out/thothctl-linux-arm64 ./cmd/thothctl && CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath -ldflags='-s -w' -o /out/tht-linux-arm64 ./cmd/tht
FROM scratch AS export FROM scratch AS export
COPY --from=build /out/ / COPY --from=build /out/ /
@@ -3,14 +3,14 @@ set -euo pipefail
export DOCKER_BUILDKIT=1 export DOCKER_BUILDKIT=1
repository_root=$(cd "$(dirname "$0")/.." && pwd) repository_root=$(cd "$(dirname "$0")/.." && pwd)
output_directory="${THT_THOTHCTL_OUTPUT_DIRECTORY:-$repository_root/dist/thothctl}" output_directory="${THT_THT_OUTPUT_DIRECTORY:-$repository_root/dist/tht}"
if [[ "$output_directory" != /* || "$output_directory" == / || "$output_directory" == */ || if [[ "$output_directory" != /* || "$output_directory" == / || "$output_directory" == */ ||
"$output_directory" == *//* || "/$output_directory/" == */../* || "$output_directory" == *//* || "/$output_directory/" == */../* ||
"/$output_directory/" == */./* ]]; then "/$output_directory/" == */./* ]]; then
echo "THT_THOTHCTL_OUTPUT_DIRECTORY must be an absolute canonical path" >&2 echo "THT_THT_OUTPUT_DIRECTORY must be an absolute canonical path" >&2
exit 2 exit 2
fi fi
mkdir -p "$output_directory" mkdir -p "$output_directory"
docker build --file "$repository_root/docker/thothctl.Dockerfile" --output "type=local,dest=$output_directory" "$repository_root" docker build --file "$repository_root/docker/tht.Dockerfile" --output "type=local,dest=$output_directory" "$repository_root"
+13 -13
View File
@@ -22,7 +22,7 @@ install -d -o 10001 -g 20002 -m 2750 /srv/thothii/secrets
install -d -o 10001 -g 10001 -m 0750 /srv/thothii/data /srv/thothii/pi-state /srv/thothii/workspace-registry install -d -o 10001 -g 10001 -m 0750 /srv/thothii/data /srv/thothii/pi-state /srv/thothii/workspace-registry
install -d -o 10001 -g 10001 -m 0700 /srv/thothii/data/workspace-secrets install -d -o 10001 -g 10001 -m 0700 /srv/thothii/data/workspace-secrets
install -d -o 10001 -g 20002 -m 2750 /srv/thothii/source/ThothII /srv/thothii/source/ThothII/scripts install -d -o 10001 -g 20002 -m 2750 /srv/thothii/source/ThothII /srv/thothii/source/ThothII/scripts
install -o 10001 -g 20002 -m 0750 /repository/scripts/build-thothctl.sh /srv/thothii/source/ThothII/scripts/build-thothctl.sh install -o 10001 -g 20002 -m 0750 /repository/scripts/build-tht.sh /srv/thothii/source/ThothII/scripts/build-tht.sh
install -o 10001 -g 20002 -m 0750 /repository/scripts/prepare-server-pi-state.sh /srv/thothii/source/ThothII/scripts/prepare-server-pi-state.sh install -o 10001 -g 20002 -m 0750 /repository/scripts/prepare-server-pi-state.sh /srv/thothii/source/ThothII/scripts/prepare-server-pi-state.sh
/srv/thothii/source/ThothII/scripts/prepare-server-pi-state.sh /srv/thothii/pi-state 10001 10001 /srv/thothii/source/ThothII/scripts/prepare-server-pi-state.sh /srv/thothii/pi-state 10001 10001
@@ -37,8 +37,8 @@ printf "%s\n" \
"if [[ \"\${1:-}\" == build ]]; then" \ "if [[ \"\${1:-}\" == build ]]; then" \
" destination=; for argument in \"\$@\"; do case \"\$argument\" in type=local,dest=*) destination=\"\${argument#type=local,dest=}\" ;; esac; done" \ " destination=; for argument in \"\$@\"; do case \"\$argument\" in type=local,dest=*) destination=\"\${argument#type=local,dest=}\" ;; esac; done" \
" test -n \"\$destination\"; mkdir -p \"\$destination\"" \ " test -n \"\$destination\"; mkdir -p \"\$destination\"" \
" printf \"%s\\n\" \"#!/bin/bash\" \"set -euo pipefail\" \"test -r \\\"\\\$2\\\"\" \"docker compose up --detach\" > \"\$destination/thothctl-linux-amd64\"" \ " printf \"%s\\n\" \"#!/bin/bash\" \"set -euo pipefail\" \"test -r \\\"\\\$2\\\"\" \"docker compose up --detach\" > \"\$destination/tht-linux-amd64\"" \
" chmod 0750 \"\$destination/thothctl-linux-amd64\"; exit 0" \ " chmod 0750 \"\$destination/tht-linux-amd64\"; exit 0" \
"fi" \ "fi" \
"test \"\${1:-}\" = compose; : > /srv/thothii/operator/start.marker" \ "test \"\${1:-}\" = compose; : > /srv/thothii/operator/start.marker" \
> /usr/local/bin/docker > /usr/local/bin/docker
@@ -52,17 +52,17 @@ for protected in /srv/thothii /srv/thothii/source /srv/thothii/secrets \
/srv/thothii/data /srv/thothii/pi-state /srv/thothii/workspace-registry; do /srv/thothii/data /srv/thothii/pi-state /srv/thothii/workspace-registry; do
if touch "$protected/operator-must-not-write" 2>/dev/null; then exit 42; fi if touch "$protected/operator-must-not-write" 2>/dev/null; then exit 42; fi
done done
THT_THOTHCTL_OUTPUT_DIRECTORY=/srv/thothii/operator/build-output \ THT_THT_OUTPUT_DIRECTORY=/srv/thothii/operator/build-output \
/srv/thothii/source/ThothII/scripts/build-thothctl.sh /srv/thothii/source/ThothII/scripts/build-tht.sh
if THT_THOTHCTL_OUTPUT_DIRECTORY=relative-output \ if THT_THT_OUTPUT_DIRECTORY=relative-output \
/srv/thothii/source/ThothII/scripts/build-thothctl.sh 2>/dev/null; then exit 44; fi /srv/thothii/source/ThothII/scripts/build-tht.sh 2>/dev/null; then exit 44; fi
root_output_error=/srv/thothii/operator/root-output.error root_output_error=/srv/thothii/operator/root-output.error
if THT_THOTHCTL_OUTPUT_DIRECTORY=/ \ if THT_THT_OUTPUT_DIRECTORY=/ \
/srv/thothii/source/ThothII/scripts/build-thothctl.sh 2>"$root_output_error"; then exit 45; fi /srv/thothii/source/ThothII/scripts/build-tht.sh 2>"$root_output_error"; then exit 45; fi
grep -Fq "THT_THOTHCTL_OUTPUT_DIRECTORY must be an absolute canonical path" \ grep -Fq "THT_THT_OUTPUT_DIRECTORY must be an absolute canonical path" \
"$root_output_error" || exit 46 "$root_output_error" || exit 46
rm -f "$root_output_error" rm -f "$root_output_error"
/srv/thothii/operator/build-output/thothctl-linux-amd64 \ /srv/thothii/operator/build-output/tht-linux-amd64 \
--installation /srv/thothii/operator/thothii-installation.yaml start --installation /srv/thothii/operator/thothii-installation.yaml start
'\'' '\''
@@ -74,8 +74,8 @@ for target in auth.json models.json settings.json; do
test "$(stat -c %u:%g /srv/thothii/pi-state/agent/$target)" = 10001:10001 test "$(stat -c %u:%g /srv/thothii/pi-state/agent/$target)" = 10001:10001
test "$(stat -c %a /srv/thothii/pi-state/agent/$target)" = 600 test "$(stat -c %a /srv/thothii/pi-state/agent/$target)" = 600
done done
test "$(stat -c %u:%g /srv/thothii/operator/build-output/thothctl-linux-amd64)" = 20001:20002 test "$(stat -c %u:%g /srv/thothii/operator/build-output/tht-linux-amd64)" = 20001:20002
test "$(stat -c %a /srv/thothii/operator/build-output/thothctl-linux-amd64)" = 750 test "$(stat -c %a /srv/thothii/operator/build-output/tht-linux-amd64)" = 750
test -f /srv/thothii/operator/start.marker test -f /srv/thothii/operator/start.marker
for protected in /srv/thothii /srv/thothii/source /srv/thothii/secrets \ for protected in /srv/thothii /srv/thothii/source /srv/thothii/secrets \
/srv/thothii/data /srv/thothii/pi-state /srv/thothii/workspace-registry; do /srv/thothii/data /srv/thothii/pi-state /srv/thothii/workspace-registry; do
@@ -2,19 +2,19 @@
set -euo pipefail set -euo pipefail
repository_root=$(cd "$(dirname "$0")/.." && pwd) repository_root=$(cd "$(dirname "$0")/.." && pwd)
dockerfile="$repository_root/docker/thothctl.Dockerfile" dockerfile="$repository_root/docker/tht.Dockerfile"
builder_image=$(awk '$1 == "FROM" && $3 == "AS" && $4 == "build" { print $2; exit }' "$dockerfile") builder_image=$(awk '$1 == "FROM" && $3 == "AS" && $4 == "build" { print $2; exit }' "$dockerfile")
expected_builder='golang:1.26.5-bookworm@sha256:1ecb7edf62a0408027bd5729dfd6b1b8766e578e8df93995b225dfd0944eb651' expected_builder='golang:1.26.5-bookworm@sha256:1ecb7edf62a0408027bd5729dfd6b1b8766e578e8df93995b225dfd0944eb651'
if [[ "$builder_image" != "$expected_builder" ]]; then if [[ "$builder_image" != "$expected_builder" ]]; then
echo "thothctl builder must pin golang:1.26.5-bookworm by the approved multi-platform digest" >&2 echo "tht builder must pin golang:1.26.5-bookworm by the approved multi-platform digest" >&2
exit 1 exit 1
fi fi
grep -qx 'go 1.26.0' "$repository_root/tools/thothctl/go.mod" grep -qx 'go 1.26.0' "$repository_root/tools/tht/go.mod"
grep -qx 'toolchain go1.26.5' "$repository_root/tools/thothctl/go.mod" grep -qx 'toolchain go1.26.5' "$repository_root/tools/tht/go.mod"
grep -Eq '^[[:space:]]*github.com/sirupsen/logrus v1\.9\.1$' "$repository_root/tools/thothctl/go.mod" grep -Eq '^[[:space:]]*github.com/sirupsen/logrus v1\.9\.1$' "$repository_root/tools/tht/go.mod"
grep -Eq '^[[:space:]]*golang.org/x/sys v0\.47\.0$' "$repository_root/tools/thothctl/go.mod" grep -Eq '^[[:space:]]*golang.org/x/sys v0\.47\.0$' "$repository_root/tools/tht/go.mod"
manifest=$(docker buildx imagetools inspect "$builder_image") manifest=$(docker buildx imagetools inspect "$builder_image")
printf '%s\n' "$manifest" | grep -Eq 'Platform:[[:space:]]+linux/amd64' printf '%s\n' "$manifest" | grep -Eq 'Platform:[[:space:]]+linux/amd64'
@@ -24,10 +24,10 @@ temporary_output=$(mktemp -d)
trap 'rm -rf "$temporary_output"' EXIT HUP INT TERM trap 'rm -rf "$temporary_output"' EXIT HUP INT TERM
docker build --file "$dockerfile" --output "type=local,dest=$temporary_output" "$repository_root" >/dev/null docker build --file "$dockerfile" --output "type=local,dest=$temporary_output" "$repository_root" >/dev/null
test -s "$temporary_output/thothctl-windows-amd64.exe" test -s "$temporary_output/tht-windows-amd64.exe"
test -s "$temporary_output/thothctl-darwin-amd64" test -s "$temporary_output/tht-darwin-amd64"
test -s "$temporary_output/thothctl-darwin-arm64" test -s "$temporary_output/tht-darwin-arm64"
test -s "$temporary_output/thothctl-linux-amd64" test -s "$temporary_output/tht-linux-amd64"
test -s "$temporary_output/thothctl-linux-arm64" test -s "$temporary_output/tht-linux-arm64"
echo "thothctl build contract passed." echo "tht build contract passed."
@@ -87,7 +87,7 @@ for required in \
} }
done done
grep -Fq '"$THTCTL" --help' "$server_guide" || { grep -Fq '"$THTCTL" --help' "$server_guide" || {
echo "server guide lacks plain thothctl --help" >&2 echo "server guide lacks plain tht --help" >&2
exit 1 exit 1
} }
if grep -Fq '"$THTCTL" --installation "$INSTALLATION" --help' "$server_guide"; then if grep -Fq '"$THTCTL" --installation "$INSTALLATION" --help' "$server_guide"; then
@@ -106,7 +106,7 @@ for manual in "$root/docs/install/local-workspace-registry.md"; do
fi fi
done done
grep -Fq 'THTCTL=/srv/thothii/operator/thothctl' \ grep -Fq 'THTCTL=/srv/thothii/operator/tht' \
"$root/docs/install/server-workspace-registry.md" || { "$root/docs/install/server-workspace-registry.md" || {
echo "server installation manual does not use the installation-aware operator CLI" >&2 echo "server installation manual does not use the installation-aware operator CLI" >&2
exit 1 exit 1
@@ -957,7 +957,7 @@ expect_guide_rejected \
"$root/docs/install/local.md" docs/install/local.md failed-pull \ "$root/docs/install/local.md" docs/install/local.md failed-pull \
"POSIX source update does not fail closed: source pull" "POSIX source update does not fail closed: source pull"
expect_guide_rejected \ expect_guide_rejected \
"failed thothctl Pi status" verify_local_guide \ "failed tht Pi status" verify_local_guide \
"$root/docs/install/local.md" docs/install/local.md failed-status \ "$root/docs/install/local.md" docs/install/local.md failed-status \
"POSIX source update does not fail closed: Pi status" "POSIX source update does not fail closed: Pi status"
expect_guide_rejected \ expect_guide_rejected \
+8 -8
View File
@@ -134,11 +134,11 @@ try {
Copy-Item -LiteralPath $source -Destination $destination Copy-Item -LiteralPath $source -Destination $destination
} }
$thothctl = Join-Path $spacedRepository "dist/thothctl/thothctl-windows-amd64.exe" $tht = Join-Path $spacedRepository "dist/tht/tht-windows-amd64.exe"
[System.IO.Directory]::CreateDirectory([System.IO.Path]::GetDirectoryName($thothctl)) | Out-Null [System.IO.Directory]::CreateDirectory([System.IO.Path]::GetDirectoryName($tht)) | Out-Null
Invoke-BoundedNative -FilePath "go" -Arguments @("build", "-trimpath", "-o", $thothctl, "./cmd/thothctl") ` Invoke-BoundedNative -FilePath "go" -Arguments @("build", "-trimpath", "-o", $tht, "./cmd/tht") `
-WorkingDirectory (Join-Path $spacedRepository "tools/thothctl") -Label "build native Windows thothctl in spaced path" | Out-Null -WorkingDirectory (Join-Path $spacedRepository "tools/tht") -Label "build native Windows tht in spaced path" | Out-Null
Invoke-BoundedNative -FilePath $thothctl -Arguments @("--help") -Label "invoke native Windows thothctl from spaced path" | Out-Null Invoke-BoundedNative -FilePath $tht -Arguments @("--help") -Label "invoke native Windows tht from spaced path" | Out-Null
$piAuth = Join-Path $fixtureRoot "Pi Auth/pi-auth.json" $piAuth = Join-Path $fixtureRoot "Pi Auth/pi-auth.json"
$secrets = Join-Path $fixtureRoot "Secrets/thothii.secrets" $secrets = Join-Path $fixtureRoot "Secrets/thothii.secrets"
@@ -266,7 +266,7 @@ overrides:
if (($runningServices -join ",") -ne "core,frontend") { if (($runningServices -join ",") -ne "core,frontend") {
throw "bounded Windows startup did not leave exactly core and frontend running" throw "bounded Windows startup did not leave exactly core and frontend running"
} }
Invoke-BoundedNative -FilePath $thothctl -Arguments @("--installation", $installation, "status") -Label "invoke installation-aware Windows thothctl in spaced path" | Out-Null Invoke-BoundedNative -FilePath $tht -Arguments @("--installation", $installation, "status") -Label "invoke installation-aware Windows tht in spaced path" | Out-Null
} }
} }
finally { finally {
@@ -311,7 +311,7 @@ if (-not $cleanupSucceeded) {
throw "Windows cleanup proof failed; fixture path retained for recovery" throw "Windows cleanup proof failed; fixture path retained for recovery"
} }
if ($DockerStartup) { if ($DockerStartup) {
Write-Output "Windows spaced-path build, native thothctl, bounded two-service startup, and exact cleanup passed." Write-Output "Windows spaced-path build, native tht, bounded two-service startup, and exact cleanup passed."
} else { } else {
Write-Output "Windows spaced-path clone, LF-byte, Compose render, and native thothctl build/invocation contracts passed; Docker startup mode was not requested." Write-Output "Windows spaced-path clone, LF-byte, Compose render, and native tht build/invocation contracts passed; Docker startup mode was not requested."
} }
@@ -5,4 +5,4 @@ root="$(cd "$(dirname "$0")/.." && pwd -P)"
# shellcheck source=./unified-deployment-smoke.sh # shellcheck source=./unified-deployment-smoke.sh
source "$root/scripts/unified-deployment-smoke.sh" source "$root/scripts/unified-deployment-smoke.sh"
task13_supervise "$TASK13_SMOKE_TIMEOUT" "thothctl update smoke" task13_smoke_main update task13_supervise "$TASK13_SMOKE_TIMEOUT" "tht update smoke" task13_smoke_main update
+37 -37
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# End-to-end release gate for the canonical two-service Compose distribution. # End-to-end release gate for the canonical two-service Compose distribution.
# This file is also sourced by thothctl-update-smoke.sh so both entry points use the same # This file is also sourced by tht-update-smoke.sh so both entry points use the same
# isolated fixture, exact cleanup, and sanitized failure reporting. # isolated fixture, exact cleanup, and sanitized failure reporting.
set -euo pipefail set -euo pipefail
@@ -556,23 +556,23 @@ task13_seed_registry() {
task13_commit_registry_change 'Seed Task 13 workspace registry' task13_commit_registry_change 'Seed Task 13 workspace registry'
} }
task13_build_thothctl() { task13_build_tht() {
local os arch local os arch
mkdir -p "$TASK13_THOTHCTL_DIR" mkdir -p "$TASK13_THT_DIR"
task13_run_logged "build thothctl cross-platform binaries" env \ task13_run_logged "build tht cross-platform binaries" env \
THT_THOTHCTL_OUTPUT_DIRECTORY="$TASK13_THOTHCTL_DIR" \ THT_THT_OUTPUT_DIRECTORY="$TASK13_THT_DIR" \
bash "$TASK13_ROOT/scripts/build-thothctl.sh" bash "$TASK13_ROOT/scripts/build-tht.sh"
os="$(uname -s)" os="$(uname -s)"
arch="$(uname -m)" arch="$(uname -m)"
case "$os/$arch" in case "$os/$arch" in
Darwin/x86_64) TASK13_THOTHCTL="$TASK13_THOTHCTL_DIR/thothctl-darwin-amd64" ;; Darwin/x86_64) TASK13_THT="$TASK13_THT_DIR/tht-darwin-amd64" ;;
Darwin/arm64) TASK13_THOTHCTL="$TASK13_THOTHCTL_DIR/thothctl-darwin-arm64" ;; Darwin/arm64) TASK13_THT="$TASK13_THT_DIR/tht-darwin-arm64" ;;
Linux/x86_64|Linux/amd64) TASK13_THOTHCTL="$TASK13_THOTHCTL_DIR/thothctl-linux-amd64" ;; Linux/x86_64|Linux/amd64) TASK13_THT="$TASK13_THT_DIR/tht-linux-amd64" ;;
Linux/aarch64|Linux/arm64) TASK13_THOTHCTL="$TASK13_THOTHCTL_DIR/thothctl-linux-arm64" ;; Linux/aarch64|Linux/arm64) TASK13_THT="$TASK13_THT_DIR/tht-linux-arm64" ;;
*) task13_fail "unsupported smoke host: $os/$arch" ;; *) task13_fail "unsupported smoke host: $os/$arch" ;;
esac esac
chmod 0700 "$TASK13_THOTHCTL" chmod 0700 "$TASK13_THT"
task13_run_logged "invoke host thothctl" "$TASK13_THOTHCTL" --help task13_run_logged "invoke host tht" "$TASK13_THT" --help
} }
task13_assert_rendered_contract() { task13_assert_rendered_contract() {
@@ -670,12 +670,12 @@ task13_assert_runtime() {
|| task13_fail "core lacks the explicit Task 13 resource label" || task13_fail "core lacks the explicit Task 13 resource label"
task13_compose_logged "internal Pi provider smoke" exec -T core \ task13_compose_logged "internal Pi provider smoke" exec -T core \
curl --connect-timeout 3 --max-time 45 -fsS -X POST \ curl --connect-timeout 3 --max-time 45 -fsS -X POST \
-H 'x-thoth-principal-issuer: thothctl' \ -H 'x-thoth-principal-issuer: tht' \
-H 'x-thoth-principal-subject: thothctl-maintenance' \ -H 'x-thoth-principal-subject: tht-maintenance' \
-H 'x-thoth-principal-display-name: Thothctl maintenance' \ -H 'x-thoth-principal-display-name: Tht maintenance' \
-H 'x-thoth-is-admin: 1' \ -H 'x-thoth-is-admin: 1' \
http://127.0.0.1:8787/pi-management/test http://127.0.0.1:8787/pi-management/test
task13_run_logged "thothctl Pi doctor" "$TASK13_THOTHCTL" --installation "$TASK13_INSTALLATION" pi doctor task13_run_logged "tht Pi doctor" "$TASK13_THT" --installation "$TASK13_INSTALLATION" pi doctor
} }
task13_server_auth_headers() { task13_server_auth_headers() {
@@ -950,20 +950,20 @@ task13_update_rollback() {
TASK13_PREVIOUS_IMAGE_ID="$before_image" TASK13_PREVIOUS_IMAGE_ID="$before_image"
before_mounts="$(task13_mount_fingerprint)" before_mounts="$(task13_mount_fingerprint)"
before_head="$(task13_active_registry_head)" before_head="$(task13_active_registry_head)"
output="$TASK13_TMP/thothctl-update.out" output="$TASK13_TMP/tht-update.out"
set +e set +e
"$TASK13_THOTHCTL" --installation "$TASK13_INSTALLATION" pi update \ "$TASK13_THT" --installation "$TASK13_INSTALLATION" pi update \
--version "$TASK13_BAD_PI_VERSION" --source pull --image "$TASK13_BAD_CANDIDATE_IMAGE" --yes \ --version "$TASK13_BAD_PI_VERSION" --source pull --image "$TASK13_BAD_CANDIDATE_IMAGE" --yes \
>"$output" 2>&1 >"$output" 2>&1
rc=$? rc=$?
set -e set -e
[[ "$rc" -ne 0 ]] || task13_fail "bad Pi candidate unexpectedly passed update verification" [[ "$rc" -ne 0 ]] || task13_fail "bad Pi candidate unexpectedly passed update verification"
if grep -Fq "$TASK13_SECRET_VALUE" "$output"; then if grep -Fq "$TASK13_SECRET_VALUE" "$output"; then
task13_fail "thothctl update output exposed the fixture secret" task13_fail "tht update output exposed the fixture secret"
fi fi
grep -Fq 'previous core image was restored' "$output" \ grep -Fq 'previous core image was restored' "$output" \
|| { task13_sanitize <"$output" >&2; task13_fail "thothctl did not report automatic rollback"; } || { task13_sanitize <"$output" >&2; task13_fail "tht did not report automatic rollback"; }
[[ -f "$TASK13_UPDATE_STATE" ]] || task13_fail "thothctl update state was not persisted" [[ -f "$TASK13_UPDATE_STATE" ]] || task13_fail "tht update state was not persisted"
phase="$(sed -n 's/.*"phase": "\([^"]*\)".*/\1/p' "$TASK13_UPDATE_STATE" | head -n 1)" phase="$(sed -n 's/.*"phase": "\([^"]*\)".*/\1/p' "$TASK13_UPDATE_STATE" | head -n 1)"
[[ "$phase" == rolled_back ]] || task13_fail "update state phase is not rolled_back" [[ "$phase" == rolled_back ]] || task13_fail "update state phase is not rolled_back"
if grep -Fq "$TASK13_SECRET_VALUE" "$TASK13_UPDATE_STATE"; then if grep -Fq "$TASK13_SECRET_VALUE" "$TASK13_UPDATE_STATE"; then
@@ -977,8 +977,8 @@ task13_update_rollback() {
[[ "$after_mounts" == "$before_mounts" ]] || task13_fail "rollback changed persistence volume identity" [[ "$after_mounts" == "$before_mounts" ]] || task13_fail "rollback changed persistence volume identity"
[[ "$after_head" == "$before_head" ]] || task13_fail "rollback changed the active registry revision" [[ "$after_head" == "$before_head" ]] || task13_fail "rollback changed the active registry revision"
task13_assert_sentinels task13_assert_sentinels
task13_run_logged "post-rollback thothctl doctor" \ task13_run_logged "post-rollback tht doctor" \
"$TASK13_THOTHCTL" --installation "$TASK13_INSTALLATION" pi doctor "$TASK13_THT" --installation "$TASK13_INSTALLATION" pi doctor
task13_compose exec -T core curl --connect-timeout 3 --max-time 10 -fsS \ task13_compose exec -T core curl --connect-timeout 3 --max-time 10 -fsS \
http://127.0.0.1:8787/workspaces \ http://127.0.0.1:8787/workspaces \
| grep -Fq 'Task 13 Smoke' || task13_fail "rollback lost the active workspace" | grep -Fq 'Task 13 Smoke' || task13_fail "rollback lost the active workspace"
@@ -1020,7 +1020,7 @@ task13_remove_transaction_image() {
if ! docker image inspect "$reference" >/dev/null 2>&1; then if ! docker image inspect "$reference" >/dev/null 2>&1; then
return 0 return 0
fi fi
if [[ ! "$reference" =~ ^thothii-core:thothctl-[0-9a-f]{16}-(candidate|previous)$ \ if [[ ! "$reference" =~ ^thothii-core:tht-[0-9a-f]{16}-(candidate|previous)$ \
|| ! "$expected_id" =~ ^sha256:([0-9a-f]{64}|owned)$ ]]; then || ! "$expected_id" =~ ^sha256:([0-9a-f]{64}|owned)$ ]]; then
printf 'refusing to remove invalid transaction image reference %s\n' "$reference" >&2 printf 'refusing to remove invalid transaction image reference %s\n' "$reference" >&2
return 1 return 1
@@ -1108,9 +1108,9 @@ task13_cleanup() {
transaction="$(sed -n 's/.*"transaction": "\([^"]*\)".*/\1/p' "$TASK13_UPDATE_STATE" | head -n 1)" transaction="$(sed -n 's/.*"transaction": "\([^"]*\)".*/\1/p' "$TASK13_UPDATE_STATE" | head -n 1)"
fi fi
if [[ -n "$transaction" ]]; then if [[ -n "$transaction" ]]; then
task13_remove_transaction_image "thothii-core:thothctl-$transaction-candidate" \ task13_remove_transaction_image "thothii-core:tht-$transaction-candidate" \
"${TASK13_BAD_CANDIDATE_ID:-}" >>"${TASK13_LOG:-/dev/null}" 2>&1 || cleanup_rc=1 "${TASK13_BAD_CANDIDATE_ID:-}" >>"${TASK13_LOG:-/dev/null}" 2>&1 || cleanup_rc=1
task13_remove_transaction_image "thothii-core:thothctl-$transaction-previous" \ task13_remove_transaction_image "thothii-core:tht-$transaction-previous" \
"${TASK13_PREVIOUS_IMAGE_ID:-}" >>"${TASK13_LOG:-/dev/null}" 2>&1 || cleanup_rc=1 "${TASK13_PREVIOUS_IMAGE_ID:-}" >>"${TASK13_LOG:-/dev/null}" 2>&1 || cleanup_rc=1
fi fi
for image in \ for image in \
@@ -1126,7 +1126,7 @@ task13_cleanup() {
done < <(docker image ls -q --filter "label=io.thothii.task13.run=$TASK13_RUN_ID" | sort -u) done < <(docker image ls -q --filter "label=io.thothii.task13.run=$TASK13_RUN_ID" | sort -u)
fi fi
if [[ -n "${TASK13_CONTROL_DIR:-}" ]]; then if [[ -n "${TASK13_CONTROL_DIR:-}" ]]; then
if [[ "$TASK13_CONTROL_DIR" == "$TASK13_ROOT/.thothctl/$TASK13_PROJECT" \ if [[ "$TASK13_CONTROL_DIR" == "$TASK13_ROOT/.tht/$TASK13_PROJECT" \
&& "$TASK13_PROJECT" =~ ^thothii-[0-9a-f]{12}$ ]]; then && "$TASK13_PROJECT" =~ ^thothii-[0-9a-f]{12}$ ]]; then
rm -rf "$TASK13_CONTROL_DIR" rm -rf "$TASK13_CONTROL_DIR"
else else
@@ -1319,8 +1319,8 @@ task13_self_test_transaction_image_cleanup() {
local calls foreign_error owned_ref foreign_ref local calls foreign_error owned_ref foreign_ref
calls="$(mktemp "${TMPDIR:-/tmp}/thothii-task13-transaction-cleanup-contract.XXXXXX")" calls="$(mktemp "${TMPDIR:-/tmp}/thothii-task13-transaction-cleanup-contract.XXXXXX")"
foreign_error="$calls.foreign-error" foreign_error="$calls.foreign-error"
owned_ref="thothii-core:thothctl-0123456789abcdef-candidate" owned_ref="thothii-core:tht-0123456789abcdef-candidate"
foreign_ref="thothii-core:thothctl-fedcba9876543210-candidate" foreign_ref="thothii-core:tht-fedcba9876543210-candidate"
if ! declare -F task13_remove_transaction_image >/dev/null; then if ! declare -F task13_remove_transaction_image >/dev/null; then
rm -f "$calls" "$foreign_error" rm -f "$calls" "$foreign_error"
@@ -1478,7 +1478,7 @@ task13_self_test_public_timeout_contract() {
"$root/scripts/unified-deployment-smoke.sh" \ "$root/scripts/unified-deployment-smoke.sh" \
|| task13_fail "direct unified smoke invocation lacks an internal supervisor" || task13_fail "direct unified smoke invocation lacks an internal supervisor"
grep -Eq 'task13_supervise[[:space:]].*task13_smoke_main[[:space:]]+update' \ grep -Eq 'task13_supervise[[:space:]].*task13_smoke_main[[:space:]]+update' \
"$root/scripts/thothctl-update-smoke.sh" \ "$root/scripts/tht-update-smoke.sh" \
|| task13_fail "direct update smoke invocation lacks an internal supervisor" || task13_fail "direct update smoke invocation lacks an internal supervisor"
grep -Eq 'task13_supervise[[:space:]].*task13_internal_semantic_smoke_main' \ grep -Eq 'task13_supervise[[:space:]].*task13_internal_semantic_smoke_main' \
"$root/scripts/internal-semantic-smoke.sh" \ "$root/scripts/internal-semantic-smoke.sh" \
@@ -1562,7 +1562,7 @@ task13_self_test_source_contract() {
registry_function='task13_start_''registry' registry_function='task13_start_''registry'
if rg -n 'docker[[:space:]]+(system[[:space:]]+)?prune' \ if rg -n 'docker[[:space:]]+(system[[:space:]]+)?prune' \
"$root/scripts/unified-deployment-smoke.sh" \ "$root/scripts/unified-deployment-smoke.sh" \
"$root/scripts/thothctl-update-smoke.sh" \ "$root/scripts/tht-update-smoke.sh" \
"$root/scripts/internal-semantic-smoke.sh" >/dev/null; then "$root/scripts/internal-semantic-smoke.sh" >/dev/null; then
task13_fail "Task 13 smoke scripts must never prune global Docker state" task13_fail "Task 13 smoke scripts must never prune global Docker state"
fi fi
@@ -1577,8 +1577,8 @@ task13_self_test_source_contract() {
|| task13_fail "the bad rollback candidate must be an immutable digest reference" || task13_fail "the bad rollback candidate must be an immutable digest reference"
grep -Eq 'timeout .*scripts/unified-deployment-smoke\.sh' "$workflow" \ grep -Eq 'timeout .*scripts/unified-deployment-smoke\.sh' "$workflow" \
|| task13_fail "CI lacks an outer timeout for the unified deployment smoke" || task13_fail "CI lacks an outer timeout for the unified deployment smoke"
grep -Eq 'timeout .*scripts/thothctl-update-smoke\.sh' "$workflow" \ grep -Eq 'timeout .*scripts/tht-update-smoke\.sh' "$workflow" \
|| task13_fail "CI lacks an outer timeout for the thothctl update smoke" || task13_fail "CI lacks an outer timeout for the tht update smoke"
uses_count="$(grep -Ec '^[[:space:]]+uses:' "$workflow")" uses_count="$(grep -Ec '^[[:space:]]+uses:' "$workflow")"
pinned_uses_count="$(grep -Ec '^[[:space:]]+uses: [^[:space:]]+@[0-9a-f]{40}([[:space:]]|$)' "$workflow")" pinned_uses_count="$(grep -Ec '^[[:space:]]+uses: [^[:space:]]+@[0-9a-f]{40}([[:space:]]|$)' "$workflow")"
[[ "$uses_count" -gt 0 && "$uses_count" -eq "$pinned_uses_count" ]] \ [[ "$uses_count" -gt 0 && "$uses_count" -eq "$pinned_uses_count" ]] \
@@ -1697,8 +1697,8 @@ task13_initialize() {
TASK13_PROFILE="local" TASK13_PROFILE="local"
TASK13_INSTALLATION="$TASK13_TMP/thothii-installation.yaml" TASK13_INSTALLATION="$TASK13_TMP/thothii-installation.yaml"
TASK13_PROJECT="thothii-$(task13_sha256_text "$TASK13_INSTALLATION" | cut -c1-12)" TASK13_PROJECT="thothii-$(task13_sha256_text "$TASK13_INSTALLATION" | cut -c1-12)"
TASK13_CONTROL_DIR="$TASK13_ROOT/.thothctl/$TASK13_PROJECT" TASK13_CONTROL_DIR="$TASK13_ROOT/.tht/$TASK13_PROJECT"
[[ ! -e "$TASK13_CONTROL_DIR" ]] || task13_fail "unique thothctl control directory already exists" [[ ! -e "$TASK13_CONTROL_DIR" ]] || task13_fail "unique tht control directory already exists"
TASK13_CURRENT_IMAGE_OVERRIDE="$TASK13_CONTROL_DIR/current-image.yaml" TASK13_CURRENT_IMAGE_OVERRIDE="$TASK13_CONTROL_DIR/current-image.yaml"
TASK13_UPDATE_STATE="$TASK13_CONTROL_DIR/update-state.json" TASK13_UPDATE_STATE="$TASK13_CONTROL_DIR/update-state.json"
TASK13_REMOTE="$TASK13_TMP/remote.git" TASK13_REMOTE="$TASK13_TMP/remote.git"
@@ -1711,7 +1711,7 @@ task13_initialize() {
TASK13_PI_MODELS="$TASK13_TMP/models.json" TASK13_PI_MODELS="$TASK13_TMP/models.json"
TASK13_PI_SETTINGS="$TASK13_TMP/pi-settings.json" TASK13_PI_SETTINGS="$TASK13_TMP/pi-settings.json"
TASK13_LLM_SERVER="$TASK13_TMP/fake-llm.mjs" TASK13_LLM_SERVER="$TASK13_TMP/fake-llm.mjs"
TASK13_THOTHCTL_DIR="$TASK13_TMP/thothctl" TASK13_THT_DIR="$TASK13_TMP/tht"
TASK13_LLM_CONTAINER="$TASK13_PROJECT-llm" TASK13_LLM_CONTAINER="$TASK13_PROJECT-llm"
TASK13_BAD_CANDIDATE_CONTAINER="$TASK13_PROJECT-bad-candidate" TASK13_BAD_CANDIDATE_CONTAINER="$TASK13_PROJECT-bad-candidate"
TASK13_CORE_IMAGE="task13-core-$TASK13_RUN_ID:local" TASK13_CORE_IMAGE="task13-core-$TASK13_RUN_ID:local"
@@ -1748,7 +1748,7 @@ task13_smoke_main() {
task13_write_fixture_files task13_write_fixture_files
task13_write_environment /fixtures/remote.git task13_write_environment /fixtures/remote.git
task13_seed_registry task13_seed_registry
task13_build_thothctl task13_build_tht
task13_start_stack task13_start_stack
task13_assert_project_ownership task13_assert_project_ownership
task13_assert_built_image_ownership task13_assert_built_image_ownership
+20 -20
View File
@@ -768,7 +768,7 @@ verify_local_guide() {
"Clone and verify LF" \ "Clone and verify LF" \
"Create the local operator files" \ "Create the local operator files" \
"Address external services" \ "Address external services" \
"Build ThothII and thothctl" \ "Build ThothII and tht" \
"Start and verify" \ "Start and verify" \
"Update an installation" \ "Update an installation" \
"Back up and restore" \ "Back up and restore" \
@@ -783,8 +783,8 @@ verify_local_guide() {
"container 127.0.0.1" \ "container 127.0.0.1" \
"bash scripts/build-local.sh" \ "bash scripts/build-local.sh" \
"scripts/build-local.ps1" \ "scripts/build-local.ps1" \
"bash scripts/build-thothctl.sh" \ "bash scripts/build-tht.sh" \
"thothctl --installation" \ "tht --installation" \
"curl --fail http://127.0.0.1:8080/health" \ "curl --fail http://127.0.0.1:8080/health" \
"http://127.0.0.1:8080" \ "http://127.0.0.1:8080" \
"git pull --ff-only" \ "git pull --ff-only" \
@@ -856,8 +856,8 @@ requirePattern("POSIX source update does not fail closed: Pi status", updateShel
/if ! RUNNING_PI_VERSION="\$\("\$THTCTL" --installation "\$INSTALLATION" pi status\)"; then/); /if ! RUNNING_PI_VERSION="\$\("\$THTCTL" --installation "\$INSTALLATION" pi status\)"; then/);
requirePattern("POSIX source update does not fail closed: local build", updateShell, requirePattern("POSIX source update does not fail closed: local build", updateShell,
/if ! bash scripts\/build-local\.sh; then/); /if ! bash scripts\/build-local\.sh; then/);
requirePattern("POSIX source update does not fail closed: thothctl build", updateShell, requirePattern("POSIX source update does not fail closed: tht build", updateShell,
/if ! bash scripts\/build-thothctl\.sh; then/); /if ! bash scripts\/build-tht\.sh; then/);
requirePattern("POSIX source update lacks the same-version/no-selector path", updateShell, requirePattern("POSIX source update lacks the same-version/no-selector path", updateShell,
/if \[\[ "\$NEXT_PI_VERSION" == "\$RUNNING_PI_VERSION" \]\]; then[\s\S]*"\$USES_BASE_CORE" == true[\s\S]*TRANSACTIONAL_PI_UPDATE=false/); /if \[\[ "\$NEXT_PI_VERSION" == "\$RUNNING_PI_VERSION" \]\]; then[\s\S]*"\$USES_BASE_CORE" == true[\s\S]*TRANSACTIONAL_PI_UPDATE=false/);
for (const [label, pattern] of [ for (const [label, pattern] of [
@@ -886,7 +886,7 @@ for (const [command, step] of [
["$InstallationStatus = @(& $THTCTL --installation $INSTALLATION status)", "installation status"], ["$InstallationStatus = @(& $THTCTL --installation $INSTALLATION status)", "installation status"],
["$RunningPiStatus = (& $THTCTL --installation $INSTALLATION pi status)", "Pi status"], ["$RunningPiStatus = (& $THTCTL --installation $INSTALLATION pi status)", "Pi status"],
["powershell -ExecutionPolicy Bypass -File scripts/build-local.ps1", "local image build"], ["powershell -ExecutionPolicy Bypass -File scripts/build-local.ps1", "local image build"],
["& \"C:\\Program Files\\Git\\bin\\bash.exe\" scripts/build-thothctl.sh", "thothctl build"], ["& \"C:\\Program Files\\Git\\bin\\bash.exe\" scripts/build-tht.sh", "tht build"],
["curl.exe --fail --silent --show-error http://127.0.0.1:8080/health", "frontend health check"], ["curl.exe --fail --silent --show-error http://127.0.0.1:8080/health", "frontend health check"],
["curl.exe --fail --silent --show-error http://127.0.0.1:8787/health", "core health check"], ["curl.exe --fail --silent --show-error http://127.0.0.1:8787/health", "core health check"],
["$FinalPiStatus = (& $THTCTL --installation $INSTALLATION pi status)", "final Pi status"], ["$FinalPiStatus = (& $THTCTL --installation $INSTALLATION pi status)", "final Pi status"],
@@ -942,18 +942,18 @@ NODE
'exit 0' >"$update_fixture/bin/bash" 'exit 0' >"$update_fixture/bin/bash"
printf '%s\n' \ printf '%s\n' \
'#!/bin/sh' \ '#!/bin/sh' \
'printf "thothctl %s\n" "$*" >>"$CALLS"' \ 'printf "tht %s\n" "$*" >>"$CALLS"' \
'case " $* " in' \ 'case " $* " in' \
' *" pi status "*) [ "$FAIL_STEP" != status ] || exit 7; printf "Pi version: 0.80.3\n" ;;' \ ' *" pi status "*) [ "$FAIL_STEP" != status ] || exit 7; printf "Pi version: 0.80.3\n" ;;' \
' *" status "*) printf "[{\"Service\":\"core\",\"Image\":\"thothii-core:local\"}]\n" ;;' \ ' *" status "*) printf "[{\"Service\":\"core\",\"Image\":\"thothii-core:local\"}]\n" ;;' \
'esac' \ 'esac' \
'exit 0' >"$update_fixture/bin/thothctl" 'exit 0' >"$update_fixture/bin/tht"
printf '%s\n' \ printf '%s\n' \
'#!/bin/sh' \ '#!/bin/sh' \
'printf "curl %s\n" "$*" >>"$CALLS"' \ 'printf "curl %s\n" "$*" >>"$CALLS"' \
'exit 0' >"$update_fixture/bin/curl" 'exit 0' >"$update_fixture/bin/curl"
chmod 0700 "$update_fixture/bin/git" "$update_fixture/bin/bash" \ chmod 0700 "$update_fixture/bin/git" "$update_fixture/bin/bash" \
"$update_fixture/bin/thothctl" "$update_fixture/bin/curl" "$update_fixture/bin/tht" "$update_fixture/bin/curl"
for fixture_step in clean dirty pull status build; do for fixture_step in clean dirty pull status build; do
calls="$update_fixture/calls-$fixture_step" calls="$update_fixture/calls-$fixture_step"
@@ -963,7 +963,7 @@ NODE
( (
cd "$update_fixture/project" cd "$update_fixture/project"
env PATH="$update_fixture/bin:$PATH" CALLS="$calls" FAIL_STEP="$fixture_step" \ env PATH="$update_fixture/bin:$PATH" CALLS="$calls" FAIL_STEP="$fixture_step" \
THTCTL="$update_fixture/bin/thothctl" INSTALLATION="$update_fixture/installation.yaml" \ THTCTL="$update_fixture/bin/tht" INSTALLATION="$update_fixture/installation.yaml" \
/bin/bash "$update_script" /bin/bash "$update_script"
) >"$output" 2>&1 ) >"$output" 2>&1
status=$? status=$?
@@ -978,7 +978,7 @@ NODE
return 1 return 1
fi fi
grep -Fq 'bash scripts/build-local.sh' "$calls" || return 1 grep -Fq 'bash scripts/build-local.sh' "$calls" || return 1
grep -Fq 'thothctl --installation ' "$calls" || return 1 grep -Fq 'tht --installation ' "$calls" || return 1
else else
[[ $status -ne 0 ]] || { echo "$fixture_step source failure fixture was accepted" >&2; return 1; } [[ $status -ne 0 ]] || { echo "$fixture_step source failure fixture was accepted" >&2; return 1; }
if grep -Fq 'Built source revision:' "$output"; then if grep -Fq 'Built source revision:' "$output"; then
@@ -1131,13 +1131,13 @@ NODE
verify_pi_management_guide() { verify_pi_management_guide() {
local guide="$root/docs/install/pi-management.md" local guide="$root/docs/install/pi-management.md"
local lifecycle_contract="$root/docs/contracts/thothctl-pi.md" local lifecycle_contract="$root/docs/contracts/tht-pi.md"
[[ -f "$guide" ]] || { [[ -f "$guide" ]] || {
echo "missing Pi management guide: docs/install/pi-management.md" >&2 echo "missing Pi management guide: docs/install/pi-management.md" >&2
return 1 return 1
} }
[[ -f "$lifecycle_contract" ]] || { [[ -f "$lifecycle_contract" ]] || {
echo "missing Pi lifecycle contract: docs/contracts/thothctl-pi.md" >&2 echo "missing Pi lifecycle contract: docs/contracts/tht-pi.md" >&2
return 1 return 1
} }
require_text "$lifecycle_contract" "Pi lifecycle contract" \ require_text "$lifecycle_contract" "Pi lifecycle contract" \
@@ -1192,7 +1192,7 @@ const marker = "## Direct support access";
const start = source.indexOf(marker); const start = source.indexOf(marker);
const support = start < 0 ? "" : source.slice(start, source.indexOf("\n## ", start + marker.length) < 0 const support = start < 0 ? "" : source.slice(start, source.indexOf("\n## ", start + marker.length) < 0
? source.length : source.indexOf("\n## ", start + marker.length)); ? source.length : source.indexOf("\n## ", start + marker.length));
for (const token of ["unsupported", "thothctl", "pi status", "pi doctor", "pi test", "pi logs"]) { for (const token of ["unsupported", "tht", "pi status", "pi doctor", "pi test", "pi logs"]) {
if (!support.toLowerCase().includes(token.toLowerCase())) { if (!support.toLowerCase().includes(token.toLowerCase())) {
throw new Error(`direct support section lacks installation-aware diagnostic: ${token}`); throw new Error(`direct support section lacks installation-aware diagnostic: ${token}`);
} }
@@ -1214,7 +1214,7 @@ verify_server_guide() {
"Address co-resident external services" \ "Address co-resident external services" \
"Prepare operator files and secrets" \ "Prepare operator files and secrets" \
"Build locally or select pinned images" \ "Build locally or select pinned images" \
"Install thothctl" \ "Install tht" \
"Start and verify readiness" \ "Start and verify readiness" \
"Configure TLS and upstream authentication" \ "Configure TLS and upstream authentication" \
"Operate Pi, drain, and roll back" \ "Operate Pi, drain, and roll back" \
@@ -1228,7 +1228,7 @@ verify_server_guide() {
"thothii-ops" \ "thothii-ops" \
"-m 2770 /srv/thothii/operator" \ "-m 2770 /srv/thothii/operator" \
"chmod 0660 /srv/thothii/operator/server.env" \ "chmod 0660 /srv/thothii/operator/server.env" \
"THT_THOTHCTL_OUTPUT_DIRECTORY=/srv/thothii/operator/build-output" \ "THT_THT_OUTPUT_DIRECTORY=/srv/thothii/operator/build-output" \
"/srv/thothii" \ "/srv/thothii" \
"example operator root" \ "example operator root" \
"/run/secrets" \ "/run/secrets" \
@@ -1243,8 +1243,8 @@ verify_server_guide() {
"embedding" \ "embedding" \
"bash scripts/build-local.sh" \ "bash scripts/build-local.sh" \
"@sha256:" \ "@sha256:" \
"bash scripts/build-thothctl.sh" \ "bash scripts/build-tht.sh" \
"thothctl --installation" \ "tht --installation" \
"sessions migrate --yes" \ "sessions migrate --yes" \
'"pending":[]' \ '"pending":[]' \
'"drifted":[]' \ '"drifted":[]' \
@@ -1325,7 +1325,7 @@ if (/session-migrate:[\s\S]{0,180}image:\s*thothii-core:local/.test(source) &&
throw new Error("server pinned migration image must equal the pinned core image"); throw new Error("server pinned migration image must equal the pinned core image");
} }
if (/```(?:sh|bash)\n[\s\S]*?\bdocker compose\s+(?:up|stop|down|restart|pull|build)\b[\s\S]*?```/i.test(source)) { if (/```(?:sh|bash)\n[\s\S]*?\bdocker compose\s+(?:up|stop|down|restart|pull|build)\b[\s\S]*?```/i.test(source)) {
throw new Error("server lifecycle must use thothctl, not raw Docker Compose"); throw new Error("server lifecycle must use tht, not raw Docker Compose");
} }
NODE NODE
echo "server installation guide contract passed" echo "server installation guide contract passed"
@@ -1735,7 +1735,7 @@ verify_manual() {
) )
else else
expected_steps=( expected_steps=(
'THTCTL=/srv/thothii/operator/thothctl' 'THTCTL=/srv/thothii/operator/tht'
'INSTALLATION=/srv/thothii/operator/thothii-installation.yaml' 'INSTALLATION=/srv/thothii/operator/thothii-installation.yaml'
'"$THTCTL" --installation "$INSTALLATION" start' '"$THTCTL" --installation "$INSTALLATION" start'
'"$THTCTL" --installation "$INSTALLATION" doctor' '"$THTCTL" --installation "$INSTALLATION" doctor'
@@ -1,4 +1,4 @@
// thothctl is the host-side operator command for a local ThothII installation. // tht is the host-side operator command for a local ThothII installation.
package main package main
import ( import (
@@ -14,15 +14,15 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/aritmolab/thothii/tools/thothctl/internal/compose" "github.com/aritmolab/thothii/tools/tht/internal/compose"
"github.com/aritmolab/thothii/tools/thothctl/internal/config" "github.com/aritmolab/thothii/tools/tht/internal/config"
"github.com/aritmolab/thothii/tools/thothctl/internal/output" "github.com/aritmolab/thothii/tools/tht/internal/output"
"github.com/aritmolab/thothii/tools/thothctl/internal/pi" "github.com/aritmolab/thothii/tools/tht/internal/pi"
"github.com/aritmolab/thothii/tools/thothctl/internal/serverops" "github.com/aritmolab/thothii/tools/tht/internal/serverops"
"github.com/aritmolab/thothii/tools/thothctl/internal/workspaceops" "github.com/aritmolab/thothii/tools/tht/internal/workspaceops"
) )
const usage = `Usage: thothctl --installation <absolute-path>/thothii-installation.yaml <command> const usage = `Usage: tht --installation <absolute-path>/thothii-installation.yaml <command>
Commands: Commands:
status Show the Compose service state. status Show the Compose service state.
@@ -75,22 +75,22 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
} }
installationPath, command, commandArgs, err := parseArgs(args) installationPath, command, commandArgs, err := parseArgs(args)
if err != nil { if err != nil {
fmt.Fprintf(stderr, "thothctl: %s\n\n%s", err, usage) fmt.Fprintf(stderr, "tht: %s\n\n%s", err, usage)
return 2 return 2
} }
installation, err := config.Load(installationPath) installation, err := config.Load(installationPath)
if err != nil { if err != nil {
fmt.Fprintf(stderr, "thothctl: %s\n", output.Sanitize(err.Error(), nil)) fmt.Fprintf(stderr, "tht: %s\n", output.Sanitize(err.Error(), nil))
return 2 return 2
} }
secretFiles, err := installation.SecretFiles() secretFiles, err := installation.SecretFiles()
if err != nil { if err != nil {
fmt.Fprintln(stderr, "thothctl: installation secret declarations could not be read") fmt.Fprintln(stderr, "tht: installation secret declarations could not be read")
return 2 return 2
} }
secretValues, err := output.SecretValuesFromFiles(secretFiles) secretValues, err := output.SecretValuesFromFiles(secretFiles)
if err != nil { if err != nil {
fmt.Fprintln(stderr, "thothctl: declared secret file could not be read") fmt.Fprintln(stderr, "tht: declared secret file could not be read")
return 2 return 2
} }
@@ -139,7 +139,7 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
return serverOperationFailure(stderr, operationErr, secretValues) return serverOperationFailure(stderr, operationErr, secretValues)
} }
if encodeErr := json.NewEncoder(stdout).Encode(status); encodeErr != nil { if encodeErr := json.NewEncoder(stdout).Encode(status); encodeErr != nil {
fmt.Fprintln(stderr, "thothctl: migration status could not be written") fmt.Fprintln(stderr, "tht: migration status could not be written")
return 1 return 1
} }
return 0 return 0
@@ -154,7 +154,7 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
removal, operationErr := serverops.Remove(ctx, installation, runner, confirmedIDs) removal, operationErr := serverops.Remove(ctx, installation, runner, confirmedIDs)
writeRemovalTargets(stdout, installation.ProjectName(), removal.Targets) writeRemovalTargets(stdout, installation.ProjectName(), removal.Targets)
if errors.Is(operationErr, serverops.ErrConfirmationRequired) { if errors.Is(operationErr, serverops.ErrConfirmationRequired) {
fmt.Fprint(stderr, "thothctl: inspect the exact targets above, then re-run with remove --yes") fmt.Fprint(stderr, "tht: inspect the exact targets above, then re-run with remove --yes")
for _, target := range removal.Targets { for _, target := range removal.Targets {
fmt.Fprintf(stderr, " %s", target.ID) fmt.Fprintf(stderr, " %s", target.ID)
} }
@@ -198,7 +198,7 @@ func workspaceCommand(ctx context.Context, installation config.Installation, run
encoder := json.NewEncoder(stdout) encoder := json.NewEncoder(stdout)
encoder.SetEscapeHTML(false) encoder.SetEscapeHTML(false)
if encodeErr := encoder.Encode(result); encodeErr != nil { if encodeErr := encoder.Encode(result); encodeErr != nil {
fmt.Fprintln(stderr, "thothctl: workspace result could not be written") fmt.Fprintln(stderr, "tht: workspace result could not be written")
return 1 return 1
} }
} else { } else {
@@ -219,9 +219,9 @@ func workspaceFailure(stderr io.Writer, err error, secretValues []string) int {
var operationErr *workspaceops.OperationError var operationErr *workspaceops.OperationError
if errors.As(err, &operationErr) && operationErr.Detail() != "" { if errors.As(err, &operationErr) && operationErr.Detail() != "" {
detail := output.SanitizeDetail(operationErr.Detail(), secretValues) detail := output.SanitizeDetail(operationErr.Detail(), secretValues)
fmt.Fprintf(stderr, "thothctl: %s: %s\n", message, detail) fmt.Fprintf(stderr, "tht: %s: %s\n", message, detail)
} else { } else {
fmt.Fprintf(stderr, "thothctl: %s\n", message) fmt.Fprintf(stderr, "tht: %s\n", message)
} }
return 1 return 1
} }
@@ -231,9 +231,9 @@ func serverOperationFailure(stderr io.Writer, err error, secretValues []string)
var operationErr *serverops.OperationError var operationErr *serverops.OperationError
if errors.As(err, &operationErr) && operationErr.Detail() != "" { if errors.As(err, &operationErr) && operationErr.Detail() != "" {
detail := output.SanitizeDetail(operationErr.Detail(), secretValues) detail := output.SanitizeDetail(operationErr.Detail(), secretValues)
fmt.Fprintf(stderr, "thothctl: %s: %s\n", message, detail) fmt.Fprintf(stderr, "tht: %s: %s\n", message, detail)
} else { } else {
fmt.Fprintf(stderr, "thothctl: %s\n", message) fmt.Fprintf(stderr, "tht: %s\n", message)
} }
if errors.Is(err, serverops.ErrConfirmationRequired) || errors.Is(err, serverops.ErrUnsafeState) { if errors.Is(err, serverops.ErrConfirmationRequired) || errors.Is(err, serverops.ErrUnsafeState) {
return 2 return 2
@@ -315,7 +315,7 @@ func piCommand(ctx context.Context, installation config.Installation, runner com
if err := pi.Configure(ctx, controlled, defaults); err != nil { if err := pi.Configure(ctx, controlled, defaults); err != nil {
return piFailure(stderr, err, secretValues) return piFailure(stderr, err, secretValues)
} }
fmt.Fprintf(stdout, "Pi defaults applied and read back. Provider credentials remain only in the host file %s (mode 0600). Never pass credentials to thothctl.\n", authFile) fmt.Fprintf(stdout, "Pi defaults applied and read back. Provider credentials remain only in the host file %s (mode 0600). Never pass credentials to tht.\n", authFile)
return 0 return 0
case "restart": case "restart":
request, err := parsePiRestartArgs( request, err := parsePiRestartArgs(
@@ -583,7 +583,7 @@ func piFailure(stderr io.Writer, err error, secretValues []string) int {
if errors.As(err, &childExit) && childExit.ExitCode() != 0 { if errors.As(err, &childExit) && childExit.ExitCode() != 0 {
code = childExit.ExitCode() code = childExit.ExitCode()
} }
fmt.Fprintf(stderr, "thothctl: %s\n", output.Sanitize(err.Error(), secretValues)) fmt.Fprintf(stderr, "tht: %s\n", output.Sanitize(err.Error(), secretValues))
return code return code
} }
@@ -605,7 +605,7 @@ func logsArgs(args []string) ([]string, error) {
} }
func commandUsageError(stderr io.Writer, message string) int { func commandUsageError(stderr io.Writer, message string) int {
fmt.Fprintf(stderr, "thothctl: %s\n", message) fmt.Fprintf(stderr, "tht: %s\n", message)
return 2 return 2
} }
@@ -620,7 +620,7 @@ func writeResult(result compose.Result, err error, secretValues []string, stdout
return 0 return 0
} }
if errors.Is(err, exec.ErrNotFound) { if errors.Is(err, exec.ErrNotFound) {
fmt.Fprintln(stderr, "thothctl: Docker is not installed or is not on PATH") fmt.Fprintln(stderr, "tht: Docker is not installed or is not on PATH")
} }
if result.ExitCode != 0 { if result.ExitCode != 0 {
return result.ExitCode return result.ExitCode
@@ -650,15 +650,15 @@ func doctor(ctx context.Context, installation config.Installation, runner compos
} }
} }
if err := requireLF(installation.ProjectDirectory); err != nil { if err := requireLF(installation.ProjectDirectory); err != nil {
fmt.Fprintf(stderr, "thothctl: %s\n", err) fmt.Fprintf(stderr, "tht: %s\n", err)
return 1 return 1
} }
if err := requireVolumes(renderedConfig); err != nil { if err := requireVolumes(renderedConfig); err != nil {
fmt.Fprintf(stderr, "thothctl: %s\n", err) fmt.Fprintf(stderr, "tht: %s\n", err)
return 1 return 1
} }
if err := requireHealthyServices(status); err != nil { if err := requireHealthyServices(status); err != nil {
fmt.Fprintf(stderr, "thothctl: %s\n", err) fmt.Fprintf(stderr, "tht: %s\n", err)
return 1 return 1
} }
fmt.Fprintln(stdout, "Doctor checks passed.") fmt.Fprintln(stdout, "Doctor checks passed.")
@@ -11,10 +11,10 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/aritmolab/thothii/tools/thothctl/internal/compose" "github.com/aritmolab/thothii/tools/tht/internal/compose"
"github.com/aritmolab/thothii/tools/thothctl/internal/config" "github.com/aritmolab/thothii/tools/tht/internal/config"
"github.com/aritmolab/thothii/tools/thothctl/internal/pi" "github.com/aritmolab/thothii/tools/tht/internal/pi"
"github.com/aritmolab/thothii/tools/thothctl/internal/testsupport" "github.com/aritmolab/thothii/tools/tht/internal/testsupport"
) )
func TestInstallationRunnerMapsProfileToSessionInventoryScope(t *testing.T) { func TestInstallationRunnerMapsProfileToSessionInventoryScope(t *testing.T) {
@@ -32,6 +32,48 @@ func TestInstallationRunnerMapsProfileToSessionInventoryScope(t *testing.T) {
} }
} }
func TestRootCommandIdentity(t *testing.T) {
fixture := newCLIFixture(t, "")
fixture.setEnvironment(t)
retiredCommand := "thoth" + "ctl"
for _, test := range []struct {
name string
args []string
wantCode int
wantText string
}{
{
name: "help banner",
args: []string{"--help"},
wantCode: 0,
wantText: "Usage: tht ",
},
{
name: "version path",
args: []string{"--installation", fixture.installationPath, "version"},
wantCode: 2,
wantText: `tht: unknown command "version"`,
},
{
name: "retired command is not an alias",
args: []string{"--installation", fixture.installationPath, retiredCommand},
wantCode: 2,
wantText: `tht: unknown command "` + retiredCommand + `"`,
},
} {
t.Run(test.name, func(t *testing.T) {
var stdout, stderr bytes.Buffer
if got := run(context.Background(), test.args, &stdout, &stderr); got != test.wantCode {
t.Fatalf("run(%v) exit code = %d, want %d", test.args, got, test.wantCode)
}
if got := stdout.String() + stderr.String(); !strings.Contains(got, test.wantText) {
t.Fatalf("run(%v) output = %q, want %q", test.args, got, test.wantText)
}
})
}
}
// Catches interactive configuration prompts that use retired model-only data instead of the // Catches interactive configuration prompts that use retired model-only data instead of the
// provider, model, and reasoning choices supplied by the dedicated Pi Management API. // provider, model, and reasoning choices supplied by the dedicated Pi Management API.
func TestResolvePiConfigureUsesNumberedClosedChoicesOnlyForTTY(t *testing.T) { func TestResolvePiConfigureUsesNumberedClosedChoicesOnlyForTTY(t *testing.T) {
@@ -106,8 +148,8 @@ func TestUsageDocumentsClosedConfigureUpdateSourcesRestartAndMaintenanceRecovery
} }
func TestParsePiRestartArgs(t *testing.T) { func TestParsePiRestartArgs(t *testing.T) {
restartPath := "/var/lib/thothctl/restart-state.json" restartPath := "/var/lib/tht/restart-state.json"
updatePath := "/var/lib/thothctl/update-state.json" updatePath := "/var/lib/tht/update-state.json"
for _, test := range []struct { for _, test := range []struct {
name string name string
args []string args []string
@@ -191,9 +233,9 @@ func TestRunSessionsMigrateRedactsCompleteDetailBeforeDisplayBound(t *testing.T)
t.Fatal(err) t.Fatal(err)
} }
fixture.setEnvironment(t, secretPath) fixture.setEnvironment(t, secretPath)
t.Setenv("THOTHCTL_FAKE_CONFIG", `{"services":{"core":{"image":"thothii-core:local"},"session-migrate":{"image":"thothii-core:local"}}}`) t.Setenv("THT_FAKE_CONFIG", `{"services":{"core":{"image":"thothii-core:local"},"session-migrate":{"image":"thothii-core:local"}}}`)
t.Setenv("THOTHCTL_FAKE_MIGRATION_FAILURE", spec.failure) t.Setenv("THT_FAKE_MIGRATION_FAILURE", spec.failure)
t.Setenv("THOTHCTL_FAKE_MIGRATION_EXIT", "23") t.Setenv("THT_FAKE_MIGRATION_EXIT", "23")
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
code := run(context.Background(), []string{ code := run(context.Background(), []string{
@@ -222,7 +264,7 @@ func TestRunRemoveDisplaysExactInstallationTargetsBeforeConfirmation(t *testing.
fixture := newCLIFixture(t, "") fixture := newCLIFixture(t, "")
fixture.setProfile(t, "server") fixture.setProfile(t, "server")
fixture.setEnvironment(t) fixture.setEnvironment(t)
t.Setenv("THOTHCTL_FAKE_STOPPED_PS", `[{"ID":"core-id","Name":"exact-core","Service":"core","State":"exited"},{"ID":"front-id","Name":"exact-frontend","Service":"frontend","State":"exited"}]`) t.Setenv("THT_FAKE_STOPPED_PS", `[{"ID":"core-id","Name":"exact-core","Service":"core","State":"exited"},{"ID":"front-id","Name":"exact-frontend","Service":"frontend","State":"exited"}]`)
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
code := run(context.Background(), []string{ code := run(context.Background(), []string{
@@ -258,7 +300,7 @@ func TestRunLogsRedactsAnUnlabelledDeclaredSecret(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
fixture.setEnvironment(t, secretPath) fixture.setEnvironment(t, secretPath)
t.Setenv("THOTHCTL_FAKE_LOG", "fake Docker log: unlabelled-secret") t.Setenv("THT_FAKE_LOG", "fake Docker log: unlabelled-secret")
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "logs"}, &stdout, &stderr) exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "logs"}, &stdout, &stderr)
@@ -298,7 +340,7 @@ func TestRunPiLogsRedactsNestedAuthAndBundleScalars(t *testing.T) {
} }
fixture.setEnvContents(t, "PI_AUTH_FILE="+authFile+"\nTHT_SECRETS_FILE="+bundleFile+"\n") fixture.setEnvContents(t, "PI_AUTH_FILE="+authFile+"\nTHT_SECRETS_FILE="+bundleFile+"\n")
t.Setenv( t.Setenv(
"THOTHCTL_FAKE_LOG", "THT_FAKE_LOG",
"dummy-canary-pi-log-json dummy-canary-pi-log-nested "+ "dummy-canary-pi-log-json dummy-canary-pi-log-nested "+
"dummy-canary-pi-log-model dummy-canary-pi-log-dwh", "dummy-canary-pi-log-model dummy-canary-pi-log-dwh",
) )
@@ -352,7 +394,7 @@ func TestRunResolvesComposeDotenvCommentsQuotesAndInterpolationForSecretFiles(t
"DOUBLE_TOKEN_FILE=\""+doubleQuotedSecret+"\" # Compose comment\n"+ "DOUBLE_TOKEN_FILE=\""+doubleQuotedSecret+"\" # Compose comment\n"+
"SINGLE_TOKEN_FILE='"+singleQuotedSecret+"' # Compose comment\n"+ "SINGLE_TOKEN_FILE='"+singleQuotedSecret+"' # Compose comment\n"+
"INTERPOLATED_TOKEN_SOURCE=\"${SECRET_ROOT}/interpolated\"\n") "INTERPOLATED_TOKEN_SOURCE=\"${SECRET_ROOT}/interpolated\"\n")
t.Setenv("THOTHCTL_FAKE_LOG", "inline-secret double-quoted-secret single-quoted-secret interpolated-secret") t.Setenv("THT_FAKE_LOG", "inline-secret double-quoted-secret single-quoted-secret interpolated-secret")
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "logs"}, &stdout, &stderr) exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "logs"}, &stdout, &stderr)
@@ -374,9 +416,9 @@ func TestRunRedactsSecretSourceInBothStreams(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
fixture.setEnvContents(t, "UNLABELLED_SECRET_SOURCE="+secretPath+"\n") fixture.setEnvContents(t, "UNLABELLED_SECRET_SOURCE="+secretPath+"\n")
t.Setenv("THOTHCTL_FAKE_LOG", "stdout source-secret") t.Setenv("THT_FAKE_LOG", "stdout source-secret")
t.Setenv("THOTHCTL_FAKE_FAILURE", "stderr source-secret") t.Setenv("THT_FAKE_FAILURE", "stderr source-secret")
t.Setenv("THOTHCTL_FAKE_EXIT", "17") t.Setenv("THT_FAKE_EXIT", "17")
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "logs"}, &stdout, &stderr) exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "logs"}, &stdout, &stderr)
@@ -396,8 +438,8 @@ func TestRunRedactsSecretWhenDoctorFails(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
fixture.setEnvContents(t, "DOCTOR_SECRET_FILE="+secretPath+"\n") fixture.setEnvContents(t, "DOCTOR_SECRET_FILE="+secretPath+"\n")
t.Setenv("THOTHCTL_FAKE_FAIL_ON", "version") t.Setenv("THT_FAKE_FAIL_ON", "version")
t.Setenv("THOTHCTL_FAKE_FAILURE", "doctor saw doctor-secret") t.Setenv("THT_FAKE_FAILURE", "doctor saw doctor-secret")
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "doctor"}, &stdout, &stderr) exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "doctor"}, &stdout, &stderr)
@@ -668,7 +710,7 @@ func TestRunDoctorAcceptsComposeJSONLinesServiceStatus(t *testing.T) {
fixture := newCLIFixture(t, "SAFE_VALUE=1\n") fixture := newCLIFixture(t, "SAFE_VALUE=1\n")
fixture.setEnvironment(t) fixture.setEnvironment(t)
t.Setenv( t.Setenv(
"THOTHCTL_FAKE_PS", "THT_FAKE_PS",
"{\"Service\":\"core\",\"State\":\"running\",\"Health\":\"healthy\"}\n"+ "{\"Service\":\"core\",\"State\":\"running\",\"Health\":\"healthy\"}\n"+
"{\"Service\":\"frontend\",\"State\":\"running\",\"Health\":\"healthy\"}", "{\"Service\":\"frontend\",\"State\":\"running\",\"Health\":\"healthy\"}",
) )
@@ -687,7 +729,7 @@ func TestRunDoctorAcceptsComposeJSONLinesServiceStatus(t *testing.T) {
func TestRunPreservesChildExitCodes(t *testing.T) { func TestRunPreservesChildExitCodes(t *testing.T) {
fixture := newCLIFixture(t, "SAFE_VALUE=1\n") fixture := newCLIFixture(t, "SAFE_VALUE=1\n")
fixture.setEnvironment(t) fixture.setEnvironment(t)
t.Setenv("THOTHCTL_FAKE_EXIT", "42") t.Setenv("THT_FAKE_EXIT", "42")
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "update", "--check-only"}, &stdout, &stderr) exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "update", "--check-only"}, &stdout, &stderr)
@@ -757,7 +799,7 @@ func TestRunPiRestartSanitizesSuccessOutput(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
fixture.setEnvironment(t, secretPath) fixture.setEnvironment(t, secretPath)
t.Setenv("THOTHCTL_FAKE_PI_VERSION", "pi-restart-secret") t.Setenv("THT_FAKE_PI_VERSION", "pi-restart-secret")
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
exitCode := run(context.Background(), []string{ exitCode := run(context.Background(), []string{
@@ -886,7 +928,7 @@ func TestRunPiMaintenanceRecoverClearsRestartLifecycleState(t *testing.T) {
func TestRunWorkspaceInspectDispatchesThroughTheMaintenanceService(t *testing.T) { func TestRunWorkspaceInspectDispatchesThroughTheMaintenanceService(t *testing.T) {
fixture := newCLIFixture(t, "SAFE_VALUE=1\n") fixture := newCLIFixture(t, "SAFE_VALUE=1\n")
fixture.setEnvironment(t) fixture.setEnvironment(t)
t.Setenv("THOTHCTL_FAKE_WORKSPACE_RESULT", `{"schemaVersion":1,"status":"succeeded","code":"ok","workspaceId":"abc","workspaceRevision":"1234567890abcdef1234567890abcdef12345678","descriptorBlob":"sha256:`+strings.Repeat("a", 64)+`","operation":"inspect","completedStages":[]}`) t.Setenv("THT_FAKE_WORKSPACE_RESULT", `{"schemaVersion":1,"status":"succeeded","code":"ok","workspaceId":"abc","workspaceRevision":"1234567890abcdef1234567890abcdef12345678","descriptorBlob":"sha256:`+strings.Repeat("a", 64)+`","operation":"inspect","completedStages":[]}`)
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "workspace", "inspect", "--workspace", "abc", "--json"}, &stdout, &stderr) exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "workspace", "inspect", "--workspace", "abc", "--json"}, &stdout, &stderr)
@@ -904,7 +946,7 @@ func TestRunWorkspaceInspectDispatchesThroughTheMaintenanceService(t *testing.T)
func TestRunWorkspaceBlockedResultsExitThreeAndRenderHumanOutput(t *testing.T) { func TestRunWorkspaceBlockedResultsExitThreeAndRenderHumanOutput(t *testing.T) {
fixture := newCLIFixture(t, "SAFE_VALUE=1\n") fixture := newCLIFixture(t, "SAFE_VALUE=1\n")
fixture.setEnvironment(t) fixture.setEnvironment(t)
t.Setenv("THOTHCTL_FAKE_WORKSPACE_RESULT", `{"schemaVersion":1,"status":"blocked","code":"manual_review_required","workspaceId":"abc","workspaceRevision":"1234567890abcdef1234567890abcdef12345678","descriptorBlob":"sha256:`+strings.Repeat("b", 64)+`","operation":"schema-suggest-fks","completedStages":["dwh"],"warnings":["review required"]}`) t.Setenv("THT_FAKE_WORKSPACE_RESULT", `{"schemaVersion":1,"status":"blocked","code":"manual_review_required","workspaceId":"abc","workspaceRevision":"1234567890abcdef1234567890abcdef12345678","descriptorBlob":"sha256:`+strings.Repeat("b", 64)+`","operation":"schema-suggest-fks","completedStages":["dwh"],"warnings":["review required"]}`)
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "workspace", "schema", "suggest-fks", "--workspace", "abc"}, &stdout, &stderr) exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "workspace", "schema", "suggest-fks", "--workspace", "abc"}, &stdout, &stderr)
@@ -1012,8 +1054,8 @@ func TestRunPiStatusPreservesDockerExitCodeAndRedactsDiagnostics(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
fixture.setEnvironment(t, secretPath) fixture.setEnvironment(t, secretPath)
t.Setenv("THOTHCTL_FAKE_FAIL_ON", "version") t.Setenv("THT_FAKE_FAIL_ON", "version")
t.Setenv("THOTHCTL_FAKE_FAILURE", "pi-status-secret") t.Setenv("THT_FAKE_FAILURE", "pi-status-secret")
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "pi", "status"}, &stdout, &stderr) exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "pi", "status"}, &stdout, &stderr)
@@ -1042,7 +1084,7 @@ func newCLIFixture(t *testing.T, envTemplate string) cliFixture {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
root, err := os.MkdirTemp(temporaryRoot, "thothctl-test-") root, err := os.MkdirTemp(temporaryRoot, "tht-test-")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -1068,30 +1110,30 @@ func newCLIFixture(t *testing.T, envTemplate string) cliFixture {
} }
argsFile := filepath.Join(root, "docker-args") argsFile := filepath.Join(root, "docker-args")
fakeDocker := `#!/bin/sh fakeDocker := `#!/bin/sh
printf '%s\n' "$@" >> "$THOTHCTL_FAKE_ARGS" printf '%s\n' "$@" >> "$THT_FAKE_ARGS"
printf '%s\n' -- >> "$THOTHCTL_FAKE_ARGS" printf '%s\n' -- >> "$THT_FAKE_ARGS"
case " $* " in case " $* " in
*" ps --all --format json core frontend "*) printf '%s\n' "${THOTHCTL_FAKE_STOPPED_PS:-[]}" ;; *" ps --all --format json core frontend "*) printf '%s\n' "${THT_FAKE_STOPPED_PS:-[]}" ;;
*" config --format json "*) *" config --format json "*)
if [ -n "${THOTHCTL_FAKE_CONFIG:-}" ]; then if [ -n "${THT_FAKE_CONFIG:-}" ]; then
printf '%s\n' "$THOTHCTL_FAKE_CONFIG" printf '%s\n' "$THT_FAKE_CONFIG"
else else
printf '%s\n' '{"volumes":{"settings":{}},"services":{"core":{"image":"thothii-core:local","environment":{"THT_LLM_URL":"https://llm.example.invalid"}}}}' printf '%s\n' '{"volumes":{"settings":{}},"services":{"core":{"image":"thothii-core:local","environment":{"THT_LLM_URL":"https://llm.example.invalid"}}}}'
fi ;; fi ;;
*" run --rm --no-deps --no-TTY session-migrate "*) *" run --rm --no-deps --no-TTY session-migrate "*)
if [ "${THOTHCTL_FAKE_MIGRATION_EXIT:-0}" -ne 0 ]; then if [ "${THT_FAKE_MIGRATION_EXIT:-0}" -ne 0 ]; then
printf '%s\n' "$THOTHCTL_FAKE_MIGRATION_FAILURE" >&2 printf '%s\n' "$THT_FAKE_MIGRATION_FAILURE" >&2
exit "$THOTHCTL_FAKE_MIGRATION_EXIT" exit "$THT_FAKE_MIGRATION_EXIT"
fi fi
printf '%s\n' '{"applied":[],"drifted":[],"pending":[]}' ;; printf '%s\n' '{"applied":[],"drifted":[],"pending":[]}' ;;
*" ps --format json "*) printf '%s\n' "$THOTHCTL_FAKE_PS" ;; *" ps --format json "*) printf '%s\n' "$THT_FAKE_PS" ;;
*" ps -q core "*) printf '%s\n' 'core-id' ;; *" ps -q core "*) printf '%s\n' 'core-id' ;;
*" image inspect --format {{.Id}} "*) printf '%s\n' "${THOTHCTL_FAKE_IMAGE_ID:-sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb}" ;; *" image inspect --format {{.Id}} "*) printf '%s\n' "${THT_FAKE_IMAGE_ID:-sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb}" ;;
*"inspect --format {{.Image}} core-id"*) printf '%s\n' "${THOTHCTL_FAKE_IMAGE_ID:-sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb}" ;; *"inspect --format {{.Image}} core-id"*) printf '%s\n' "${THT_FAKE_IMAGE_ID:-sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb}" ;;
*"inspect --format {{json .Mounts}} core-id"*) printf '%s\n' '[{"Type":"volume","Name":"settings","Source":"settings","Destination":"/home/thoth/.pi","RW":true}]' ;; *"inspect --format {{json .Mounts}} core-id"*) printf '%s\n' '[{"Type":"volume","Name":"settings","Source":"settings","Destination":"/home/thoth/.pi","RW":true}]' ;;
*"io.thothii.pi.version"*) printf '%s\n' "${THOTHCTL_FAKE_PI_VERSION:-0.80.3}" ;; *"io.thothii.pi.version"*) printf '%s\n' "${THT_FAKE_PI_VERSION:-0.80.3}" ;;
*"PI_VERSION"*) printf '%s\n' "${THOTHCTL_FAKE_PI_VERSION:-0.80.3}" ;; *"PI_VERSION"*) printf '%s\n' "${THT_FAKE_PI_VERSION:-0.80.3}" ;;
*" pi --version "*) printf '%s\n' "${THOTHCTL_FAKE_PI_VERSION:-0.80.3}" ;; *" pi --version "*) printf '%s\n' "${THT_FAKE_PI_VERSION:-0.80.3}" ;;
*"/pi-management/options "*) printf '%s\n' '{"providers":["provider"],"models":[{"provider":"provider","id":"model"}],"reasoning":["low","medium","high"]}' ;; *"/pi-management/options "*) printf '%s\n' '{"providers":["provider"],"models":[{"provider":"provider","id":"model"}],"reasoning":["low","medium","high"]}' ;;
*"settings-cli.js --snapshot"*) printf '%s\n' '{"exists":false,"rawBase64":""}' ;; *"settings-cli.js --snapshot"*) printf '%s\n' '{"exists":false,"rawBase64":""}' ;;
*"/settings "*) printf '%s\n' '{"provider":"provider","model":"model","thinking":"medium"}' ;; *"/settings "*) printf '%s\n' '{"provider":"provider","model":"model","thinking":"medium"}' ;;
@@ -1100,17 +1142,17 @@ case " $* " in
*"/internal/maintenance/activate "*) printf '%s\n' '{"active":true,"admissions":0}' ;; *"/internal/maintenance/activate "*) printf '%s\n' '{"active":true,"admissions":0}' ;;
*"/internal/maintenance/deactivate "*) printf '%s\n' '{"active":false,"admissions":0}' ;; *"/internal/maintenance/deactivate "*) printf '%s\n' '{"active":false,"admissions":0}' ;;
*"/internal/maintenance/status "*) printf '%s\n' '{"active":true,"admissions":0}' ;; *"/internal/maintenance/status "*) printf '%s\n' '{"active":true,"admissions":0}' ;;
*" logs "*) printf '%s\n' "$THOTHCTL_FAKE_LOG" ;; *" logs "*) printf '%s\n' "$THT_FAKE_LOG" ;;
*" run --rm --no-deps --no-TTY "*" workspace-maintenance "*) printf '%s\n' "$THOTHCTL_FAKE_WORKSPACE_RESULT" ;; *" run --rm --no-deps --no-TTY "*" workspace-maintenance "*) printf '%s\n' "$THT_FAKE_WORKSPACE_RESULT" ;;
esac esac
if [ "${THOTHCTL_FAKE_FAIL_ON:-}" = "version" ]; then if [ "${THT_FAKE_FAIL_ON:-}" = "version" ]; then
printf '%s\n' "${THOTHCTL_FAKE_FAILURE:-fake Docker failure}" >&2 printf '%s\n' "${THT_FAKE_FAILURE:-fake Docker failure}" >&2
exit 41 exit 41
fi fi
if [ "${THOTHCTL_FAKE_EXIT:-0}" -ne 0 ]; then if [ "${THT_FAKE_EXIT:-0}" -ne 0 ]; then
printf '%s\n' "${THOTHCTL_FAKE_FAILURE:-fake Docker failure}" >&2 printf '%s\n' "${THT_FAKE_FAILURE:-fake Docker failure}" >&2
fi fi
exit "${THOTHCTL_FAKE_EXIT:-0}" exit "${THT_FAKE_EXIT:-0}"
` `
if err := os.WriteFile(filepath.Join(pathDirectory, "docker"), []byte(fakeDocker), 0o700); err != nil { if err := os.WriteFile(filepath.Join(pathDirectory, "docker"), []byte(fakeDocker), 0o700); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -1133,19 +1175,19 @@ func (f cliFixture) setEnvContents(t *testing.T, env string) {
t.Fatal(err) t.Fatal(err)
} }
t.Setenv("PATH", f.pathDirectory) t.Setenv("PATH", f.pathDirectory)
t.Setenv("THOTHCTL_FAKE_ARGS", f.argsFile) t.Setenv("THT_FAKE_ARGS", f.argsFile)
t.Setenv("THOTHCTL_FAKE_EXIT", "0") t.Setenv("THT_FAKE_EXIT", "0")
t.Setenv("THOTHCTL_FAKE_LOG", "") t.Setenv("THT_FAKE_LOG", "")
t.Setenv("THOTHCTL_FAKE_FAILURE", "") t.Setenv("THT_FAKE_FAILURE", "")
t.Setenv("THOTHCTL_FAKE_FAIL_ON", "") t.Setenv("THT_FAKE_FAIL_ON", "")
t.Setenv("THOTHCTL_FAKE_STOPPED_PS", "[]") t.Setenv("THT_FAKE_STOPPED_PS", "[]")
t.Setenv("THOTHCTL_FAKE_PS", `[{"Service":"core","State":"running","Health":"healthy"},{"Service":"frontend","State":"running","Health":"healthy"}]`) t.Setenv("THT_FAKE_PS", `[{"Service":"core","State":"running","Health":"healthy"},{"Service":"frontend","State":"running","Health":"healthy"}]`)
t.Setenv("THOTHCTL_FAKE_CONFIG", "") t.Setenv("THT_FAKE_CONFIG", "")
t.Setenv("THOTHCTL_FAKE_MIGRATION_FAILURE", "") t.Setenv("THT_FAKE_MIGRATION_FAILURE", "")
t.Setenv("THOTHCTL_FAKE_MIGRATION_EXIT", "0") t.Setenv("THT_FAKE_MIGRATION_EXIT", "0")
t.Setenv("THOTHCTL_FAKE_WORKSPACE_RESULT", "") t.Setenv("THT_FAKE_WORKSPACE_RESULT", "")
t.Setenv("THOTHCTL_FAKE_IMAGE_ID", "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") t.Setenv("THT_FAKE_IMAGE_ID", "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
t.Setenv("THOTHCTL_FAKE_PI_VERSION", "0.80.3") t.Setenv("THT_FAKE_PI_VERSION", "0.80.3")
} }
func (f cliFixture) setProfile(t *testing.T, profile string) { func (f cliFixture) setProfile(t *testing.T, profile string) {
+1 -1
View File
@@ -1,4 +1,4 @@
module github.com/aritmolab/thothii/tools/thothctl module github.com/aritmolab/thothii/tools/tht
go 1.26.0 go 1.26.0
@@ -1,4 +1,4 @@
// Package config loads the non-secret, local installation descriptor used by thothctl. // Package config loads the non-secret, local installation descriptor used by tht.
package config package config
import ( import (
@@ -15,7 +15,7 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/aritmolab/thothii/tools/thothctl/internal/safeio" "github.com/aritmolab/thothii/tools/tht/internal/safeio"
"github.com/compose-spec/compose-go/v2/dotenv" "github.com/compose-spec/compose-go/v2/dotenv"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@@ -235,7 +235,7 @@ func (i Installation) ComposeFiles() []string {
// ControlDirectory contains state that is private to one installation descriptor, even when // ControlDirectory contains state that is private to one installation descriptor, even when
// multiple installations intentionally share one source checkout. // multiple installations intentionally share one source checkout.
func (i Installation) ControlDirectory() string { func (i Installation) ControlDirectory() string {
return filepath.Join(i.ProjectDirectory, ".thothctl", i.ProjectName()) return filepath.Join(i.ProjectDirectory, ".tht", i.ProjectName())
} }
func (i Installation) CurrentImageOverridePath() string { func (i Installation) CurrentImageOverridePath() string {
@@ -284,7 +284,7 @@ func (i Installation) composeArgs(files []string, command ...string) []string {
// SecretFiles returns canonical local secret paths declared through *_FILE or *_SOURCE variables. // SecretFiles returns canonical local secret paths declared through *_FILE or *_SOURCE variables.
// Compose's dotenv parser resolves comments, quotes, escapes, and interpolation. Unsupported or // Compose's dotenv parser resolves comments, quotes, escapes, and interpolation. Unsupported or
// unresolved source interpolation is rejected before thothctl invokes Docker. // unresolved source interpolation is rejected before tht invokes Docker.
func (i Installation) SecretFiles() ([]string, error) { func (i Installation) SecretFiles() ([]string, error) {
contents, err := safeio.ReadCanonicalRegular(i.EnvFile, maxEnvironmentFileBytes) contents, err := safeio.ReadCanonicalRegular(i.EnvFile, maxEnvironmentFileBytes)
if err != nil { if err != nil {
@@ -218,8 +218,8 @@ func TestInstallationControlPathsAreIsolatedForDescriptorsSharingOneCheckout(t *
t.Fatalf("shared-checkout installations reused %q", first.CurrentImageOverridePath()) t.Fatalf("shared-checkout installations reused %q", first.CurrentImageOverridePath())
} }
for _, installation := range []Installation{first, second} { for _, installation := range []Installation{first, second} {
if filepath.Dir(filepath.Dir(installation.CurrentImageOverridePath())) != filepath.Join(projectDirectory, ".thothctl") { if filepath.Dir(filepath.Dir(installation.CurrentImageOverridePath())) != filepath.Join(projectDirectory, ".tht") {
t.Fatalf("current-image path %q is not installation-specific under .thothctl", installation.CurrentImageOverridePath()) t.Fatalf("current-image path %q is not installation-specific under .tht", installation.CurrentImageOverridePath())
} }
if filepath.Dir(installation.UpdateStatePath()) != filepath.Dir(installation.CurrentImageOverridePath()) { if filepath.Dir(installation.UpdateStatePath()) != filepath.Dir(installation.CurrentImageOverridePath()) {
t.Fatalf("state %q and selector %q do not share one installation control directory", installation.UpdateStatePath(), installation.CurrentImageOverridePath()) t.Fatalf("state %q and selector %q do not share one installation control directory", installation.UpdateStatePath(), installation.CurrentImageOverridePath())
@@ -246,7 +246,7 @@ func writeInstallation(t *testing.T, profile string) (string, string, string, st
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
physicalRoot, err := os.MkdirTemp(temporaryRoot, "thothctl-config-") physicalRoot, err := os.MkdirTemp(temporaryRoot, "tht-config-")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -10,7 +10,7 @@ import (
"sort" "sort"
"strings" "strings"
"github.com/aritmolab/thothii/tools/thothctl/internal/safeio" "github.com/aritmolab/thothii/tools/tht/internal/safeio"
"github.com/compose-spec/compose-go/v2/dotenv" "github.com/compose-spec/compose-go/v2/dotenv"
) )
@@ -161,7 +161,7 @@ func physicalTempDir(t *testing.T) string {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
directory, err := os.MkdirTemp(root, "thothctl-output-test-") directory, err := os.MkdirTemp(root, "tht-output-test-")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -12,7 +12,7 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/aritmolab/thothii/tools/thothctl/internal/compose" "github.com/aritmolab/thothii/tools/tht/internal/compose"
) )
var choicePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$`) var choicePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$`)
@@ -40,9 +40,9 @@ type settingsFileSnapshot struct {
} }
var internalIdentityHeaders = []string{ var internalIdentityHeaders = []string{
"-H", "x-thoth-principal-issuer: thothctl", "-H", "x-thoth-principal-issuer: tht",
"-H", "x-thoth-principal-subject: thothctl-maintenance", "-H", "x-thoth-principal-subject: tht-maintenance",
"-H", "x-thoth-principal-display-name: Thothctl maintenance", "-H", "x-thoth-principal-display-name: Tht maintenance",
"-H", "x-thoth-is-admin: 1", "-H", "x-thoth-is-admin: 1",
} }
@@ -221,7 +221,7 @@ func readEffectiveSettings(ctx context.Context, runner Runner) ([]byte, error) {
return canonical, nil return canonical, nil
} }
// Runner is the narrow, shell-free command boundary shared with thothctl. // Runner is the narrow, shell-free command boundary shared with tht.
type Runner interface { type Runner interface {
Run(context.Context, []string, io.Reader) (compose.Result, error) Run(context.Context, []string, io.Reader) (compose.Result, error)
} }
@@ -9,7 +9,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/aritmolab/thothii/tools/thothctl/internal/compose" "github.com/aritmolab/thothii/tools/tht/internal/compose"
) )
func TestDoctorRequiresExternalEndpointAuthPiStateAndHealth(t *testing.T) { func TestDoctorRequiresExternalEndpointAuthPiStateAndHealth(t *testing.T) {
@@ -218,7 +218,7 @@ func TestConfigureCompensationRestoresAbsentAndExactEmptyPriorFiles(t *testing.T
} }
} }
// Catches thothctl reading the legacy public model route instead of the admin-only closed Pi // Catches tht reading the legacy public model route instead of the admin-only closed Pi
// Management choices before it writes shared installation defaults. // Management choices before it writes shared installation defaults.
func TestConfigureLoadsDedicatedClosedOptionsWritesRealCoreSettingsAndUsesUpstreamIdentity(t *testing.T) { func TestConfigureLoadsDedicatedClosedOptionsWritesRealCoreSettingsAndUsesUpstreamIdentity(t *testing.T) {
fake := newFakeRunner() fake := newFakeRunner()
@@ -227,7 +227,7 @@ func TestConfigureLoadsDedicatedClosedOptionsWritesRealCoreSettingsAndUsesUpstre
} }
assertCalled(t, fake.calls, "/pi-management/options") assertCalled(t, fake.calls, "/pi-management/options")
assertCalled(t, fake.calls, "node /app/backend/dist/settings/settings-cli.js --provider provider --model model --thinking medium") assertCalled(t, fake.calls, "node /app/backend/dist/settings/settings-cli.js --provider provider --model model --thinking medium")
assertCalled(t, fake.calls, "x-thoth-principal-subject: thothctl-maintenance") assertCalled(t, fake.calls, "x-thoth-principal-subject: tht-maintenance")
if got := strings.Join(fake.calls, "\n"); strings.Contains(got, "pi-defaults.json") || strings.Contains(got, "secret") { if got := strings.Join(fake.calls, "\n"); strings.Contains(got, "pi-defaults.json") || strings.Contains(got, "secret") {
t.Fatalf("commands=%q", got) t.Fatalf("commands=%q", got)
} }
@@ -243,7 +243,7 @@ func TestTestUsesDedicatedSmokeEndpointAndIndependentImageVersionProbe(t *testin
if err := Test(context.Background(), fake); err != nil { if err := Test(context.Background(), fake); err != nil {
t.Fatalf("Test() error = %v", err) t.Fatalf("Test() error = %v", err)
} }
for _, command := range []string{"pi --version", "/pi-management/test", "x-thoth-principal-subject: thothctl-maintenance"} { for _, command := range []string{"pi --version", "/pi-management/test", "x-thoth-principal-subject: tht-maintenance"} {
assertCalled(t, fake.calls, command) assertCalled(t, fake.calls, command)
} }
for _, legacy := range []string{"/health", "/models", "/settings"} { for _, legacy := range []string{"/health", "/models", "/settings"} {
@@ -271,7 +271,7 @@ func TestTestRequiresDedicatedSmokeEndpointToReportReady(t *testing.T) {
assertCalled(t, fake.calls, "pi --version") assertCalled(t, fake.calls, "pi --version")
} }
// Catches thothctl accepting a reasoning level that the backend did not publish as a closed // Catches tht accepting a reasoning level that the backend did not publish as a closed
// installation option, which would bypass the Pi Management validation surface. // installation option, which would bypass the Pi Management validation surface.
func TestConfigureRejectsReasoningOutsideDedicatedClosedOptions(t *testing.T) { func TestConfigureRejectsReasoningOutsideDedicatedClosedOptions(t *testing.T) {
fake := newFakeRunner() fake := newFakeRunner()
@@ -93,7 +93,7 @@ func TestRestartPinsCapturedImageWhenConfiguredTagMovesBeforeRecreate(t *testing
if result.Version != "0.80.3" || fake.currentImage != "sha256:old" { if result.Version != "0.80.3" || fake.currentImage != "sha256:old" {
t.Fatalf("restart result=%+v image=%q; want captured 0.80.3 / sha256:old", result, fake.currentImage) t.Fatalf("restart result=%+v image=%q; want captured 0.80.3 / sha256:old", result, fake.currentImage)
} }
assertCalled(t, fake.calls, "image tag sha256:old thothii-core:thothctl-") assertCalled(t, fake.calls, "image tag sha256:old thothii-core:tht-")
assertCalled(t, fake.calls, "pi-lifecycle-") assertCalled(t, fake.calls, "pi-lifecycle-")
if matches, globErr := filepath.Glob(filepath.Join(dir, "pi-lifecycle-*.yaml")); globErr != nil || len(matches) != 0 { if matches, globErr := filepath.Glob(filepath.Join(dir, "pi-lifecycle-*.yaml")); globErr != nil || len(matches) != 0 {
t.Fatalf("successful restart overrides = %v, error = %v; want safe cleanup", matches, globErr) t.Fatalf("successful restart overrides = %v, error = %v; want safe cleanup", matches, globErr)
@@ -285,7 +285,7 @@ func TestRecoverLifecycleMaintenanceVerifiesAndClearsRestartState(t *testing.T)
delete(fake.tags, restartState.Previous.Reference) delete(fake.tags, restartState.Previous.Reference)
fake.tags[fake.configuredImage] = "sha256:moved-before-recovery" fake.tags[fake.configuredImage] = "sha256:moved-before-recovery"
candidate := previous candidate := previous
candidate.Reference = "thothii-core:thothctl-recover-candidate" candidate.Reference = "thothii-core:tht-recover-candidate"
writeStateForTest(t, updateStatePath, State{ writeStateForTest(t, updateStatePath, State{
Transaction: "update-recovery", Transaction: "update-recovery",
Phase: PhasePromoting, Phase: PhasePromoting,
@@ -40,15 +40,15 @@ func TestUpdateAndRestartStatePathsShareOneLifecycleLock(t *testing.T) {
func TestAdvisoryLockCrashReleasesAndReacquires(t *testing.T) { func TestAdvisoryLockCrashReleasesAndReacquires(t *testing.T) {
statePath := filepath.Join(t.TempDir(), "update-state.json") statePath := filepath.Join(t.TempDir(), "update-state.json")
if os.Getenv("THOTHCTL_LOCK_CRASH_HELPER") == "1" { if os.Getenv("THT_LOCK_CRASH_HELPER") == "1" {
lock, err := acquireLock(os.Getenv("THOTHCTL_LOCK_STATE")) lock, err := acquireLock(os.Getenv("THT_LOCK_STATE"))
if err != nil || lock == nil { if err != nil || lock == nil {
os.Exit(23) os.Exit(23)
} }
os.Exit(0) // Deliberately bypass Release: the OS must release ownership. os.Exit(0) // Deliberately bypass Release: the OS must release ownership.
} }
command := exec.Command(os.Args[0], "-test.run=^TestAdvisoryLockCrashReleasesAndReacquires$") command := exec.Command(os.Args[0], "-test.run=^TestAdvisoryLockCrashReleasesAndReacquires$")
command.Env = append(os.Environ(), "THOTHCTL_LOCK_CRASH_HELPER=1", "THOTHCTL_LOCK_STATE="+statePath) command.Env = append(os.Environ(), "THT_LOCK_CRASH_HELPER=1", "THT_LOCK_STATE="+statePath)
if output, err := command.CombinedOutput(); err != nil { if output, err := command.CombinedOutput(); err != nil {
t.Fatalf("crash helper failed: %v: %s", err, output) t.Fatalf("crash helper failed: %v: %s", err, output)
} }
@@ -15,7 +15,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/aritmolab/thothii/tools/thothctl/internal/compose" "github.com/aritmolab/thothii/tools/tht/internal/compose"
"github.com/distribution/reference" "github.com/distribution/reference"
) )
@@ -748,7 +748,7 @@ func lifecycleTransaction(statePath string) string {
} }
func lifecycleImageTag(transaction, role string) string { func lifecycleImageTag(transaction, role string) string {
return "thothii-core:thothctl-" + transaction + "-" + role return "thothii-core:tht-" + transaction + "-" + role
} }
func lifecycleOverridePath(statePath, transaction string) string { func lifecycleOverridePath(statePath, transaction string) string {
@@ -11,7 +11,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/aritmolab/thothii/tools/thothctl/internal/compose" "github.com/aritmolab/thothii/tools/tht/internal/compose"
) )
func TestActiveSessionsParsesAuthenticatedBackendBareArrayFixture(t *testing.T) { func TestActiveSessionsParsesAuthenticatedBackendBareArrayFixture(t *testing.T) {
@@ -58,7 +58,7 @@ func TestUpdateBuildsPinnedVersionRecreatesOnlyCoreAndPersistsRecoveryState(t *t
fake := newFakeRunner() fake := newFakeRunner()
dir := t.TempDir() dir := t.TempDir()
result, err := Update(context.Background(), fake, Request{ result, err := Update(context.Background(), fake, Request{
StatePath: filepath.Join(dir, ".thothctl", "update-state.json"), StatePath: filepath.Join(dir, ".tht", "update-state.json"),
Version: "0.81.0", Version: "0.81.0",
Source: BuildSource, Source: BuildSource,
Confirm: true, Confirm: true,
@@ -85,11 +85,11 @@ func TestUpdateBuildsPinnedVersionRecreatesOnlyCoreAndPersistsRecoveryState(t *t
func TestUpdateUsesATransactionScopedComposeOverrideWithoutMutatingTheConfiguredImage(t *testing.T) { func TestUpdateUsesATransactionScopedComposeOverrideWithoutMutatingTheConfiguredImage(t *testing.T) {
fake := newFakeRunner() fake := newFakeRunner()
statePath := filepath.Join(t.TempDir(), ".thothctl", "update-state.json") statePath := filepath.Join(t.TempDir(), ".tht", "update-state.json")
if _, err := Update(context.Background(), fake, Request{StatePath: statePath, Version: "0.81.0", Source: BuildSource, Confirm: true}); err != nil { if _, err := Update(context.Background(), fake, Request{StatePath: statePath, Version: "0.81.0", Source: BuildSource, Confirm: true}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if fake.buildReference == "" || fake.buildReference == fake.configuredImage || !strings.Contains(fake.buildReference, "thothctl-") { if fake.buildReference == "" || fake.buildReference == fake.configuredImage || !strings.Contains(fake.buildReference, "tht-") {
t.Fatalf("build reference = %q, configured = %q; want unique lifecycle tag", fake.buildReference, fake.configuredImage) t.Fatalf("build reference = %q, configured = %q; want unique lifecycle tag", fake.buildReference, fake.configuredImage)
} }
assertNotCalled(t, fake.calls, "image tag sha256:old "+fake.configuredImage) assertNotCalled(t, fake.calls, "image tag sha256:old "+fake.configuredImage)
@@ -103,7 +103,7 @@ func TestUpdateUsesATransactionScopedComposeOverrideWithoutMutatingTheConfigured
func TestSuccessfulUpdateAndRollbackRemainSelectedOnFreshRecreate(t *testing.T) { func TestSuccessfulUpdateAndRollbackRemainSelectedOnFreshRecreate(t *testing.T) {
fake := newFakeRunner() fake := newFakeRunner()
statePath := filepath.Join(t.TempDir(), ".thothctl", "update-state.json") statePath := filepath.Join(t.TempDir(), ".tht", "update-state.json")
if _, err := Update(context.Background(), fake, Request{StatePath: statePath, Version: "0.81.0", Source: BuildSource, Confirm: true}); err != nil { if _, err := Update(context.Background(), fake, Request{StatePath: statePath, Version: "0.81.0", Source: BuildSource, Confirm: true}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -205,7 +205,7 @@ func TestDigestPinnedConfiguredImageIsNeverUsedAsARollbackTagTarget(t *testing.T
fake := newFakeRunner() fake := newFakeRunner()
fake.configuredImage = "registry.example.invalid/core@sha256:" + strings.Repeat("b", 64) fake.configuredImage = "registry.example.invalid/core@sha256:" + strings.Repeat("b", 64)
fake.tags = map[string]string{fake.configuredImage: "sha256:old"} fake.tags = map[string]string{fake.configuredImage: "sha256:old"}
statePath := filepath.Join(t.TempDir(), ".thothctl", "state.json") statePath := filepath.Join(t.TempDir(), ".tht", "state.json")
if _, err := Update(context.Background(), fake, Request{StatePath: statePath, Version: "0.81.0", Source: BuildSource, Confirm: true}); err != nil { if _, err := Update(context.Background(), fake, Request{StatePath: statePath, Version: "0.81.0", Source: BuildSource, Confirm: true}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -343,7 +343,7 @@ func TestCompensationReactivatesMaintenanceAndRescansBeforeRollback(t *testing.T
func TestAutomaticRollbackSurvivesADeadCandidateCore(t *testing.T) { func TestAutomaticRollbackSurvivesADeadCandidateCore(t *testing.T) {
fake := newFakeRunner() fake := newFakeRunner()
fake.fail = "dead-candidate" fake.fail = "dead-candidate"
statePath := filepath.Join(t.TempDir(), ".thothctl", "update-state.json") statePath := filepath.Join(t.TempDir(), ".tht", "update-state.json")
result, err := Update(context.Background(), fake, Request{ result, err := Update(context.Background(), fake, Request{
StatePath: statePath, StatePath: statePath,
@@ -367,9 +367,9 @@ func TestAutomaticRollbackSurvivesADeadCandidateCore(t *testing.T) {
func TestManualRollbackSurvivesADeadCandidateCore(t *testing.T) { func TestManualRollbackSurvivesADeadCandidateCore(t *testing.T) {
fake := newFakeRunner() fake := newFakeRunner()
statePath := filepath.Join(t.TempDir(), ".thothctl", "update-state.json") statePath := filepath.Join(t.TempDir(), ".tht", "update-state.json")
previous := stateImageForTest(t, fake) previous := stateImageForTest(t, fake)
previous.Reference = "thothii-core:thothctl-dead-candidate-previous" previous.Reference = "thothii-core:tht-dead-candidate-previous"
fake.tags[previous.Reference] = previous.ID fake.tags[previous.Reference] = previous.ID
writeStateForTest(t, statePath, State{ writeStateForTest(t, statePath, State{
Transaction: "dead-candidate", Transaction: "dead-candidate",
@@ -405,7 +405,7 @@ func TestUpdatePullsOnlyDigestPinnedSource(t *testing.T) {
t.Fatalf("Update() pull error = %v", err) t.Fatalf("Update() pull error = %v", err)
} }
assertCalled(t, fake.calls, "pull "+digest) assertCalled(t, fake.calls, "pull "+digest)
assertCalled(t, fake.calls, "image tag "+digest+" thothii-core:thothctl-") assertCalled(t, fake.calls, "image tag "+digest+" thothii-core:tht-")
assertNotCalled(t, fake.calls, "image tag "+digest+" thothii-core:local") assertNotCalled(t, fake.calls, "image tag "+digest+" thothii-core:local")
fake = newFakeRunner() fake = newFakeRunner()
@@ -442,7 +442,7 @@ func TestUpdateRollsBackAfterPostRecreateFailures(t *testing.T) {
if result.Phase != PhaseRolledBack { if result.Phase != PhaseRolledBack {
t.Fatalf("phase = %q, want %q", result.Phase, PhaseRolledBack) t.Fatalf("phase = %q, want %q", result.Phase, PhaseRolledBack)
} }
assertCalled(t, fake.calls, "image tag sha256:old thothii-core:thothctl-") assertCalled(t, fake.calls, "image tag sha256:old thothii-core:tht-")
assertNotCalled(t, fake.calls, "image tag sha256:old thothii-core:local") assertNotCalled(t, fake.calls, "image tag sha256:old thothii-core:local")
assertCalled(t, fake.calls, "up --detach --wait --wait-timeout 45 --no-deps --force-recreate core") assertCalled(t, fake.calls, "up --detach --wait --wait-timeout 45 --no-deps --force-recreate core")
if got := string(readStateBytes(t, statePath)); !strings.Contains(got, `"phase": "rolled_back"`) { if got := string(readStateBytes(t, statePath)); !strings.Contains(got, `"phase": "rolled_back"`) {
@@ -613,11 +613,11 @@ func TestRecoverMaintenanceCompletesAnInterruptedDurablePromotion(t *testing.T)
fake.version = "0.81.0" fake.version = "0.81.0"
fake.expectedVersion = "0.81.0" fake.expectedVersion = "0.81.0"
fake.labelVersion = "0.81.0" fake.labelVersion = "0.81.0"
statePath := filepath.Join(t.TempDir(), ".thothctl", "update-state.json") statePath := filepath.Join(t.TempDir(), ".tht", "update-state.json")
previous := stateImageForTest(t, newFakeRunner()) previous := stateImageForTest(t, newFakeRunner())
candidate := previous candidate := previous
candidate.ID = "sha256:candidate" candidate.ID = "sha256:candidate"
candidate.Reference = "thothii-core:thothctl-recover-candidate" candidate.Reference = "thothii-core:tht-recover-candidate"
fake.tags[candidate.Reference] = candidate.ID fake.tags[candidate.Reference] = candidate.ID
state := State{ state := State{
Transaction: "promotion-recovery", Transaction: "promotion-recovery",
@@ -647,7 +647,7 @@ func TestRollbackFinalStateWriteFailureKeepsMaintenanceAndOverrideForRecovery(t
fake := newFakeRunner() fake := newFakeRunner()
statePath := filepath.Join(t.TempDir(), "state.json") statePath := filepath.Join(t.TempDir(), "state.json")
previous := stateImageForTest(t, fake) previous := stateImageForTest(t, fake)
previous.Reference = "thothii-core:thothctl-rollback-test-previous" previous.Reference = "thothii-core:tht-rollback-test-previous"
fake.tags[previous.Reference] = previous.ID fake.tags[previous.Reference] = previous.ID
writeStateForTest(t, statePath, State{ writeStateForTest(t, statePath, State{
Transaction: "rollback-test", Transaction: "rollback-test",
@@ -700,7 +700,7 @@ func TestCandidateBuildAndPullFailuresRemainPreMutationAndNeverRecreateCore(t *t
t.Run(testCase.name, func(t *testing.T) { t.Run(testCase.name, func(t *testing.T) {
fake := newFakeRunner() fake := newFakeRunner()
fake.fail = testCase.failure fake.fail = testCase.failure
statePath := filepath.Join(t.TempDir(), ".thothctl", "update-state.json") statePath := filepath.Join(t.TempDir(), ".tht", "update-state.json")
result, err := Update(context.Background(), fake, Request{StatePath: statePath, Version: "0.81.0", Source: testCase.source, Image: testCase.image, Confirm: true}) result, err := Update(context.Background(), fake, Request{StatePath: statePath, Version: "0.81.0", Source: testCase.source, Image: testCase.image, Confirm: true})
if err == nil { if err == nil {
t.Fatal("Update() error = nil, want preparation failure") t.Fatal("Update() error = nil, want preparation failure")
@@ -727,7 +727,7 @@ func TestSuccessfulCompensationPreservesTheOriginalTypedCause(t *testing.T) {
for _, cause := range []error{ErrActiveSessions, ErrInterruptedUpdate} { for _, cause := range []error{ErrActiveSessions, ErrInterruptedUpdate} {
fake := newFakeRunner() fake := newFakeRunner()
fake.maintenance = true fake.maintenance = true
statePath := filepath.Join(t.TempDir(), ".thothctl", "update-state.json") statePath := filepath.Join(t.TempDir(), ".tht", "update-state.json")
state := State{Transaction: "typed-cause", Phase: PhaseRecreated, MutationStarted: true, Previous: stateImageForTest(t, fake)} state := State{Transaction: "typed-cause", Phase: PhaseRecreated, MutationStarted: true, Previous: stateImageForTest(t, fake)}
result, err, clear := compensate(context.Background(), fake, statePath, lifecycleOverridePath(statePath, state.Transaction), state, cause, defaultLifecycleHooks) result, err, clear := compensate(context.Background(), fake, statePath, lifecycleOverridePath(statePath, state.Transaction), state, cause, defaultLifecycleHooks)
if result.Phase != PhaseRolledBack || !clear { if result.Phase != PhaseRolledBack || !clear {
@@ -780,7 +780,7 @@ func TestRollbackRestoresInterruptedOrPreviouslyRecordedState(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
previous.ConfigurationSHA = configured.ConfigurationSHA previous.ConfigurationSHA = configured.ConfigurationSHA
previous.Reference = "thothii-core:thothctl-test-previous" previous.Reference = "thothii-core:tht-test-previous"
fake.tags[previous.Reference] = previous.ID fake.tags[previous.Reference] = previous.ID
writeStateForTest(t, statePath, State{Version: 1, Phase: PhaseRecreated, Previous: previous}) writeStateForTest(t, statePath, State{Version: 1, Phase: PhaseRecreated, Previous: previous})
result, err := Rollback(context.Background(), fake, statePath, pairedRestartStatePath(statePath), true) result, err := Rollback(context.Background(), fake, statePath, pairedRestartStatePath(statePath), true)
@@ -790,7 +790,7 @@ func TestRollbackRestoresInterruptedOrPreviouslyRecordedState(t *testing.T) {
if result.Phase != PhaseRolledBack { if result.Phase != PhaseRolledBack {
t.Fatalf("phase = %q, want %q", result.Phase, PhaseRolledBack) t.Fatalf("phase = %q, want %q", result.Phase, PhaseRolledBack)
} }
assertCalled(t, fake.calls, "image tag sha256:old thothii-core:thothctl-test-previous") assertCalled(t, fake.calls, "image tag sha256:old thothii-core:tht-test-previous")
assertCalled(t, fake.calls, "up --detach --wait --wait-timeout 45 --no-deps --force-recreate core") assertCalled(t, fake.calls, "up --detach --wait --wait-timeout 45 --no-deps --force-recreate core")
} }
@@ -1349,6 +1349,6 @@ func stateImageForTest(t *testing.T, fake *fakeRunner) Image {
t.Fatal(err) t.Fatal(err)
} }
image.ConfigurationSHA = configured.ConfigurationSHA image.ConfigurationSHA = configured.ConfigurationSHA
image.Reference = "thothii-core:thothctl-test-previous" image.Reference = "thothii-core:tht-test-previous"
return image return image
} }
@@ -6,7 +6,7 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/aritmolab/thothii/tools/thothctl/internal/testsupport" "github.com/aritmolab/thothii/tools/tht/internal/testsupport"
) )
func TestReadCanonicalRegularRejectsFinalAndParentSymlinks(t *testing.T) { func TestReadCanonicalRegularRejectsFinalAndParentSymlinks(t *testing.T) {
@@ -14,7 +14,7 @@ func TestReadCanonicalRegularRejectsFinalAndParentSymlinks(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
root, err := os.MkdirTemp(temporaryRoot, "thothctl-safeio-") root, err := os.MkdirTemp(temporaryRoot, "tht-safeio-")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -47,7 +47,7 @@ func TestReadCanonicalUTF8RejectsNonUTF8AndHardlinks(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
root, err := os.MkdirTemp(temporaryRoot, "thothctl-safeio-") root, err := os.MkdirTemp(temporaryRoot, "tht-safeio-")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -79,7 +79,7 @@ func TestWriteCanonicalNewFileRejectsExistingTargets(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
root, err := os.MkdirTemp(temporaryRoot, "thothctl-safeio-") root, err := os.MkdirTemp(temporaryRoot, "tht-safeio-")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -41,7 +41,7 @@ func ReadCanonicalRegular(path string, maximum int64) ([]byte, error) {
if err != nil { if err != nil {
return nil, ErrUnsafeFile return nil, ErrUnsafeFile
} }
file := os.NewFile(uintptr(descriptor), "thothctl-safeio") file := os.NewFile(uintptr(descriptor), "tht-safeio")
if file == nil { if file == nil {
unix.Close(descriptor) unix.Close(descriptor)
return nil, ErrUnsafeFile return nil, ErrUnsafeFile
@@ -16,7 +16,7 @@ func TestReadCanonicalRegularRejectsNamedPipeWithoutBlocking(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
root, err := os.MkdirTemp(temporaryRoot, "thothctl-safeio-") root, err := os.MkdirTemp(temporaryRoot, "tht-safeio-")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -47,7 +47,7 @@ func ReadCanonicalRegular(path string, maximum int64) ([]byte, error) {
if err != nil { if err != nil {
return nil, ErrUnsafeFile return nil, ErrUnsafeFile
} }
file := os.NewFile(uintptr(handle), "thothctl-safeio") file := os.NewFile(uintptr(handle), "tht-safeio")
if file == nil { if file == nil {
windows.CloseHandle(handle) windows.CloseHandle(handle)
return nil, ErrUnsafeFile return nil, ErrUnsafeFile
@@ -12,8 +12,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/aritmolab/thothii/tools/thothctl/internal/compose" "github.com/aritmolab/thothii/tools/tht/internal/compose"
"github.com/aritmolab/thothii/tools/thothctl/internal/config" "github.com/aritmolab/thothii/tools/tht/internal/config"
) )
var ( var (
@@ -11,8 +11,8 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/aritmolab/thothii/tools/thothctl/internal/compose" "github.com/aritmolab/thothii/tools/tht/internal/compose"
"github.com/aritmolab/thothii/tools/thothctl/internal/config" "github.com/aritmolab/thothii/tools/tht/internal/config"
) )
type fakeRunner struct { type fakeRunner struct {
@@ -298,7 +298,7 @@ func testInstallation(t *testing.T) config.Installation {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
root, err := os.MkdirTemp(temporaryRoot, "thothctl-serverops-") root, err := os.MkdirTemp(temporaryRoot, "tht-serverops-")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -1,4 +1,4 @@
// Package testsupport provides portable helpers shared by thothctl tests. // Package testsupport provides portable helpers shared by tht tests.
package testsupport package testsupport
import ( import (
@@ -15,9 +15,9 @@ import (
"sort" "sort"
"strings" "strings"
"github.com/aritmolab/thothii/tools/thothctl/internal/compose" "github.com/aritmolab/thothii/tools/tht/internal/compose"
"github.com/aritmolab/thothii/tools/thothctl/internal/config" "github.com/aritmolab/thothii/tools/tht/internal/config"
"github.com/aritmolab/thothii/tools/thothctl/internal/safeio" "github.com/aritmolab/thothii/tools/tht/internal/safeio"
) )
var ( var (
@@ -12,8 +12,8 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/aritmolab/thothii/tools/thothctl/internal/compose" "github.com/aritmolab/thothii/tools/tht/internal/compose"
"github.com/aritmolab/thothii/tools/thothctl/internal/config" "github.com/aritmolab/thothii/tools/tht/internal/config"
) )
type fakeRunner struct { type fakeRunner struct {
@@ -167,7 +167,7 @@ func testInstallation(t *testing.T) config.Installation {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
root, err := os.MkdirTemp(temporaryRoot, "thothctl-workspaceops-") root, err := os.MkdirTemp(temporaryRoot, "tht-workspaceops-")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }