Skip to content

Commit 490d8e1

Browse files
stubbiclaude
andauthored
test(conformance): un-skip the Ready-gated idempotency corpus (runtime fixed in #90) (#91)
Follow-up to #90. The agent image has been **republished FROM upstream** as `ghcr.io/paperclipinc/hermes-agent:v0.16.0` (verified: multi-arch, `/init` s6 entrypoint, `HERMES_HOME=/opt/data`, version label), so the Ready-gated conformance corpus can run for real. - Bumps the 10 conformance fixtures `v2026.5.29.2` → `v0.16.0` (the upstream-based runtime). - Un-skips the 9 `#68`-gated idempotency entries. The suite now exercises real Ready + 10× reconcile-idempotency against the published image. - `ollama-webterminal-tailscale` stays skipped — still needs a live ephemeral Tailscale auth key (unrelated; #64). The `Idempotency` conformance job on this PR validates it end-to-end on a live kind cluster pulling the republished image. Closes the loop on #89. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 121de7e commit 490d8e1

14 files changed

Lines changed: 96 additions & 45 deletions

.github/workflows/conformance.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ jobs:
4242
idempotency:
4343
name: Idempotency
4444
runs-on: ubuntu-latest
45-
timeout-minutes: 45
45+
timeout-minutes: 65
4646
steps:
4747
- uses: actions/checkout@v6
4848
- uses: actions/setup-go@v6

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ conformance-negative:
359359

360360
.PHONY: conformance-idempotency
361361
conformance-idempotency:
362-
cd test/conformance && go test -v -timeout 30m -ginkgo.v -ginkgo.focus="idempotency" ./...
362+
cd test/conformance && go test -v -timeout 55m -ginkgo.v -ginkgo.focus="idempotency" ./...
363363

364364
.PHONY: conformance-upgrade
365365
conformance-upgrade:

test/conformance/helpers.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ func kubectlDelete(yaml string) (string, error) {
5252
return runStdin("kubectl", []string{"delete", "--ignore-not-found", "-f", "-"}, yaml)
5353
}
5454

55+
// kubectlDeleteNoWait deletes without blocking on finalizers. Used for per-entry
56+
// cleanup in the idempotency corpus: some fixtures (e.g. backup-enabled) install
57+
// an on-delete finalizer whose snapshot Job cannot complete with placeholder
58+
// credentials, so a default `kubectl delete` (which waits for the finalizer)
59+
// would hang and starve the remaining corpus entries. The ephemeral cluster is
60+
// torn down at the end, so leaving a terminating instance behind is harmless.
61+
func kubectlDeleteNoWait(yaml string) (string, error) {
62+
return runStdin("kubectl", []string{"delete", "--ignore-not-found", "--wait=false", "-f", "-"}, yaml)
63+
}
64+
5565
func clientcmdPath() string {
5666
if p := os.Getenv("KUBECONFIG"); p != "" {
5767
return p

test/conformance/idempotency_test.go

Lines changed: 72 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,44 +14,31 @@ 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-
// The Ready-gated corpus stays skipped only until the agent image is republished.
18-
//
19-
// The runtime blockers that previously made Ready unreachable are FIXED in the
20-
// operator code (PR #90, issue #89): the operator now runs the upstream s6
21-
// hermes-agent image with `gateway run` + the OpenAI API server, probes
22-
// HTTPGet /health, and injects a placeholder LLM provider so the gateway comes
23-
// up without live calls (validated end-to-end on kind — an instance reaches
24-
// Ready=True). The old failure modes are gone: no one-shot `hermes-agent run`,
25-
// no TCPSocket :8443 with nothing listening, no LLM-credential requirement just
26-
// to start, no missing modules (the upstream image ships everything incl. a
27-
// browser).
28-
//
29-
// The one thing the conformance suite still needs is the *published* image to BE
30-
// that upstream-based runtime: these fixtures pin
31-
// `ghcr.io/paperclipinc/hermes-agent:v2026.5.29.2`, which only ships the new
32-
// runtime after the FROM-upstream Dockerfile (PR #90) is merged and that tag is
33-
// republished. Un-skip in the follow-up once the image is republished.
34-
// (waitForInstanceReady dumps pod diagnostics on timeout if anything regresses.)
35-
const idempotencyReadyBlockedSkip = "runtime fixed in #90 (upstream s6 image + gateway run + /health, validated on kind); un-skip once ghcr.io/paperclipinc/hermes-agent:v2026.5.29.2 is republished FROM upstream"
17+
// The Ready-gated corpus now runs. The runtime blockers (#68 → #89) are fixed in
18+
// #90: the operator runs the upstream s6 hermes-agent image with `gateway run` +
19+
// the OpenAI API server, probes HTTPGet /health, and injects a placeholder LLM
20+
// provider so the gateway comes up without live calls. The fixtures pin
21+
// `ghcr.io/paperclipinc/hermes-agent:v0.16.0`, which is the upstream-based
22+
// runtime. Validated end-to-end on kind (an instance reaches Ready=True).
23+
// waitForInstanceReady dumps pod diagnostics on timeout if anything regresses.
3624

3725
var idempotencyCorpus = []struct {
3826
label string
3927
fixture string
4028
// skip, when non-empty, skips this corpus entry with the given reason.
4129
// Used for fixtures that cannot reach Ready in CI for reasons unrelated to
42-
// operator idempotency (e.g. they require live external credentials, or are
43-
// blocked by an out-of-scope operator bug).
30+
// operator idempotency (e.g. they require live external credentials).
4431
skip string
4532
}{
46-
{label: "minimal", fixture: "minimal.yaml", skip: idempotencyReadyBlockedSkip},
47-
{label: "maximal", fixture: "maximal.yaml", skip: idempotencyReadyBlockedSkip},
48-
{label: "gateways-all", fixture: "gateways-all.yaml", skip: idempotencyReadyBlockedSkip},
49-
{label: "selfconfig-enabled", fixture: "selfconfig-enabled.yaml", skip: idempotencyReadyBlockedSkip},
50-
{label: "profilestore-enabled", fixture: "profilestore-enabled.yaml", skip: idempotencyReadyBlockedSkip},
51-
{label: "autoupdate-enabled", fixture: "autoupdate-enabled.yaml", skip: idempotencyReadyBlockedSkip},
52-
{label: "backup-enabled", fixture: "backup-enabled.yaml", skip: idempotencyReadyBlockedSkip},
53-
{label: "networking-ingress", fixture: "networking-ingress.yaml", skip: idempotencyReadyBlockedSkip},
54-
{label: "observability-full", fixture: "observability-full.yaml", skip: idempotencyReadyBlockedSkip},
33+
{label: "minimal", fixture: "minimal.yaml"},
34+
{label: "maximal", fixture: "maximal.yaml"},
35+
{label: "gateways-all", fixture: "gateways-all.yaml"},
36+
{label: "selfconfig-enabled", fixture: "selfconfig-enabled.yaml"},
37+
{label: "profilestore-enabled", fixture: "profilestore-enabled.yaml"},
38+
{label: "autoupdate-enabled", fixture: "autoupdate-enabled.yaml"},
39+
{label: "backup-enabled", fixture: "backup-enabled.yaml"},
40+
{label: "networking-ingress", fixture: "networking-ingress.yaml"},
41+
{label: "observability-full", fixture: "observability-full.yaml"},
5542
{
5643
label: "ollama-webterminal-tailscale",
5744
fixture: "ollama-webterminal-tailscale.yaml",
@@ -72,6 +59,52 @@ const (
7259
idempotencyPokeWait = 15 * time.Second
7360
)
7461

62+
// seedConformanceSecrets creates the dummy Secrets the feature-rich corpus
63+
// fixtures reference (gateway tokens, Honcho API key, maximal's extra-env). The
64+
// operator wires these into the agent container via non-optional secretKeyRefs,
65+
// so they must exist or the pod fails with CreateContainerConfigError. Values are
66+
// placeholders — Ready only needs the env to resolve, not the upstream to accept.
67+
func seedConformanceSecrets(ns string) {
68+
manifest := fmt.Sprintf(`
69+
apiVersion: v1
70+
kind: Secret
71+
metadata: {name: tg-token, namespace: %[1]s}
72+
stringData: {token: dummy}
73+
---
74+
apiVersion: v1
75+
kind: Secret
76+
metadata: {name: discord-token, namespace: %[1]s}
77+
stringData: {token: dummy}
78+
---
79+
apiVersion: v1
80+
kind: Secret
81+
metadata: {name: slack-token, namespace: %[1]s}
82+
stringData: {bot-token: dummy, app-token: dummy, signing-secret: dummy}
83+
---
84+
apiVersion: v1
85+
kind: Secret
86+
metadata: {name: wa-token, namespace: %[1]s}
87+
stringData: {token: dummy}
88+
---
89+
apiVersion: v1
90+
kind: Secret
91+
metadata: {name: sig-token, namespace: %[1]s}
92+
stringData: {phone-number: "+10000000000", auth-token: dummy}
93+
---
94+
apiVersion: v1
95+
kind: Secret
96+
metadata: {name: api-keys, namespace: %[1]s}
97+
stringData: {honcho-api-key: dummy}
98+
---
99+
apiVersion: v1
100+
kind: Secret
101+
metadata: {name: hermes-maximal-extra-env, namespace: %[1]s}
102+
stringData: {HERMES_EXTRA: "1"}
103+
`, ns)
104+
out, err := kubectlApply(manifest)
105+
Expect(err).ToNot(HaveOccurred(), "seed conformance secrets: %s", out)
106+
}
107+
75108
var _ = Describe("idempotency canary", Ordered, func() {
76109
var (
77110
ns string
@@ -83,6 +116,11 @@ var _ = Describe("idempotency canary", Ordered, func() {
83116
DeferCleanup(func() {
84117
deleteNamespace(ns)
85118
})
119+
// Seed the dummy Secrets the feature-rich fixtures reference (gateway
120+
// tokens, Honcho key, maximal's extra-env). A real deployment ships these
121+
// alongside the instance; the operator injects them into the agent via
122+
// non-optional secretKeyRefs, so they must exist for the pod to start.
123+
seedConformanceSecrets(ns)
86124
})
87125

88126
for _, entry := range idempotencyCorpus {
@@ -109,7 +147,10 @@ var _ = Describe("idempotency canary", Ordered, func() {
109147
Expect(instName).ToNot(BeEmpty(), "could not extract name from fixture %s", entry.fixture)
110148

111149
DeferCleanup(func() {
112-
_, _ = kubectlDelete(namespaced)
150+
// Non-blocking: a fixture's on-delete finalizer (e.g.
151+
// backup-enabled's snapshot Job on placeholder creds) must not
152+
// stall the rest of the corpus. The namespace is torn down at end.
153+
_, _ = kubectlDeleteNoWait(namespaced)
113154
})
114155
})
115156

test/conformance/testdata/autoupdate-enabled.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ metadata:
55
spec:
66
image:
77
repository: ghcr.io/paperclipinc/hermes-agent
8-
tag: "v2026.5.29.2"
8+
tag: "v0.16.0"
99
storage:
1010
persistence:
1111
enabled: true

test/conformance/testdata/backup-enabled.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ metadata:
55
spec:
66
image:
77
repository: ghcr.io/paperclipinc/hermes-agent
8-
tag: "v2026.5.29.2"
8+
tag: "v0.16.0"
99
storage:
1010
persistence:
1111
enabled: true

test/conformance/testdata/gateways-all.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ metadata:
55
spec:
66
image:
77
repository: ghcr.io/paperclipinc/hermes-agent
8-
tag: "v2026.5.29.2"
8+
tag: "v0.16.0"
99
storage:
1010
persistence:
1111
enabled: true

test/conformance/testdata/maximal.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ metadata:
77
spec:
88
image:
99
repository: ghcr.io/paperclipinc/hermes-agent
10-
tag: "v2026.5.29.2"
10+
tag: "v0.16.0"
1111
pullPolicy: IfNotPresent
1212
storage:
1313
persistence:
@@ -72,13 +72,13 @@ spec:
7272
probes:
7373
liveness:
7474
httpGet:
75-
path: /healthz
75+
path: /health
7676
port: 8443
7777
initialDelaySeconds: 30
7878
periodSeconds: 10
7979
readiness:
8080
httpGet:
81-
path: /readyz
81+
path: /health
8282
port: 8443
8383
initialDelaySeconds: 10
8484
periodSeconds: 5

test/conformance/testdata/minimal.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ metadata:
55
spec:
66
image:
77
repository: ghcr.io/paperclipinc/hermes-agent
8-
tag: "v2026.5.29.2"
8+
tag: "v0.16.0"
99
storage:
1010
persistence:
1111
enabled: true

test/conformance/testdata/networking-ingress.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ metadata:
55
spec:
66
image:
77
repository: ghcr.io/paperclipinc/hermes-agent
8-
tag: "v2026.5.29.2"
8+
tag: "v0.16.0"
99
storage:
1010
persistence:
1111
enabled: true

0 commit comments

Comments
 (0)