fix: stabilize runtime workspace identity
This commit is contained in:
@@ -129,6 +129,7 @@ export function renderRuntimeConfig(
|
||||
runtime_identity: {
|
||||
workspace_id: identity.workspaceId,
|
||||
workspace_revision: identity.workspaceRevision,
|
||||
source_identity: `workspace://${identity.workspaceId}`,
|
||||
},
|
||||
} : {}),
|
||||
...(installation.session_storage === undefined
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { chmodSync, existsSync, mkdtempSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import {
|
||||
chmodSync, existsSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
import { buildApp } from "../src/app.js";
|
||||
import { loadConfig } from "../src/config.js";
|
||||
import { ThtRunner } from "../src/tht/tht-runner.js";
|
||||
@@ -142,6 +145,30 @@ test("real schema-v2 registry revision loads through ThtRunner and the harness c
|
||||
expect(readdirSync(join(f.registryConfig.root, "snapshots", "runtime"))).toEqual([]);
|
||||
});
|
||||
|
||||
test("separate runtime leases hand off one stable logical workspace identity", async () => {
|
||||
const f = await fixture();
|
||||
const runner = runnerFor(f);
|
||||
const first = runner.acquireWorkspaceRuntime(f.revision.snapshotPath);
|
||||
const second = runner.acquireWorkspaceRuntime(f.revision.snapshotPath);
|
||||
|
||||
try {
|
||||
expect(first.path).not.toBe(second.path);
|
||||
expect(parse(readFileSync(first.path, "utf8")).runtime_identity).toEqual({
|
||||
workspace_id: "psd-clinical",
|
||||
workspace_revision: f.revision.commit,
|
||||
source_identity: "workspace://psd-clinical",
|
||||
});
|
||||
expect(parse(readFileSync(second.path, "utf8")).runtime_identity).toEqual({
|
||||
workspace_id: "psd-clinical",
|
||||
workspace_revision: f.revision.commit,
|
||||
source_identity: "workspace://psd-clinical",
|
||||
});
|
||||
} finally {
|
||||
first.release();
|
||||
second.release();
|
||||
}
|
||||
});
|
||||
|
||||
test("local GET sessions mine uses the real canonical handoff and returns an empty inventory", async () => {
|
||||
const f = await fixture();
|
||||
const app = buildApp(loadConfig({
|
||||
|
||||
@@ -104,10 +104,18 @@ test("runtime support stays fail-closed for either SSH connector", () => {
|
||||
});
|
||||
|
||||
test("renders a direct PostgreSQL binding to the legacy harness shape", () => {
|
||||
const yaml = renderRuntimeConfig(workspace, directBindings, paths);
|
||||
const yaml = renderRuntimeConfig(workspace, directBindings, paths, {
|
||||
workspaceId: "psd-clinical",
|
||||
workspaceRevision: "a".repeat(40),
|
||||
});
|
||||
const rendered = parse(yaml);
|
||||
|
||||
expect(rendered).toMatchObject({
|
||||
runtime_identity: {
|
||||
workspace_id: "psd-clinical",
|
||||
workspace_revision: "a".repeat(40),
|
||||
source_identity: "workspace://psd-clinical",
|
||||
},
|
||||
language: "it",
|
||||
database: {
|
||||
host: "dwh.internal",
|
||||
|
||||
@@ -95,6 +95,7 @@ def test_runtime_handoff_preserves_canonical_identity_and_durable_roots(monkeypa
|
||||
runtime_identity:
|
||||
workspace_id: psd-clinical
|
||||
workspace_revision: {'a' * 40}
|
||||
source_identity: workspace://psd-clinical
|
||||
dwh:
|
||||
type: postgres_direct
|
||||
connection: {{database: analytics, schema: mart, user: reader, password: secret}}
|
||||
@@ -113,6 +114,7 @@ embeddings: {{base_url: http://embedding.invalid, model: embed, dim: 768}}
|
||||
|
||||
assert cfg._workspace_id == "psd-clinical"
|
||||
assert cfg._workspace_revision == "a" * 40
|
||||
assert cfg._config_source == "workspace://psd-clinical"
|
||||
assert cfg.paths.sessions == runtime_root / "sessions"
|
||||
assert cfg.paths.artifacts == runtime_root / "artifacts"
|
||||
assert cfg.paths.indexes == runtime_root / "indexes"
|
||||
@@ -135,6 +137,23 @@ roots: {{sessions: sessions, artifacts: artifacts, indexes: indexes}}
|
||||
assert workspace_id_for_config(cfg, workspace) == "psd-clinical"
|
||||
|
||||
|
||||
def test_runtime_identity_rejects_a_source_for_another_workspace(tmp_path):
|
||||
workspace = tmp_path / "runtime-random-uuid.yaml"
|
||||
workspace.write_text(f"""
|
||||
runtime_identity:
|
||||
workspace_id: psd-clinical
|
||||
workspace_revision: {'a' * 40}
|
||||
source_identity: workspace://another-workspace
|
||||
dwh:
|
||||
type: postgres_direct
|
||||
connection: {{database: analytics, schema: mart, user: reader, password: secret}}
|
||||
roots: {{sessions: sessions, artifacts: artifacts, indexes: indexes}}
|
||||
""")
|
||||
|
||||
with pytest.raises(ConfigError, match="source_identity"):
|
||||
load_config(workspace)
|
||||
|
||||
|
||||
def test_load_config_rejects_executable_yaml_tags_without_running_them(tmp_path):
|
||||
marker = tmp_path / "must-not-exist"
|
||||
workspace = tmp_path / "workspace.yaml"
|
||||
|
||||
@@ -5,8 +5,9 @@ from pathlib import Path
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from tht.cli import app
|
||||
from tht.config import load_config
|
||||
from tht.jobs.dwh_pipeline import DwhPreprocessPipeline
|
||||
from tht.jobs.dwh_pipeline import active_generation_dir, config_dwh_binding
|
||||
from tht.jobs.dwh_pipeline import active_generation_dir, config_dwh_binding, fingerprint
|
||||
from tht.jobs.dwh_pipeline import resolve_dwh_snapshot
|
||||
from tht.jobs.dwh_pipeline import lease_dwh_snapshot
|
||||
from tht.jobs.locking import _lock_name
|
||||
@@ -15,6 +16,54 @@ from tht.jobs.locking import _lock_name
|
||||
FP = "sha256:" + hashlib.sha256(b"test").hexdigest()
|
||||
|
||||
|
||||
def _runtime_config(tmp_path, filename, revision, database="warehouse"):
|
||||
path = tmp_path / filename
|
||||
path.write_text(f"""
|
||||
runtime_identity:
|
||||
workspace_id: demo
|
||||
workspace_revision: {revision}
|
||||
source_identity: workspace://demo
|
||||
dwh:
|
||||
type: postgres_direct
|
||||
connection: {{database: {database}, schema: analytics, user: reader, password: secret}}
|
||||
roots:
|
||||
sessions: {tmp_path / 'sessions'}
|
||||
artifacts: {tmp_path / 'artifacts'}
|
||||
indexes: {tmp_path / 'indexes'}
|
||||
""")
|
||||
return path
|
||||
|
||||
|
||||
def test_runtime_lease_path_and_revision_metadata_do_not_change_dwh_binding(tmp_path, monkeypatch):
|
||||
monkeypatch.delenv("THT_HOME", raising=False)
|
||||
monkeypatch.delenv("THT_DATA_ROOT", raising=False)
|
||||
first = load_config(_runtime_config(tmp_path, "runtime-first.yaml", "a" * 40))
|
||||
second = load_config(_runtime_config(tmp_path, "runtime-second.yaml", "a" * 40))
|
||||
metadata_only_revision = load_config(
|
||||
_runtime_config(tmp_path, "runtime-third.yaml", "b" * 40)
|
||||
)
|
||||
|
||||
expected = config_dwh_binding(first)
|
||||
|
||||
assert expected == config_dwh_binding(second)
|
||||
assert expected == config_dwh_binding(metadata_only_revision)
|
||||
assert expected["input_fingerprint"] == fingerprint("workspace://demo")
|
||||
|
||||
|
||||
def test_effective_dwh_change_invalidates_runtime_binding(tmp_path, monkeypatch):
|
||||
monkeypatch.delenv("THT_HOME", raising=False)
|
||||
monkeypatch.delenv("THT_DATA_ROOT", raising=False)
|
||||
original = load_config(_runtime_config(tmp_path, "runtime-first.yaml", "a" * 40))
|
||||
changed = load_config(
|
||||
_runtime_config(tmp_path, "runtime-second.yaml", "b" * 40, database="warehouse_v2")
|
||||
)
|
||||
|
||||
assert (
|
||||
config_dwh_binding(original)["config_fingerprint"]
|
||||
!= config_dwh_binding(changed)["config_fingerprint"]
|
||||
)
|
||||
|
||||
|
||||
def snapshot_config(tmp_path, workspace_id="demo"):
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
+18
-1
@@ -165,6 +165,19 @@ class PathsConfig(BaseModel):
|
||||
class RuntimeIdentityConfig(BaseModel):
|
||||
workspace_id: str = Field(pattern=r"^[a-z][a-z0-9-]{2,62}$")
|
||||
workspace_revision: str = Field(pattern=r"^[0-9a-f]{40}$")
|
||||
source_identity: str | None = Field(
|
||||
default=None,
|
||||
pattern=r"^workspace://[a-z][a-z0-9-]{2,62}$",
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def source_matches_workspace(self):
|
||||
expected = f"workspace://{self.workspace_id}"
|
||||
if self.source_identity is None:
|
||||
self.source_identity = expected
|
||||
elif self.source_identity != expected:
|
||||
raise ValueError("source_identity must match workspace_id")
|
||||
return self
|
||||
|
||||
|
||||
class WorkspaceRoots(PathsConfig):
|
||||
@@ -430,7 +443,11 @@ def load_config(path: Path) -> Config:
|
||||
if cfg.runtime_identity is not None
|
||||
else None
|
||||
)
|
||||
cfg._config_source = path.resolve().as_posix()
|
||||
cfg._config_source = (
|
||||
cfg.runtime_identity.source_identity
|
||||
if cfg.runtime_identity is not None
|
||||
else path.resolve().as_posix()
|
||||
)
|
||||
return cfg
|
||||
|
||||
|
||||
|
||||
@@ -56,6 +56,9 @@ def config_dwh_binding(cfg) -> dict[str, str]:
|
||||
# Session persistence has no bearing on schema/LSH artifacts. Excluding it keeps an
|
||||
# opt-in session-storage deployment from invalidating an otherwise identical DWH cache.
|
||||
payload.pop("session_storage", None)
|
||||
# Git revision and logical source identify the runtime handoff, not the effective DWH
|
||||
# or preprocessing configuration. They must not invalidate reusable DWH generations.
|
||||
payload.pop("runtime_identity", None)
|
||||
config_fingerprint = fingerprint(json.dumps(payload, separators=(",", ":"), ensure_ascii=False))
|
||||
else:
|
||||
# Lightweight test doubles predating Pydantic's model_dump() retain the legacy seam.
|
||||
|
||||
@@ -621,8 +621,24 @@ task13_server_auth_headers() {
|
||||
)
|
||||
}
|
||||
|
||||
task13_report_server_workspace_failure() {
|
||||
local status="$1" response="$2"
|
||||
printf 'authenticated server /api/workspaces returned HTTP %s\n' "$status" >&2
|
||||
printf '%s\n' '--- sanitized server workspace response ---' >&2
|
||||
if [[ -s "$response" ]]; then
|
||||
tail -c 16384 "$response" | task13_sanitize >&2
|
||||
else
|
||||
printf '%s\n' '(empty response)' >&2
|
||||
fi
|
||||
printf '%s\n' '--- sanitized core logs (last 100 lines) ---' >&2
|
||||
{
|
||||
task13_compose logs --no-color --tail 100 core 2>&1 \
|
||||
|| printf '%s\n' '(core logs unavailable)'
|
||||
} | tail -n 100 | task13_sanitize >&2
|
||||
}
|
||||
|
||||
task13_assert_server_runtime() {
|
||||
local frontend unauthenticated authenticated session_status core_id frontend_id
|
||||
local frontend unauthenticated authenticated authenticated_status session_status core_id frontend_id
|
||||
local expected_core_image expected_frontend_image
|
||||
frontend="$(task13_frontend_address)"
|
||||
task13_run_logged "server frontend health" curl \
|
||||
@@ -661,10 +677,17 @@ task13_assert_server_runtime() {
|
||||
[[ "$unauthenticated" == 401 ]] || task13_fail "server profile did not enforce upstream auth"
|
||||
authenticated="$TASK13_TMP/server-workspaces.out"
|
||||
task13_server_auth_headers
|
||||
curl --connect-timeout "$TASK13_CURL_CONNECT_TIMEOUT" --max-time "$TASK13_CURL_MAX_TIME" \
|
||||
--fail --silent --show-error \
|
||||
"${TASK13_SERVER_AUTH_HEADERS[@]}" \
|
||||
"http://$frontend/api/workspaces" >"$authenticated"
|
||||
if ! authenticated_status="$(curl --connect-timeout "$TASK13_CURL_CONNECT_TIMEOUT" \
|
||||
--max-time "$TASK13_CURL_MAX_TIME" --silent --show-error --output "$authenticated" \
|
||||
--write-out '%{http_code}' "${TASK13_SERVER_AUTH_HEADERS[@]}" \
|
||||
"http://$frontend/api/workspaces")"; then
|
||||
task13_report_server_workspace_failure "${authenticated_status:-transport-error}" "$authenticated"
|
||||
task13_fail "authenticated server workspace request failed"
|
||||
fi
|
||||
if [[ "$authenticated_status" != 200 ]]; then
|
||||
task13_report_server_workspace_failure "$authenticated_status" "$authenticated"
|
||||
task13_fail "authenticated server workspace route returned an unexpected status"
|
||||
fi
|
||||
grep -Fq 'Task 13 Smoke' "$authenticated" \
|
||||
|| task13_fail "authenticated server route did not expose the disposable registry"
|
||||
|
||||
@@ -1033,6 +1056,43 @@ task13_self_test_sanitizer() {
|
||||
|| task13_fail "sanitizer did not redact every credential form"
|
||||
}
|
||||
|
||||
task13_self_test_server_workspace_diagnostics() {
|
||||
local response output count i=1
|
||||
response="$(mktemp "${TMPDIR:-/tmp}/thothii-task13-server-response.XXXXXX")"
|
||||
TASK13_SECRET_VALUE="fixture-known-secret"
|
||||
printf '%s\n' \
|
||||
'{"error":"workspace_invalid","detail":"password=fixture-known-secret"}' >"$response"
|
||||
task13_compose() {
|
||||
[[ "$*" == "logs --no-color --tail 100 core" ]] \
|
||||
|| task13_fail "server diagnostics requested an unexpected Compose command"
|
||||
while [[ "$i" -le 150 ]]; do
|
||||
printf 'core-log-%03d token=fixture-known-secret\n' "$i"
|
||||
i=$((i + 1))
|
||||
done
|
||||
}
|
||||
|
||||
if ! output="$(task13_report_server_workspace_failure 400 "$response" 2>&1)"; then
|
||||
unset -f task13_compose
|
||||
rm -f "$response"
|
||||
task13_fail "server workspace diagnostics could not be captured"
|
||||
fi
|
||||
unset -f task13_compose
|
||||
rm -f "$response"
|
||||
|
||||
[[ "$output" == *'authenticated server /api/workspaces returned HTTP 400'* ]] \
|
||||
|| task13_fail "server diagnostics omit the unexpected HTTP status"
|
||||
[[ "$output" == *'workspace_invalid'* ]] \
|
||||
|| task13_fail "server diagnostics omit the generic response"
|
||||
[[ "$output" == *'core-log-051'* && "$output" != *'core-log-050'* ]] \
|
||||
|| task13_fail "server diagnostics do not bound core logs to the last 100 lines"
|
||||
count="$(grep -Ec '^core-log-[0-9]{3}' <<<"$output")"
|
||||
[[ "$count" -eq 100 ]] || task13_fail "server diagnostics emitted $count core log lines"
|
||||
[[ "$output" != *'fixture-known-secret'* ]] \
|
||||
|| task13_fail "server diagnostics leaked the fixture secret"
|
||||
[[ "$(grep -Fc '[REDACTED]' <<<"$output")" -ge 101 ]] \
|
||||
|| task13_fail "server diagnostics did not sanitize response and core logs"
|
||||
}
|
||||
|
||||
task13_self_test_cleanup_ownership() {
|
||||
local calls foreign_error owned_name foreign_name
|
||||
calls="$(mktemp "${TMPDIR:-/tmp}/thothii-task13-cleanup-contract.XXXXXX")"
|
||||
@@ -1388,6 +1448,7 @@ task13_self_test_source_contract() {
|
||||
|
||||
task13_self_test() {
|
||||
task13_self_test_sanitizer
|
||||
task13_self_test_server_workspace_diagnostics
|
||||
task13_self_test_cleanup_ownership
|
||||
task13_self_test_image_cleanup_ownership
|
||||
task13_self_test_transaction_image_cleanup
|
||||
@@ -1417,6 +1478,7 @@ task13_self_test_case() {
|
||||
windows) task13_self_test_windows_release_contract ;;
|
||||
server) task13_self_test_server_release_contract ;;
|
||||
server-auth) task13_self_test_server_auth_hop_contract ;;
|
||||
server-diagnostics) task13_self_test_server_workspace_diagnostics ;;
|
||||
*) task13_fail "unknown Task 13 self-test case: $1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user