← back to Cli Printing Press
ci(ci): split fast and full test lanes (#357)
9e260025fa5b357c2750c97d95ca573a6a3a9edf · 2026-04-27 22:54:35 -0700 · Trevin Chow
Files touched
M .github/workflows/lint.ymlM internal/browsersniff/classifier.goM internal/cli/verify_skill_test.goM internal/cli/verify_skill_unknown_command_test.goM internal/generator/generator_test.goM internal/generator/session_handshake_test.goM internal/openapi/parser_test.go
Diff
commit 9e260025fa5b357c2750c97d95ca573a6a3a9edf
Author: Trevin Chow <trevin@trevinchow.com>
Date: Mon Apr 27 22:54:35 2026 -0700
ci(ci): split fast and full test lanes (#357)
---
.github/workflows/lint.yml | 115 +++++++++++++++++++++-
internal/browsersniff/classifier.go | 11 ++-
internal/cli/verify_skill_test.go | 8 ++
internal/cli/verify_skill_unknown_command_test.go | 6 ++
internal/generator/generator_test.go | 17 ++++
internal/generator/session_handshake_test.go | 6 ++
internal/openapi/parser_test.go | 4 +
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index af73f666..d142f5a1 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -1,4 +1,4 @@
-name: Lint
+name: CI
on:
pull_request:
@@ -62,7 +62,7 @@ jobs:
test:
runs-on: ubuntu-latest
- timeout-minutes: 15
+ timeout-minutes: 10
steps:
- uses: actions/checkout@v6
@@ -73,4 +73,113 @@ jobs:
cache-dependency-path: go.sum
- name: Run Go tests
- run: go test ./...
+ shell: bash
+ run: |
+ set -o pipefail
+
+ go test -short -json -timeout 12m ./... 2>&1 | tee /tmp/go-test.json
+ status=${PIPESTATUS[0]}
+
+ python3 - /tmp/go-test.json <<'PY'
+ import json
+ import sys
+
+ packages = {}
+ tests = 0
+ with open(sys.argv[1]) as events:
+ for line in events:
+ try:
+ event = json.loads(line)
+ except Exception:
+ continue
+ if event.get("Action") == "pass" and event.get("Test"):
+ tests += 1
+ if event.get("Action") in {"pass", "fail"} and event.get("Test") is None:
+ packages[event["Package"]] = event.get("Elapsed", 0)
+
+ print("Slowest Go packages:")
+ for pkg, elapsed in sorted(packages.items(), key=lambda item: -item[1])[:15]:
+ print(f"{elapsed:6.1f}s {pkg}")
+ print(f"test_pass_events {tests}")
+ PY
+
+ exit "$status"
+
+ generated-test:
+ needs: test
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+
+ - name: Check whether full generated tests are needed
+ id: changes
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ git fetch --no-tags --prune origin "${{ github.base_ref }}"
+ mapfile -t changed < <(git diff --name-only --diff-filter=ACMR "origin/${{ github.base_ref }}...HEAD")
+
+ echo "Changed files:"
+ printf ' %s\n' "${changed[@]}"
+
+ needs_full=false
+ for file in "${changed[@]}"; do
+ case "$file" in
+ .github/workflows/lint.yml|go.mod|go.sum|cmd/*|catalog/*|internal/cli/*|internal/generator/*|internal/openapi/*|internal/pipeline/*|internal/spec/*|testdata/*)
+ needs_full=true
+ break
+ ;;
+ esac
+ done
+
+ echo "needs_full=$needs_full" >> "$GITHUB_OUTPUT"
+
+ - name: Skip full generated tests
+ if: steps.changes.outputs.needs_full != 'true'
+ run: |
+ echo "Skipping full generated tests: no generator, parser, pipeline, CLI, module, workflow, catalog, or testdata changes."
+
+ - uses: actions/setup-go@v6
+ if: steps.changes.outputs.needs_full == 'true'
+ with:
+ go-version-file: go.mod
+ cache: true
+ cache-dependency-path: go.sum
+
+ - name: Run full Go tests
+ if: steps.changes.outputs.needs_full == 'true'
+ shell: bash
+ run: |
+ set -o pipefail
+
+ go test -json -timeout 12m ./... 2>&1 | tee /tmp/go-test-full.json
+ status=${PIPESTATUS[0]}
+
+ python3 - /tmp/go-test-full.json <<'PY'
+ import json
+ import sys
+
+ packages = {}
+ tests = 0
+ with open(sys.argv[1]) as events:
+ for line in events:
+ try:
+ event = json.loads(line)
+ except Exception:
+ continue
+ if event.get("Action") == "pass" and event.get("Test"):
+ tests += 1
+ if event.get("Action") in {"pass", "fail"} and event.get("Test") is None:
+ packages[event["Package"]] = event.get("Elapsed", 0)
+
+ print("Slowest Go packages:")
+ for pkg, elapsed in sorted(packages.items(), key=lambda item: -item[1])[:15]:
+ print(f"{elapsed:6.1f}s {pkg}")
+ print(f"test_pass_events {tests}")
+ PY
+
+ exit "$status"
diff --git a/internal/browsersniff/classifier.go b/internal/browsersniff/classifier.go
index 2606f423..a18b57d3 100644
--- a/internal/browsersniff/classifier.go
+++ b/internal/browsersniff/classifier.go
@@ -6,6 +6,7 @@ import (
"net/url"
"regexp"
"strings"
+ "sync"
)
type EndpointGroup struct {
@@ -18,6 +19,7 @@ var (
uuidSegmentPattern = regexp.MustCompile(`(?i)^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`)
hashSegmentPattern = regexp.MustCompile(`(?i)^[0-9a-f]{32,}$`)
numericPattern = regexp.MustCompile(`^\d+$`)
+ blocklistMu sync.RWMutex
additionalBlocklist []string
)
@@ -25,7 +27,11 @@ func ClassifyEntries(entries []EnrichedEntry) (api []EnrichedEntry, noise []Enri
api = make([]EnrichedEntry, 0, len(entries))
noise = make([]EnrichedEntry, 0, len(entries))
- blocklist := append(DefaultBlocklist(), additionalBlocklist...)
+ blocklistMu.RLock()
+ extraBlocklist := append([]string(nil), additionalBlocklist...)
+ blocklistMu.RUnlock()
+
+ blocklist := append(DefaultBlocklist(), extraBlocklist...)
for _, entry := range entries {
score := scoreEntry(entry, blocklist)
classified := entry
@@ -45,6 +51,9 @@ func ClassifyEntries(entries []EnrichedEntry) (api []EnrichedEntry, noise []Enri
}
func SetAdditionalBlocklist(domains []string) {
+ blocklistMu.Lock()
+ defer blocklistMu.Unlock()
+
additionalBlocklist = append([]string(nil), domains...)
}
diff --git a/internal/cli/verify_skill_test.go b/internal/cli/verify_skill_test.go
index 1eed77db..25966649 100644
--- a/internal/cli/verify_skill_test.go
+++ b/internal/cli/verify_skill_test.go
@@ -17,6 +17,8 @@ import (
// `printing-press verify-skill` catches it at generation time instead of
// letting it ship to the library.
func TestVerifySkill_DetectsWrongFlagOnCommand(t *testing.T) {
+ t.Parallel()
+
bin := buildPrintingPressBinary(t)
dir := t.TempDir()
@@ -82,6 +84,8 @@ fixture-pp-cli search "chicken" --max-time 30m
// the verifier does NOT report a false-positive flag-commands finding
// when the SKILL example uses a flag declared on the top-level command.
func TestVerifySkill_NoFalsePositiveOnSharedLeafName(t *testing.T) {
+ t.Parallel()
+
bin := buildPrintingPressBinary(t)
dir := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(dir, "internal", "cli"), 0o755))
@@ -146,6 +150,8 @@ func newProfileSaveCmd() *cobra.Command {
// TestVerifySkill_PassesWhenSkillMatches confirms the verifier doesn't
// false-positive on a well-formed CLI.
func TestVerifySkill_PassesWhenSkillMatches(t *testing.T) {
+ t.Parallel()
+
bin := buildPrintingPressBinary(t)
dir := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(dir, "internal", "cli"), 0o755))
@@ -169,6 +175,8 @@ func newSearchCmd() *cobra.Command {
// TestVerifySkill_RejectsMissingInputs confirms usage errors (code 2).
func TestVerifySkill_RejectsMissingInputs(t *testing.T) {
+ t.Parallel()
+
bin := buildPrintingPressBinary(t)
// Missing --dir
diff --git a/internal/cli/verify_skill_unknown_command_test.go b/internal/cli/verify_skill_unknown_command_test.go
index 77607c5f..e8458814 100644
--- a/internal/cli/verify_skill_unknown_command_test.go
+++ b/internal/cli/verify_skill_unknown_command_test.go
@@ -16,6 +16,8 @@ import (
// path (`<cli> qr get-qrcode`) for a resource the cobra source actually
// registers as a leaf (`<cli> qr`) is rejected.
func TestVerifySkill_DetectsUnknownCommand(t *testing.T) {
+ t.Parallel()
+
bin := buildPrintingPressBinary(t)
dir := t.TempDir()
@@ -68,6 +70,8 @@ description: "fixture"
// negative case: a SKILL whose command-reference paths all map to real
// cobra Use: declarations passes the unknown-command check.
func TestVerifySkill_UnknownCommandPassesWhenAllPathsResolve(t *testing.T) {
+ t.Parallel()
+
bin := buildPrintingPressBinary(t)
dir := t.TempDir()
@@ -112,6 +116,8 @@ description: "fixture"
// built-in commands (help, completion, version) are whitelisted — references
// to `<cli> help` in SKILL.md must NOT fire unknown-command.
func TestVerifySkill_UnknownCommandSkipsBuiltins(t *testing.T) {
+ t.Parallel()
+
bin := buildPrintingPressBinary(t)
dir := t.TempDir()
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index bb59ae70..87ff23de 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -71,7 +71,10 @@ func TestGenerateProjectsCompile(t *testing.T) {
}
for _, tt := range tests {
+ tt := tt //nolint:modernize // keep the parallel subtest capture explicit
t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
apiSpec, err := spec.Parse(tt.specPath)
require.NoError(t, err)
@@ -523,6 +526,14 @@ func countFiles(t *testing.T, root string) int {
func runGoCommand(t *testing.T, dir string, args ...string) {
t.Helper()
+ if testing.Short() && len(args) > 0 && (args[0] == "build" || args[0] == "test") {
+ t.Skip("generated CLI compile tests run in the full generated-test CI lane")
+ }
+ runGoCommandRequired(t, dir, args...)
+}
+
+func runGoCommandRequired(t *testing.T, dir string, args ...string) {
+ t.Helper()
// Generated-project compile tests exercise module resolution via -mod=mod;
// production Validate still owns the go mod tidy quality gate.
@@ -1039,6 +1050,8 @@ func TestGenerateHTMLExtractionPerModeGating(t *testing.T) {
}
t.Run("embedded-json-only omits page+links helpers", func(t *testing.T) {
+ t.Parallel()
+
dir := filepath.Join(t.TempDir(), "ejonly-pp-cli")
require.NoError(t, New(specWithMode("ejonly", spec.HTMLExtractModeEmbeddedJSON), dir).Generate())
body := read(t, dir)
@@ -1064,6 +1077,8 @@ func TestGenerateHTMLExtractionPerModeGating(t *testing.T) {
})
t.Run("page-only omits embedded-json helpers", func(t *testing.T) {
+ t.Parallel()
+
dir := filepath.Join(t.TempDir(), "pageonly-pp-cli")
require.NoError(t, New(specWithMode("pageonly", spec.HTMLExtractModePage), dir).Generate())
body := read(t, dir)
@@ -1082,6 +1097,8 @@ func TestGenerateHTMLExtractionPerModeGating(t *testing.T) {
})
t.Run("mixed modes emit both branches", func(t *testing.T) {
+ t.Parallel()
+
// One endpoint per mode in the same spec.
mixedSpec := &spec.APISpec{
Name: "mixed",
diff --git a/internal/generator/session_handshake_test.go b/internal/generator/session_handshake_test.go
index 22b8aa40..a3c881fc 100644
--- a/internal/generator/session_handshake_test.go
+++ b/internal/generator/session_handshake_test.go
@@ -13,6 +13,8 @@ import (
// session.go helper and wires it into the client when auth.type is
// "session_handshake". This is the WU-2 acceptance test (retro issue #174).
func TestSessionHandshakeGeneration(t *testing.T) {
+ t.Parallel()
+
sp := &spec.APISpec{
Name: "demo",
Version: "1.0.0",
@@ -104,6 +106,8 @@ func TestSessionHandshakeGeneration(t *testing.T) {
}
func TestSessionHandshakeBrowserTransportSharesJar(t *testing.T) {
+ t.Parallel()
+
sp := &spec.APISpec{
Name: "demo",
Version: "1.0.0",
@@ -171,6 +175,8 @@ func TestSessionHandshakeBrowserTransportSharesJar(t *testing.T) {
// TestSessionHandshakeNotEmittedForOtherAuth verifies the session helper is
// NOT emitted for non-session auth types — no file bloat for bearer_token CLIs.
func TestSessionHandshakeNotEmittedForOtherAuth(t *testing.T) {
+ t.Parallel()
+
sp := &spec.APISpec{
Name: "demo",
Version: "1.0.0",
diff --git a/internal/openapi/parser_test.go b/internal/openapi/parser_test.go
index 6052e853..8da9694d 100644
--- a/internal/openapi/parser_test.go
+++ b/internal/openapi/parser_test.go
@@ -168,6 +168,9 @@ resources:
func TestGenerateFromOpenAPICompiles(t *testing.T) {
t.Parallel()
+ if testing.Short() {
+ t.Skip("OpenAPI generated CLI compile coverage runs in the generated-test CI lane")
+ }
tests := []struct {
name string
@@ -178,6 +181,7 @@ func TestGenerateFromOpenAPICompiles(t *testing.T) {
}
for _, tt := range tests {
+ tt := tt //nolint:modernize // keep the parallel subtest capture explicit
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
← 7f248d59 feat(cli): emit MCP tools for novel CLI features via shell-o
·
back to Cli Printing Press
·
fix(cli): manifest emission no longer gated by stale mcp_rea 2e892e36 →