Files
mathiasandClaude Sonnet 5 805b76d7c3
CD / Detect unsubstituted template (push) Successful in 0s
CD / Lint / Test / Vet (push) Successful in 5s
CD / var-go/oath (push) Has been skipped
CD / Build & Import (push) Successful in 14s
CD / Deploy via GitOps (push) Successful in 1s
feat(oath): gate cad-atlas's own real candidate, not swedsl's toy stub (#8)
oathcandidate/ is a separate Go module (mirrors swedsl's own
oath/testdata/selfcandidate pattern, keeping var-go's transitive deps
out of the deployed atlas binary) whose Build() parses the committed
.gitea/workflows/cd.yml and checks the "oath" job exists and invokes
cmd/vargo-gate. TDD: passes against the real file, fails closed on a
fixture missing the job.

Rewires the oath CI job to go-run vargo-gate from its real module path
(git.d-ma.be/mathias/swedsl/oath/cmd/vargo-gate@oath/v0.28.0, unblocked
by swedsl#35/#38) against VARGO_CANDIDATE_DIR=oathcandidate, instead of
checking out swedsl and gating its hardcoded toy fixture. Private-module
auth via a short-lived GIT_ASKPASS script (token never in argv, never
written to git config, matches act_runner's env:-block-with-secrets
gotcha).

Discovered along the way: var-go's parser needs single-line,
period-separated oath sentences with no Given/When/Then/And keyword
stripping — this repo's older oaths (incl. #1) used an unverified
multi-line keyword-prefixed style. #8's oath uses the proven format.

Still not required by branch protection pending a real-PR confirmation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 14:33:17 +02:00

80 lines
3.1 KiB
Go

package oathcandidate
import (
"os"
"path/filepath"
"testing"
oath "git.d-ma.be/mathias/swedsl/oath"
)
// realOath is cad-atlas#8's actual oath text — the same var block committed to
// that issue. Gating it against the REAL checked-out .gitea/workflows/cd.yml
// proves the candidate reads real CI config, not a fixture standing in for it.
//
// Format note (discovered writing this test): var-go's parser requires a
// SINGLE-LINE paragraph — sentences are split by "." within that line, not by
// newline — and does NOT strip Given/When/Then/And keywords before matching a
// step. cad-atlas's older oaths (e.g. issue #1) use a multi-line, keyword-prefixed
// style that was never actually exercised against this parser (every prior gate
// run errored before reaching real sentence matching). Plain declarative
// sentences, period-separated, one line — see swedsl's own gate_test.go fixtures.
const realOath = "```var\n" +
`the CI workflow file ".gitea/workflows/cd.yml" is parsed. it defines a job named "oath". the job named "oath" invokes "cmd/vargo-gate".` +
"\n```\n"
// TestBuild_GatesRealWorkflow is named before Build existed (TDD): it fails to
// compile until Build() and the workflow-parsing steps exist, and fails to pass
// until they parse the REAL committed cd.yml correctly — this is the file that
// must go from red to green, not a mock.
func TestBuild_GatesRealWorkflow(t *testing.T) {
// go test's cwd is already this package's dir (oathcandidate/), matching
// SubprocessGate's cmd.Dir = candidateModuleDir contract exactly — no chdir
// needed to reproduce it here.
verdict, err := oath.Gate([]byte(realOath), Build())
if err != nil {
t.Fatalf("Gate returned error: %v", err)
}
if !verdict.Pass {
if verdict.Failure != nil {
t.Fatalf("Gate did not pass: failure=%+v", *verdict.Failure)
}
t.Fatalf("Gate did not pass against the real committed cd.yml: %+v", verdict)
}
}
// TestBuild_FailsClosedOnMissingJob proves the candidate is a REAL check, not a
// rubber stamp: gating a workflow file that has no "oath" job must fail.
func TestBuild_FailsClosedOnMissingJob(t *testing.T) {
dir := t.TempDir()
workflowsDir := filepath.Join(dir, ".gitea", "workflows")
if err := os.MkdirAll(workflowsDir, 0o755); err != nil {
t.Fatal(err)
}
noOathJob := "jobs:\n check:\n steps:\n - run: go test ./...\n"
if err := os.WriteFile(filepath.Join(workflowsDir, "cd.yml"), []byte(noOathJob), 0o644); err != nil {
t.Fatal(err)
}
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
// SubprocessGate always runs the candidate with cmd.Dir = candidateModuleDir,
// one level under the repo root (cad-atlas's real layout) — reproduce that by
// chdir-ing into a sibling "candidate/" dir under the fixture root.
candDir := filepath.Join(dir, "candidate")
if err := os.MkdirAll(candDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.Chdir(candDir); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.Chdir(cwd) })
verdict, err := oath.Gate([]byte(realOath), Build())
if err == nil && verdict.Pass {
t.Fatalf("expected the gate to fail closed on a workflow with no oath job, got Pass=true")
}
}