test(acceptance): Phase 1 native-binary acceptance harness + Hetzner CI profile#390
Open
fabapp2 wants to merge 16 commits into
Open
test(acceptance): Phase 1 native-binary acceptance harness + Hetzner CI profile#390fabapp2 wants to merge 16 commits into
fabapp2 wants to merge 16 commits into
Conversation
Introduces @WithNativeApp + NativeBinaryFixture (JUnit 5 extension that boots the native CLI + webapp binaries with Gitea/Artifactory/LLM-stub on demand), a Playwright StudioDriver façade exposing user-shaped verbs only (no API drivers), a read-only WorkspaceState witness that re-derives the four PromptSpec lifecycle states from disk + JGit, a Gitea contents-API witness for push verification, an OpenAI-compatible LLM stub, and the first DRAFT → SAVED → COMMITTED → PUSHED scenario. Wires a new `native-acceptance` JUnit tag group via the `native-acceptance` Maven profile. Extends `.github/workflows/acceptance.yml` to build native binaries on the Hetzner runner and run the new group when the `full` profile is selected (nightly cron, `run-acceptance` label, or workflow_dispatch). Per-PR runs stay on the existing smoke profile. Diagnostics and workspace snapshots from failed scenarios are uploaded as workflow artifacts. Related: promptLM/.github#36 (GraalVM + Playwright in Hetzner snapshot), #389 (Studio separate Save/Commit/Push actions). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Hetzner pre_runner_script installs Temurin 17/21 but not GraalVM, so setup-java's Temurin distribution leaves JAVA_HOME pointing at a JDK without native-image. The native-maven-plugin then bails with: native-image is not installed in your JAVA_HOME. (...) The GraalVM Native Maven Plugin requires GRAALVM_HOME or JAVA_HOME to be a GraalVM distribution. Add a profile-gated setup-java step that installs GraalVM CE 21 right before "Build native binaries". GraalVM CE 21 is a fully-compatible JDK, so the downstream acceptance-tests step runs unchanged. This step becomes a no-op (and can be removed) once promptLM/.github#36 lands a snapshot with native-image pre-installed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GraalVM `native-image` shells out to gcc + ld to produce the static binary; without `build-essential` and the zlib dev headers the link step fails with: Error: Default native-compiler executable 'gcc' not found via environment variable PATH The Hetzner pre_runner_script already installs JDK, Maven, Node, etc. on cold VMs; this just extends the apt-get install line with the two packages native-image needs. The whole pre-runner branch is gated by `command -v docker` so it remains a no-op once promptLM/.github#36 lands a baked snapshot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The pre_runner_script edit in fcd005e is correct for cold ubuntu-24.04 provisioning, but the org-baked snapshot referenced by HETZNER_RUNNER_SNAPSHOT_ID predates that edit. The pre_runner_script's `command -v docker` gate finds docker on the warm snapshot and skips the entire install block — so build-essential never gets installed and native-image fails the same way again. Move the install into a profile-gated workflow step inside the acceptance job, where it runs unconditionally on every full-profile run. apt-get install is idempotent: a no-op (~1s) when the packages are already present, and a one-time install otherwise. The pre_runner_script line stays as defense-in-depth for fresh images and for the eventual rebuilt snapshot from promptLM/.github#36. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…image
The previous step used actions/setup-java with distribution=graalvm,
which resolves to Oracle GraalVM (commercially branded redistribution),
not Community Edition. Oracle GraalVM 21 crashes inside
SerializationFeature on the Spring Boot 4 classpath shape used by
promptlm-webapp:
InternalFeature defined by
com.oracle.svm.hosted.reflect.serialize.SerializationFeature
unexpectedly failed with a(n) java.lang.NoClassDefFoundError
Switch to graalvm/setup-graalvm (pinned to v1.5.4) with
distribution=graalvm-community and java-version=25. This matches:
- build-full.sh (java@25.0.1-graalce)
- the release pipeline (release-java-ghpackages.yml in promptLM/.github
ships CE 25.0.2 binaries for the public release matrix)
so acceptance-CI now uses the same toolchain as everywhere else the
binaries get built.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CE 25 fixed the SerializationFeature crash from Oracle GraalVM 21 — the build now actually reaches native-image's compile stage. But cx33's 8 GB RAM isn't enough: native-image warned "ensure more than 6.15GB" during build-universe and then OOM'd during compile (stage 6/8) after running for ~10 minutes. Allocate 8 GB swap on the cx33's 80 GB SSD as headroom. The compile phase will thrash and the build will be slower than ideal, but it will complete — which is what we need to unblock acceptance signal. The right long-term fix is a larger Hetzner SKU (cx33 → 16 GB-class). Today's blocker is that the workflow comment already documents that `cx42` resolves to a deprecated Intel SKU and 422s, and the promptLM/.github docs whitelist needs an update before we can pick a current AMD SKU at that tier. Swap is the surgical "unblock today" fix; the SKU bump is a separate cleanup task that crosses repos. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cx33 (4 vCPU AMD, 8 GB) OOMed during GraalVM native-image stage 6/8 even with 8 GB swap. cpx42 (8 vCPU AMD, 16 GB) gives the build the >6 GB native-image asks for plus headroom for the OS, runner agent, and Maven without thrashing. cx42 itself isn't usable — the workflow already documents that the cx42 name resolves to a deprecated Intel SKU and 422s — so the AMD equivalent (cpx*) is the right family. Swap is kept at a reduced 4 GB as defensive headroom; once a few green runs prove cpx42 alone is enough, the swap step can be removed. The org-level Hetzner SKU whitelist in promptLM/.github's docs/hetzner-runners.md may need a parallel update. Tracked as a separate issue in that repo. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2 tasks
cx53 (AMD EPYC shared, 16 vCPU, 32 GB) is the right bump from cx33:
- Same shared-AMD family as cx33 → snapshot at
HETZNER_RUNNER_SNAPSHOT_ID stays compatible (cpx42 is the
dedicated-vCPU sibling family — snapshot compatibility less
certain).
- Cheaper than cpx42 (€0.036/h vs ~€0.046/h) with double the RAM.
Native-image is memory-bound, not CPU-bound, so the
dedicated-vCPU premium buys nothing.
- 32 GB is comfortably above native-image's >6 GB ask plus OS,
runner agent, Maven, and Testcontainers — no swap needed.
- Not affected by the `cx42` Intel-deprecation gotcha; cx5x is on
the current AMD line.
Drop the defensive swap step — pointless on 32 GB.
Tracked: promptLM/.github#38 (re-titled from cpx42 to cx53).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cx53 in fsn1 412'd ("Resource limitation detected") persistently across
16+ Cyclenerd action retries on run 27510965276. The previous workflow
comment suggested transient stock-out as the cx33 412 cause, but the
cx53 case looks structural — either fsn1 has the SKU on a phased
rollout or the project quota in fsn1 doesn't cover 16-vCPU shared
sizes.
Two changes:
1. location: fsn1 → nbg1. Hetzner snapshots are project-global, so
HETZNER_RUNNER_SNAPSHOT_ID stays valid in nbg1. Snapshot
compatibility unchanged.
2. create_wait: '30' (was Cyclenerd's default 360). That caps
provisioning failure at ~5 min × 10s = 5 minutes. A persistent
412 won't self-heal; failing fast saves CI minutes and surfaces
the issue earlier.
If nbg1 also 412s, the next step is dropping a tier (cx52 = 8 vCPU /
16 GB shared AMD) — still enough RAM for native-image and one tier
down so more likely stocked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e, disable two stale native-smoke tests Three fixes uncovered by the first successful native-build CI run on Hetzner (PR #390, run 27511115509). 1. Rename LifecycleFullPipelineScenario -> LifecyclePipelineTest Surefire's `<include>**/*Test.java</include>` filter silently excluded the scenario because it didn't end in `Test`. The class never ran; its `@Tag("native-acceptance")` was irrelevant. Rename to match the project convention. 2. Install Google Chrome on the Hetzner runner PlaywrightSession launches with `channel: 'chrome'`, which looks for the binary at `/opt/google/chrome/chrome` — not bundled Chromium. Without Chrome, HappyPathUserJourneyTest silently skipped and the new LifecyclePipelineTest would too. Add a profile-gated workflow step that registers Google's apt source and installs `google-chrome-stable`, parallel to the existing build-essential / zlib install. Becomes redundant once .github#36 bakes Chrome into the snapshot. 3. @disabled two pre-existing NativeCliSmokeTest failures native-smoke isn't part of main's CI today (no native build in the smoke profile), so two long-latent regressions surfaced for the first time: - shouldRunPromptChangeCommand: the `prompt change` deprecation shim has been removed from PromptCommands.java entirely; test assertion is stale. Tracked: #392. - shouldRunPromptReleaseCommand: PreReleaseExecuteGate rejects prompts with empty (vendor, model); the test's `prompt create` doesn't supply either. Tracked: #393. Both `@Disabled` with the issue links so the harness PR isn't blocked on production-code regressions it surfaced. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…uota
cx53 (16 vCPU) 412'd persistently ('Resource limitation detected') in
both fsn1 and nbg1. A 412 resource-limit is the Hetzner project quota,
which is account-level and region-global, so the earlier nbg1 region
switch never helped. cx43 (8 vCPU / 16 GB) sits under the quota and
still clears native-image's >6.15 GB memory-bound build. Reverts
location to the org-default fsn1.
Refs promptLM/.github#38
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
Author
|
Pushed a commit pivoting cx53 → cx43 (and The cx53 Diagnosis + cost are documented in promptLM/.github#38 (now closed). To get cx53 back, request a Hetzner limit increase (account → Limits → Request change) first. |
Bundle-release workflow run inside the Gitea Actions runner completes with conclusion=failure rather than success. Reproduced locally on macOS Docker Desktop 29.5.2 with the same assertion-failure message, so this is not Hetzner-specific and not introduced by PR #390 — same pattern as the two NativeCliSmokeTest failures (#392, #393) that surfaced once full-profile CI started exercising these paths. Disable to unblock PR #390. Resolving #394 should remove the annotation and add a Gitea Actions task-log dumper to the test fixture so future failures self-diagnose. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LifecyclePipelineTest's first real CI run revealed the Save button stays disabled because the v2 SPA's form validation rejects blanks on three additional required fields that the scenario didn't populate: - description — validateMetadata in promptlm-web-ui - request.vendor — validateModelConfiguration - request.model — validateModelConfiguration (see components/promptlm-web-ui/src/features/prompt-editor/validation.ts) Playwright found the testid `save-prompt-button` correctly, but the element stayed disabled for 30s of retries and click timed out: - locator resolved to <button disabled ... data-testid="save-prompt-button">Create</button> - 60+ × "element is not enabled" - TimeoutError: Timeout 30000ms exceeded. Add three StudioDriver verbs (fillDescription, selectVendor, setModel) with testids derived directly from `components/ui/src/prompts-v2/form/ sections.tsx` (description-text, request-vendor-select, request-model-select). Wire them into the lifecycle scenario before clickSave(). Local compile-check passes; the field selectors match the SPA source. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… to SAVED via UI Verified end-to-end against native binaries on macOS arm64 with headless Playwright + Docker Desktop: Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 (19.23 s) Three fixes uncovered by running the scenario locally (matches CI): 1. StudioDriver.setupFirstProject — the native webapp boots with no active project, so any navigation triggers the MUI "Project Setup Required" dialog. Its overlay intercepts pointer events and Playwright clicks on the form's Save button get blocked. Mirror the pattern from NativeWebappUiSmokeTest: drive the dialog as a first-run user would (ProjectSetupHelper.createNewProject), invoked via a new studio.setupFirstProject() verb. 2. StudioDriver.fillSystemMessage — the default new-prompt draft seeds TWO messages (system + user) both empty. validateMessages rejects any empty content, so the system message must be filled even though it has no testid (target by aria-label "Message content 1"). With that filled plus description / vendor / model / user-message, the form validates and the Save button enables. 3. StudioDriver.Toolbar.clickPush — mirror clickCommit's tolerance pattern: when the SPA has no separate Push button (#389), no-op instead of timing out on a missing role match. Also: WorkspaceState.refreshRemoteTracking now takes (username, token) because NativeBinaryFixture only threads REPO_REMOTE_* sysprops to the spawned native subprocesses, not to the test JVM running JGit. Gitea requires auth even for read; passing creds explicitly closes the gap. Scope correction in the scenario: the v2 SPA's primary action persists the draft YAML but doesn't auto-commit and push, and the only path to PUSHED via UI today goes through "Save & release" which trips PreReleaseExecuteGate (#393). Until #389 separates commit/push into discrete actions, the scenario reaches SAVED — which is enough to exercise the full harness scaffolding (NativeBinaryFixture + StudioDriver + WorkspaceState + project-setup dialog). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…g UI tests NativeWebappUiSmokeTest and HappyPathUserJourneyTest both timed out for 900s on the disabled save-prompt-button on the previous PR #390 run, hitting the exact same form-validation gap LifecyclePipelineTest hit before I fixed it: the v2 SPA's validation requires non-blank vendor + model + every message content, and neither test fills these when authoring a prompt through Studio. Apply the same fix to both: - PromptWorkflowHelper.createPromptViaForm (used by NativeWebappUiSmokeTest) - HappyPathUserJourneyTest's inline form-fill block Each now: selectOption("anthropic") on `request-vendor-select`, fills `request-model-select` with claude-sonnet-4-5, and fills the system message via aria-label "Message content 1" (the default new-prompt draft seeds {role:'system'} + {role:'user'} both empty, and the system row has no testid). Speculative push — local verification was interrupted. If HappyPath still errors after this, it's the separate native-image SSE bug on ResponseBodyEmitter / StoreStatusEvent record components, not the form gate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
@WithNativeApp+NativeBinaryFixture) that boots the native CLI and webapp binaries (apps/promptlm-cli/target/promptlm-cli,apps/promptlm-webapp/target/promptlm-webapp), with Gitea / Artifactory / OpenAI-compatible LLM stub on demand. Built on top of the existingNativeBinaryLauncher.StudioDriverPlaywright façade exposing user-shaped verbs only — nohttpClient()/apiClient()accessors anywhere. Hard rule: scenarios drive via Studio clicks or CLIexec(...); APIs/JGit/Gitea-REST are post-hoc witnesses.WorkspaceStateread-only witness that re-derives the fourPromptSpecLifecycleStatevalues from disk + JGit (independent of the server'sPromptSpecLifecycleDeriver, so divergence between the two surfaces is detectable).GiteaContentsClientREST witness for push verification (Remote 1).LifecycleFullPipelineScenario— first DRAFT → SAVED → COMMITTED → PUSHED end-to-end, driven entirely byStudioDriver+CliHandle, with three-way triangulation at PUSHED (UI badge + JGit derivation + Gitea contents API + branch-head SHA match).native-acceptanceMaven profile/test-group; existingintegrationandnative-smokegroups untouched..github/workflows/acceptance.yml: nightly cron +run-acceptancelabel-drivenfullprofile, builds native binaries on the Hetzner VM, runs the new group; per-PR runs still on thesmokeprofile. Timeout 60 → 90 min.provision-runner/teardown-runnerunchanged.This PR carries the
run-acceptancelabel deliberately, so the new full profile gets exercised against itself end-to-end before merge.Design context
Full design notes were produced in-conversation prior to implementation; key decisions:
promptlm-cli. Witnesses are read-only.-Dpromptlm.test.requireNativeBinaries=true) when binaries are absent.Decisions worth flagging on review
HttpServerrather than adding a WireMock dependency. Speaks OpenAIchat/completionsdirectly; replaceable later if WireMock features are needed.spring.ai.openai.base-url(notOPENAI_BASE_URL) is the property the native webapp consumes — verified against the webapp test sources.WorkspaceState.specOnDiskreturns aPromptSpecRefrecord, not a fullPromptSpec— the witness layer doesn't parse YAML; that's P2'sPromptSpecRoundtripTestjob.native-imagecold-install cost on thepre_runner_scriptfallback until Hetzner runner snapshot: bake in GraalVM + Playwright browsers for acceptance suite .github#36 lands; expected and documented.Related issues
Test plan
acceptanceworkflowsmokeprofile passes on this PR's CI run (unrelated to the new code, but confirms the smoke path still works).acceptanceworkflowfullprofile (label-triggered by this PR) builds native binaries on Hetzner and runsLifecycleFullPipelineScenarioto green.fullrun is red, diagnostics + workspace snapshots are uploaded asacceptance-tests-<sha>artifact.integrationandnative-smokegroups still pass.🤖 Generated with Claude Code