Skip to content

Commit f1e5b72

Browse files
stubbiclaude
andauthored
test(conformance): refresh Ready-gated skip reasons (#68 fixed) + dump diagnostics on timeout (#87)
Follow-up to #85 and #88. **Net effect: the Ready-gated idempotency corpus stays skipped, but for accurate reasons, plus a diagnostics dump.** (This branch briefly un-skipped the corpus to find out what was actually blocking Ready — that investigation is summarized below.) ## What the investigation found #68 (the `init-uv` contract) is **fixed**: #85 ships the lockfiles at `/opt/venv-template/`, #88 ships the `uv` binary, and `v2026.5.29.2` was republished + verified. `init-uv` now succeeds and the pod reaches its main `hermes` container. But un-skipping showed the instance still can't become Ready in CI — three deeper, separate blockers (surfaced by the new diagnostics dump): 1. **No LLM provider** — the published hermes-agent exits immediately without a provider + credentials (`Failed to initialize agent: No LLM provider configured`). CI fixtures carry none → container exits 0 → CrashLoopBackOff. 2. **Wrong serve command / port model** — the long-lived agent is `hermes gateway run` (a chat-platform daemon that binds **no** `:8443` server), not the one-shot `hermes-agent run` the image entrypoint invokes. The operator's `TCPSocket :8443` readiness probe can't pass as-is. 3. **Missing image deps** — `websockets`, `hermes_cli.dashboard_auth`. Reaching Ready in CI needs design-level decisions (inject LLM creds, rework the entrypoint/readiness/port model, fix image deps) — out of scope for the #68 image fix. ## This PR - Keeps the corpus skipped, but replaces the stale `#68` skip reason with `idempotencyReadyBlockedSkip` describing the real blockers (and noting #68 is resolved). Tracked under #64. - Adds `dumpInstanceDiagnostics` to `waitForInstanceReady` (instance YAML, pods/sts, describe, events, init-container logs) so the next Ready attempt is debuggable from the CI log instead of a bare timeout. Conformance stays green (skips). Un-skip once an instance can actually reach Ready in CI. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 13fb1e5 commit f1e5b72

2 files changed

Lines changed: 66 additions & 27 deletions

File tree

test/conformance/helpers.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,38 @@ func waitForInstanceReady(ctx context.Context, c client.Client, ns, name string,
8989
}
9090
time.Sleep(2 * time.Second)
9191
}
92+
dumpInstanceDiagnostics(ns, name)
9293
Fail(fmt.Sprintf("HermesInstance %s/%s did not become Ready within %s", ns, name, timeout))
9394
}
9495

96+
// dumpInstanceDiagnostics best-effort prints cluster state for a HermesInstance
97+
// that failed to become Ready, so the CI log shows *where* it is stuck (init
98+
// container errors, image pull, crash loop, or simply still-progressing) instead
99+
// of a bare timeout. Shells out to kubectl against the suite's kubeconfig.
100+
func dumpInstanceDiagnostics(ns, name string) {
101+
kc := clientcmdPath()
102+
sel := "app.kubernetes.io/instance=" + name
103+
steps := [][]string{
104+
{"-n", ns, "get", "hermesinstance", name, "-o", "yaml"},
105+
{"-n", ns, "get", "pods,sts", "-o", "wide"},
106+
{"-n", ns, "describe", "pods", "-l", sel},
107+
{"-n", ns, "get", "events", "--sort-by=.lastTimestamp"},
108+
}
109+
fmt.Fprintf(GinkgoWriter, "\n===== diagnostics for %s/%s (not Ready) =====\n", ns, name)
110+
for _, s := range steps {
111+
args := append([]string{"--kubeconfig", kc}, s...)
112+
out, _ := run("kubectl", args...)
113+
fmt.Fprintf(GinkgoWriter, "\n----- kubectl %s -----\n%s\n", strings.Join(s, " "), out)
114+
}
115+
// Init-container logs reveal the exact failure (e.g. init-uv cp / uv sync).
116+
for _, ic := range []string{"init-apt", "init-uv", "init-pip"} {
117+
out, _ := run("kubectl", "--kubeconfig", kc, "-n", ns, "logs",
118+
"-l", sel, "-c", ic, "--tail", "50", "--prefix")
119+
fmt.Fprintf(GinkgoWriter, "\n----- logs %s -----\n%s\n", ic, out)
120+
}
121+
fmt.Fprintf(GinkgoWriter, "===== end diagnostics =====\n")
122+
}
123+
95124
func hasReadyTrue(inst *hermesv1.HermesInstance) bool {
96125
for _, cond := range inst.Status.Conditions {
97126
if cond.Type == "Ready" && cond.Status == "True" {

test/conformance/idempotency_test.go

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,29 @@ import (
1414
// more times. After each requeue we assert the resourceFingerprint is unchanged
1515
// (generation + resourceVersion must not move). This catches lesson #437
1616
// regressions: a reconciler that always re-writes owned objects will fail here.
17-
// idempotencyImageContractSkip documents why the Ready-gated idempotency corpus
18-
// is currently skipped. Once the conformance suite actually runs (#64), every
19-
// HermesInstance fails to reach Ready: the operator's `init-uv` init container
20-
// (internal/resources/runtime_init.go) copies pyproject.toml/uv.lock from
21-
// /opt/venv-template/, but the published ghcr.io/paperclipinc/hermes-agent image
22-
// builds its venv at /opt/venv and ships nothing at /opt/venv-template/. The init
23-
// container exits 1, the pod never starts the hermes container, and the
24-
// StatefulSet never reaches readyReplicas==replicas. That is an operator/agent
25-
// image contract bug unrelated to reconciler idempotency, tracked in #68. These
26-
// entries are skipped (visibly, with this reason) rather than left to hang until
27-
// timeout and fail. Remove the skips once #68 is fixed.
28-
const idempotencyImageContractSkip = "blocked by #68: operator init-uv copies from /opt/venv-template which is absent in the published hermes-agent image, so no HermesInstance reaches Ready; unskip once #68 is fixed"
17+
// The Ready-gated corpus stays skipped, but the reason has moved on from #68.
18+
//
19+
// #68 (the init-uv contract: lockfiles missing at /opt/venv-template/) is fixed
20+
// — #85 ships the lockfiles, #88 ships the uv binary, and v2026.5.29.2 was
21+
// republished. With that, init-uv now succeeds (verified) and the pod reaches
22+
// its main `hermes` container. But the instance still cannot become Ready in CI:
23+
//
24+
// 1. The published hermes-agent exits immediately unless an LLM provider +
25+
// credentials are configured ("Failed to initialize agent: No LLM provider
26+
// configured"). The CI fixtures carry none, so the container exits 0 and
27+
// CrashLoopBackOffs.
28+
// 2. The agent's long-lived form is `hermes gateway run` (a chat-platform
29+
// daemon), not the one-shot `hermes-agent run` the image entrypoint invokes
30+
// — and it binds no :8443 server, so the operator's TCPSocket :8443
31+
// readiness probe cannot pass as-is.
32+
// 3. The published image is missing modules (websockets, hermes_cli.dashboard_auth).
33+
//
34+
// Reaching Ready in CI therefore needs design-level decisions (inject LLM
35+
// credentials, rework the entrypoint/readiness/port model, fix image deps), not
36+
// just the #68 image fix. Tracked under #64. waitForInstanceReady now dumps pod
37+
// diagnostics on timeout so the blocker is visible in the CI log. Unskip these
38+
// once an instance can actually reach Ready in CI.
39+
const idempotencyReadyBlockedSkip = "cannot reach Ready in CI: hermes-agent needs an LLM provider+credentials to start and the operator's :8443 gateway-readiness model does not match the chat-gateway agent (see #64); the #68 init-uv contract itself is fixed (#85, #88)"
2940

3041
var idempotencyCorpus = []struct {
3142
label string
@@ -36,27 +47,26 @@ var idempotencyCorpus = []struct {
3647
// blocked by an out-of-scope operator bug).
3748
skip string
3849
}{
39-
{label: "minimal", fixture: "minimal.yaml", skip: idempotencyImageContractSkip},
40-
{label: "maximal", fixture: "maximal.yaml", skip: idempotencyImageContractSkip},
41-
{label: "gateways-all", fixture: "gateways-all.yaml", skip: idempotencyImageContractSkip},
42-
{label: "selfconfig-enabled", fixture: "selfconfig-enabled.yaml", skip: idempotencyImageContractSkip},
43-
{label: "profilestore-enabled", fixture: "profilestore-enabled.yaml", skip: idempotencyImageContractSkip},
44-
{label: "autoupdate-enabled", fixture: "autoupdate-enabled.yaml", skip: idempotencyImageContractSkip},
45-
{label: "backup-enabled", fixture: "backup-enabled.yaml", skip: idempotencyImageContractSkip},
46-
{label: "networking-ingress", fixture: "networking-ingress.yaml", skip: idempotencyImageContractSkip},
47-
{label: "observability-full", fixture: "observability-full.yaml", skip: idempotencyImageContractSkip},
50+
{label: "minimal", fixture: "minimal.yaml", skip: idempotencyReadyBlockedSkip},
51+
{label: "maximal", fixture: "maximal.yaml", skip: idempotencyReadyBlockedSkip},
52+
{label: "gateways-all", fixture: "gateways-all.yaml", skip: idempotencyReadyBlockedSkip},
53+
{label: "selfconfig-enabled", fixture: "selfconfig-enabled.yaml", skip: idempotencyReadyBlockedSkip},
54+
{label: "profilestore-enabled", fixture: "profilestore-enabled.yaml", skip: idempotencyReadyBlockedSkip},
55+
{label: "autoupdate-enabled", fixture: "autoupdate-enabled.yaml", skip: idempotencyReadyBlockedSkip},
56+
{label: "backup-enabled", fixture: "backup-enabled.yaml", skip: idempotencyReadyBlockedSkip},
57+
{label: "networking-ingress", fixture: "networking-ingress.yaml", skip: idempotencyReadyBlockedSkip},
58+
{label: "observability-full", fixture: "observability-full.yaml", skip: idempotencyReadyBlockedSkip},
4859
{
4960
label: "ollama-webterminal-tailscale",
5061
fixture: "ollama-webterminal-tailscale.yaml",
51-
// Blocked twice over: by #68 (init-uv contract, like every entry) and,
52-
// even after #68, by the operator-managed tailscale sidecar. That sidecar
53-
// runs `containerboot`, which exits when TS_AUTHKEY cannot join a tailnet.
62+
// Blocked by the operator-managed tailscale sidecar: it runs
63+
// `containerboot`, which exits when TS_AUTHKEY cannot join a tailnet.
5464
// The fixture ships a dummy auth key (no real ephemeral key is available
5565
// in CI), so the sidecar container never becomes Ready, the pod stays
5666
// NotReady, and the HermesInstance never reaches Ready=True. Unskip only
57-
// once #68 is fixed AND a real ephemeral tailnet auth key is injected via
58-
// secret in CI. See #64.
59-
skip: "requires a live tailscale ephemeral auth key to reach Ready (dummy key cannot join a tailnet), and is also blocked by #68; see #64",
67+
// once a real ephemeral tailnet auth key is injected via secret in CI.
68+
// See #64. (#68, which blocked every other entry, is now fixed.)
69+
skip: "requires a live tailscale ephemeral auth key to reach Ready (dummy key cannot join a tailnet); see #64",
6070
},
6171
}
6272

0 commit comments

Comments
 (0)