mirror of
https://github.com/lukaszraczylo/llm-testbench.git
synced 2026-10-12 00:45:19 +00:00
Add a new delivery category with three subcategories: git, containers, and release-engineering. Each subcategory holds 10 self-contained, deterministically-scored tests, grounded in real git, Docker, and release-engineering behaviour. The git subcategory covers bisect step derivation, rebase versus merge, conventional-commit classification, git worktree, force-with-lease versus bare force, hook choice, detached-HEAD recovery, cherry-pick versus revert, a .gitignore negation trace, and stash-pop conflict behaviour. Two of these tests recompute their ground truth against a real, throwaway git repository created in t.TempDir, and skip if git is not installed. The containers subcategory covers Dockerfile layer-cache busting, ENTRYPOINT and CMD interaction, multi-stage image size, non-root users and capabilities, HEALTHCHECK semantics, .dockerignore effects, COPY versus ADD, build-arg versus runtime env, and layer counting. The release-engineering subcategory covers semver bump derivation from a changelog, the clean-tree requirement, rollback ordering, canary versus blue-green, tag-to-release sequencing, commit-to-changelog mapping, checksum and signing purpose, pipeline stage ordering, pin versus range dependency policy, and hotfix flow ordering. A shared negation-aware evaluator helper, local to this category's files, scores a forbidden phrase only when it appears unnegated, so a response correctly warning against bare --force or a silent tracked-file mutation still scores full credit. registerDeliveryTests wires the three subcategories together and is exercised only by a new wiring test in this worktree, since catalog.go and catalog_test.go stay untouched until the orchestrator integrates this category centrally.
418 lines
18 KiB
Go
418 lines
18 KiB
Go
package tests
|
|
|
|
import (
|
|
"regexp"
|
|
|
|
"github.com/lukaszraczylo/llm-testbench/internal/eval"
|
|
"github.com/lukaszraczylo/llm-testbench/internal/testkit"
|
|
)
|
|
|
|
func registerDeliveryReleaseTests(r *testkit.Registry) {
|
|
r.Register(delRelSemverBumpChangelogTest())
|
|
r.Register(delRelCleanTreeRequirementTest())
|
|
r.Register(delRelRollbackOrderingTest())
|
|
r.Register(delRelCanaryVsBlueGreenTest())
|
|
r.Register(delRelTagToReleaseSequenceTest())
|
|
r.Register(delRelCommitChangelogMappingTest())
|
|
r.Register(delRelChecksumsSigningPurposeTest())
|
|
r.Register(delRelPipelineStageOrderingTest())
|
|
r.Register(delRelPinVsRangePolicyTest())
|
|
r.Register(delRelHotfixFlowOrderingTest())
|
|
}
|
|
|
|
// delRelSemverBumpChangelogTest: derive the next semver version from a
|
|
// merged-change list dominated by one feat commit.
|
|
//
|
|
// ground truth: of the four merged changes, the fix is a patch-level
|
|
// change, the chore and docs entries are not release-relevant on their own,
|
|
// and the feat entry adds a new, backward-compatible, optional query
|
|
// parameter - a minor-level change under Semantic Versioning. The highest
|
|
// severity present (feat) determines the bump: v2.3.1 with a minor bump
|
|
// becomes 2.4.0 (MINOR increments, PATCH resets to 0).
|
|
func delRelSemverBumpChangelogTest() testkit.Test {
|
|
prompt := `Since the last release v2.3.1, these changes have merged:
|
|
|
|
- fix(auth): correct token refresh race condition
|
|
- feat(api): add optional "include_deleted" query parameter to GET /items (backward compatible)
|
|
- chore(ci): cache go build output between runs
|
|
- docs: fix typo in CONTRIBUTING.md
|
|
|
|
None of these changes removes or renames any existing field, endpoint, or
|
|
behavior. Per Semantic Versioning, what is the next version number?
|
|
Respond with only a JSON object: {"version":"X.Y.Z"}`
|
|
|
|
return testkit.Test{
|
|
ID: "rel-semver-bump-changelog",
|
|
Category: "delivery",
|
|
Subcategory: "release-engineering",
|
|
Description: "Derive the next semver version (minor bump to 2.4.0) from a merged-change list dominated by one backward-compatible feat.",
|
|
Prompt: prompt,
|
|
Eval: eval.JSONField("version", "2.4.0"),
|
|
}
|
|
}
|
|
|
|
// delRelSilentlyModifyPattern matches "silently modif(y|ies|ying)" -
|
|
// delRelCleanTreeRequirementTest's forbidden phrase, deliberately quoted
|
|
// from the prompt's own wording so a response that engages with the
|
|
// question at all is likely to reuse it, whether to recommend it (wrong,
|
|
// unnegated) or warn against it (correct, negated).
|
|
var delRelSilentlyModifyPattern = regexp.MustCompile(`(?i)silently modif\w*`)
|
|
|
|
// delRelCleanTreeRequirementTest: explain why release tooling requires a
|
|
// clean git tree, and require that a hook silently mutating tracked files
|
|
// mid-release is only ever mentioned in a negated (warned-against) form.
|
|
//
|
|
// ground truth: release tooling (e.g. goreleaser) refuses to run against a
|
|
// dirty tree because the published artifact, its checksums, and its
|
|
// signature must correspond exactly to a known, reviewed commit; a hook
|
|
// that silently modifies tracked files right before packaging would ship
|
|
// code that was never actually committed or reviewed, breaking that
|
|
// guarantee, so it must never be allowed - not merely worked around.
|
|
func delRelCleanTreeRequirementTest() testkit.Test {
|
|
prompt := `Your release pipeline runs a release tool (for example
|
|
goreleaser) against a tagged commit. A teammate proposes adding a pre-build
|
|
hook that would silently modify tracked source files (auto-formatting them)
|
|
immediately before the tool packages the release, with no separate commit
|
|
for the change.
|
|
|
|
In 2-3 sentences: explain why release tooling requires a clean git working
|
|
tree before it runs, and say whether a pre-build hook should be allowed to
|
|
silently modify tracked files this way.`
|
|
|
|
evaluator := eval.All(
|
|
eval.W(eval.ContainsAny("clean tree", "clean working tree", "clean git tree", "uncommitted", "dirty"), 2),
|
|
eval.W(delNoUnnegatedMention(delRelSilentlyModifyPattern, "no unsafe recommendation to silently modify tracked files"), 2),
|
|
)
|
|
|
|
return testkit.Test{
|
|
ID: "rel-clean-tree-requirement",
|
|
Category: "delivery",
|
|
Subcategory: "release-engineering",
|
|
Description: "Explain the clean-tree requirement and require any mention of a hook silently mutating tracked files to be negated.",
|
|
Prompt: prompt,
|
|
Eval: evaluator,
|
|
}
|
|
}
|
|
|
|
// delRelRollbackOrderingWant is the one defensible incident-response order
|
|
// for delRelRollbackOrderingTest.
|
|
//
|
|
// ground truth: page-oncall first, so the response is coordinated and no
|
|
// one else takes a conflicting action at the same time; then
|
|
// stop-new-traffic-if-needed, to bound ongoing user harm before the actual
|
|
// fix lands; then redeploy-previous-image, the fix itself; then
|
|
// confirm-health-checks-pass, verifying the fix actually worked before
|
|
// declaring the incident over; then post-incident-notes, written up only
|
|
// once the incident is resolved.
|
|
var delRelRollbackOrderingWant = []string{
|
|
"page-oncall",
|
|
"stop-new-traffic-if-needed",
|
|
"redeploy-previous-image",
|
|
"confirm-health-checks-pass",
|
|
"post-incident-notes",
|
|
}
|
|
|
|
// delRelRollbackOrderingTest: order the steps of a production rollback
|
|
// after a failed deploy.
|
|
func delRelRollbackOrderingTest() testkit.Test {
|
|
prompt := `Release v3.1.0 was just deployed and is now failing health
|
|
checks in production. The previous release v3.0.4 was healthy. Give the
|
|
ordered list of steps to safely roll back to the last known-good release,
|
|
choosing from and ordering exactly these labels:
|
|
["redeploy-previous-image", "page-oncall", "confirm-health-checks-pass",
|
|
"stop-new-traffic-if-needed", "post-incident-notes"]
|
|
|
|
Respond with only a JSON array of the labels, in the order you would
|
|
perform them.`
|
|
|
|
return testkit.Test{
|
|
ID: "rel-rollback-ordering",
|
|
Category: "delivery",
|
|
Subcategory: "release-engineering",
|
|
Description: "Order a production rollback: page-oncall, stop traffic, redeploy, confirm health, then post-incident notes.",
|
|
Prompt: prompt,
|
|
Eval: eval.JSONStringArrayEquals(delRelRollbackOrderingWant),
|
|
}
|
|
}
|
|
|
|
// delRelCanaryVsBlueGreenTest: pick canary for gradual percentage-based
|
|
// rollout, blue-green for instant all-or-nothing cutover.
|
|
//
|
|
// ground truth: canary is specifically the strategy that exposes a change
|
|
// to a small, gradually increasing percentage of real traffic while
|
|
// watching metrics, with the ability to halt at any percentage - exactly
|
|
// scenario_a. blue-green is specifically the strategy with two full,
|
|
// independently warmed environments and an instant all-or-nothing traffic
|
|
// switch that can flip back immediately - exactly scenario_b.
|
|
func delRelCanaryVsBlueGreenTest() testkit.Test {
|
|
prompt := `For each of these two situations, is "canary" or "blue-green"
|
|
the matching deployment strategy?
|
|
|
|
scenario_a: "Expose a risky new feature to a small percentage (e.g. 5%) of
|
|
real production traffic first, gradually increasing that percentage while
|
|
watching error-rate metrics, with the ability to automatically halt the
|
|
rollout at any percentage if metrics regress."
|
|
scenario_b: "An instant, all-or-nothing cutover from the old version to the
|
|
new version, using a full, previously warmed standby environment, so 100%
|
|
of traffic switches at once and can be flipped back to the old environment
|
|
instantly if anything goes wrong."
|
|
|
|
Respond with only a JSON object:
|
|
{"scenario_a":"canary"|"blue-green","scenario_b":"canary"|"blue-green"}`
|
|
|
|
evaluator := eval.Mean(
|
|
eval.JSONField("scenario_a", "canary"),
|
|
eval.JSONField("scenario_b", "blue-green"),
|
|
)
|
|
|
|
return testkit.Test{
|
|
ID: "rel-canary-vs-bluegreen",
|
|
Category: "delivery",
|
|
Subcategory: "release-engineering",
|
|
Description: "Choose canary for a gradual percentage rollout and blue-green for an instant all-or-nothing cutover.",
|
|
Prompt: prompt,
|
|
Eval: evaluator,
|
|
}
|
|
}
|
|
|
|
// delRelTagToReleaseWant is the one defensible order from a verified main
|
|
// branch to a published GitHub release with notes.
|
|
//
|
|
// ground truth: run-tests verifies main is releasable before it is tagged
|
|
// at all. create-git-tag then tags that verified commit, and push-tag
|
|
// publishes the tag to the remote - a prerequisite for anything that
|
|
// references it. generate-changelog builds release notes from the commits
|
|
// since the last tag. create-github-release publishes the GitHub release
|
|
// against the now-pushed tag, using those generated notes. announce comes
|
|
// last, only once the release genuinely exists.
|
|
var delRelTagToReleaseWant = []string{
|
|
"run-tests",
|
|
"create-git-tag",
|
|
"push-tag",
|
|
"generate-changelog",
|
|
"create-github-release",
|
|
"announce",
|
|
}
|
|
|
|
// delRelTagToReleaseSequenceTest: order the steps from a verified main
|
|
// branch to a published, announced GitHub release.
|
|
func delRelTagToReleaseSequenceTest() testkit.Test {
|
|
prompt := `Starting from an already-merged, untested-since-merge main
|
|
branch, give the ordered sequence of steps to reach a published,
|
|
announced GitHub release with generated release notes, choosing from and
|
|
ordering exactly these labels:
|
|
["run-tests", "create-git-tag", "push-tag", "generate-changelog",
|
|
"create-github-release", "announce"]
|
|
|
|
Respond with only a JSON array of the labels, in the order you would
|
|
perform them.`
|
|
|
|
return testkit.Test{
|
|
ID: "rel-tag-to-release-sequence",
|
|
Category: "delivery",
|
|
Subcategory: "release-engineering",
|
|
Description: "Order tests, tag, push-tag, changelog, GitHub release, and announcement from a merged main branch to a published release.",
|
|
Prompt: prompt,
|
|
Eval: eval.JSONStringArrayEquals(delRelTagToReleaseWant),
|
|
}
|
|
}
|
|
|
|
// delRelCommitChangelogMappingTest: map 4 commits to their Keep a Changelog
|
|
// section.
|
|
//
|
|
// ground truth (Keep a Changelog sections: Added, Changed, Deprecated,
|
|
// Removed, Fixed, Security): a plain "feat" commit adds new behavior ->
|
|
// Added. A plain "fix" commit -> Fixed. A "perf" commit changes existing
|
|
// behavior's characteristics without adding or fixing a defect -> Changed.
|
|
// A "fix(security)" commit specifically patches a vulnerability, which Keep
|
|
// a Changelog calls out as its own Security section rather than the
|
|
// generic Fixed section.
|
|
func delRelCommitChangelogMappingTest() testkit.Test {
|
|
prompt := `Using the Keep a Changelog section names (Added, Changed,
|
|
Deprecated, Removed, Fixed, Security), map each of these 4 commits to the
|
|
single changelog section it belongs in:
|
|
|
|
commit_a: "feat(runner): add bounded concurrency to the test fan-out via errgroup"
|
|
commit_b: "fix(eval): correct off-by-one in numeric extraction"
|
|
commit_c: "perf(exec): reuse the go env cache across evaluator calls"
|
|
commit_d: "fix(security): patch a path traversal vulnerability in the template loader"
|
|
|
|
Respond with only a JSON object:
|
|
{"commit_a":"...","commit_b":"...","commit_c":"...","commit_d":"..."}`
|
|
|
|
evaluator := eval.Mean(
|
|
eval.JSONField("commit_a", "Added"),
|
|
eval.JSONField("commit_b", "Fixed"),
|
|
eval.JSONField("commit_c", "Changed"),
|
|
eval.JSONField("commit_d", "Security"),
|
|
)
|
|
|
|
return testkit.Test{
|
|
ID: "rel-commit-changelog-mapping",
|
|
Category: "delivery",
|
|
Subcategory: "release-engineering",
|
|
Description: "Map feat/fix/perf/security-fix commits to their Keep a Changelog section, including the Security carve-out.",
|
|
Prompt: prompt,
|
|
Eval: evaluator,
|
|
}
|
|
}
|
|
|
|
// delRelChecksumsSigningPurposeTest: explain what a checksums file and its
|
|
// signature each protect against.
|
|
//
|
|
// ground truth: a checksums (e.g. SHA256) file lets a downloader verify the
|
|
// binary was not corrupted or truncated in transit - an integrity check.
|
|
// On its own, though, an attacker who swaps the binary can just regenerate
|
|
// a matching checksum, so a signature over the checksums file is what ties
|
|
// it back to a trusted publisher identity - an authenticity/provenance
|
|
// check that a bare checksum cannot provide by itself.
|
|
func delRelChecksumsSigningPurposeTest() testkit.Test {
|
|
prompt := `A release pipeline publishes SHA256 checksums alongside each
|
|
binary artifact, and additionally signs the checksums file with a GPG or
|
|
cosign key. In 1-2 sentences, explain what problem each of these two things
|
|
solves for someone downloading the release: the checksums file, and the
|
|
signature over it.`
|
|
|
|
evaluator := eval.All(
|
|
eval.W(eval.ContainsAny("integrity", "corrupt", "tamper", "not been altered", "not been modified"), 2),
|
|
eval.W(eval.ContainsAny("authentic", "provenance", "came from", "verify the publisher", "signed by", "identity"), 2),
|
|
)
|
|
|
|
return testkit.Test{
|
|
ID: "rel-checksums-signing-purpose",
|
|
Category: "delivery",
|
|
Subcategory: "release-engineering",
|
|
Description: "Explain checksums as an integrity check and the signature over them as an authenticity/provenance check.",
|
|
Prompt: prompt,
|
|
Eval: evaluator,
|
|
}
|
|
}
|
|
|
|
// delRelPipelineStageOrderingWant is the one defensible pipeline stage
|
|
// order for delRelPipelineStageOrderingTest.
|
|
//
|
|
// ground truth: lint runs first because it is the fastest check and fails
|
|
// fast on style/static issues before spending time on anything else; then
|
|
// unit-test (fast, isolated); then build, compiling/packaging only once the
|
|
// code is known-good; then integration-test, which needs the built
|
|
// artifact; then publish-artifact, only after both test tiers pass;
|
|
// deploy-staging promotes the published artifact to a pre-prod
|
|
// environment; deploy-production is the final promotion, only after
|
|
// staging is verified.
|
|
var delRelPipelineStageOrderingWant = []string{
|
|
"lint",
|
|
"unit-test",
|
|
"build",
|
|
"integration-test",
|
|
"publish-artifact",
|
|
"deploy-staging",
|
|
"deploy-production",
|
|
}
|
|
|
|
// delRelPipelineStageOrderingTest: order a CI/CD pipeline's stages.
|
|
func delRelPipelineStageOrderingTest() testkit.Test {
|
|
prompt := `Order these CI/CD pipeline stages into the sequence a
|
|
well-designed pipeline would run them in, fastest/cheapest checks first and
|
|
each stage only running once its prerequisites have passed:
|
|
["deploy-production", "build", "lint", "publish-artifact",
|
|
"integration-test", "deploy-staging", "unit-test"]
|
|
|
|
Respond with only a JSON array of the labels, in the order they should
|
|
run.`
|
|
|
|
return testkit.Test{
|
|
ID: "rel-pipeline-stage-ordering",
|
|
Category: "delivery",
|
|
Subcategory: "release-engineering",
|
|
Description: "Order pipeline stages fail-fast-first: lint, unit-test, build, integration-test, publish, staging, production.",
|
|
Prompt: prompt,
|
|
Eval: eval.JSONStringArrayEquals(delRelPipelineStageOrderingWant),
|
|
}
|
|
}
|
|
|
|
// delRelPinVsRangePolicyTest: pick exact pinning for an application's own
|
|
// dependencies, and a version range for a published library.
|
|
//
|
|
// ground truth: an application that is deployed (not consumed by other
|
|
// projects) should pin exact versions so every environment resolves the
|
|
// identical dependency graph, changing only via an explicit, reviewed bump
|
|
// - exactly scenario_a. A published library should instead declare a
|
|
// version range, because pinning an exact version would force every
|
|
// consumer into dependency-resolution conflicts whenever two libraries in
|
|
// their own dependency tree pin different exact versions of the same
|
|
// shared dependency - exactly scenario_b.
|
|
func delRelPinVsRangePolicyTest() testkit.Test {
|
|
prompt := `For each of these two situations, should dependencies be
|
|
declared as an exact pinned version, or as a version range?
|
|
|
|
scenario_a: "A production application's own direct dependencies, where
|
|
every environment (dev, CI, staging, production) must build against
|
|
exactly the same resolved dependency graph, with nothing changing
|
|
underneath it between releases without an explicit, reviewed bump."
|
|
scenario_b: "A publicly published library that other projects will import
|
|
as a dependency, where being overly strict would force every consumer into
|
|
dependency-resolution conflicts whenever two libraries in their own tree
|
|
pin different exact versions of the same shared dependency."
|
|
|
|
Respond with only a JSON object:
|
|
{"scenario_a":"pin"|"range","scenario_b":"pin"|"range"}`
|
|
|
|
evaluator := eval.Mean(
|
|
eval.JSONField("scenario_a", "pin"),
|
|
eval.JSONField("scenario_b", "range"),
|
|
)
|
|
|
|
return testkit.Test{
|
|
ID: "rel-pin-vs-range-policy",
|
|
Category: "delivery",
|
|
Subcategory: "release-engineering",
|
|
Description: "Choose exact pinning for an application's own dependencies and a version range for a published library.",
|
|
Prompt: prompt,
|
|
Eval: evaluator,
|
|
}
|
|
}
|
|
|
|
// delRelHotfixFlowOrderingWant is the one defensible order for an urgent
|
|
// production hotfix when main has diverged with unrelated unfinished work.
|
|
//
|
|
// ground truth: branch-from-production-tag starts from exactly the code
|
|
// currently running in production, not from main, which carries unrelated
|
|
// in-progress work that must not ship early. apply-minimal-fix makes the
|
|
// smallest change that resolves the incident, nothing else. run-tests
|
|
// verifies the fix before shipping it under time pressure.
|
|
// tag-hotfix-release cuts the release from this branch. deploy ships the
|
|
// fix to production immediately, the urgent action. merge-back-to-main
|
|
// comes last: it matters so the fix is not lost on the next regular
|
|
// release, but it does not block getting the fix into production first.
|
|
var delRelHotfixFlowOrderingWant = []string{
|
|
"branch-from-production-tag",
|
|
"apply-minimal-fix",
|
|
"run-tests",
|
|
"tag-hotfix-release",
|
|
"deploy",
|
|
"merge-back-to-main",
|
|
}
|
|
|
|
// delRelHotfixFlowOrderingTest: order an urgent hotfix flow when main has
|
|
// diverged with unrelated in-progress work that must not ship yet.
|
|
func delRelHotfixFlowOrderingTest() testkit.Test {
|
|
prompt := `Production is on fire and needs an urgent hotfix. The "main"
|
|
branch has since diverged with unrelated, unfinished work that must NOT
|
|
ship as part of this fix. Order these steps into the sequence you would
|
|
actually perform them in:
|
|
["merge-back-to-main", "tag-hotfix-release", "branch-from-production-tag",
|
|
"deploy", "run-tests", "apply-minimal-fix"]
|
|
|
|
Respond with only a JSON array of the labels, in the order you would
|
|
perform them.`
|
|
|
|
return testkit.Test{
|
|
ID: "rel-hotfix-flow-ordering",
|
|
Category: "delivery",
|
|
Subcategory: "release-engineering",
|
|
Description: "Order an urgent hotfix flow branched from production (not diverged main), through deploy, merging back to main last.",
|
|
Prompt: prompt,
|
|
Eval: eval.JSONStringArrayEquals(delRelHotfixFlowOrderingWant),
|
|
}
|
|
}
|