Drop Windows-only DPAPI export/import names from the darwin -run filter; those tests skip on macOS and already run under the Windows job. Co-authored-by: Cursor <cursoragent@cursor.com>
793 lines
24 KiB
Go
793 lines
24 KiB
Go
package scripts_test
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type changelogGateRepo struct {
|
|
root string
|
|
gate string
|
|
base string
|
|
}
|
|
|
|
const changelogGateBase = `# Changelog
|
|
|
|
## [Unreleased]
|
|
|
|
## [1.0.0] - 2026-07-01
|
|
|
|
### Added
|
|
|
|
- Initial release.
|
|
`
|
|
|
|
const changelogGateValidRelease = `# Changelog
|
|
|
|
## [Unreleased]
|
|
|
|
## [1.0.1-beta.1] - 2026-07-17
|
|
|
|
### Changed
|
|
|
|
- Valid release note.
|
|
|
|
## [1.0.0] - 2026-07-01
|
|
|
|
### Added
|
|
|
|
- Initial release.
|
|
`
|
|
|
|
func newChangelogGateRepo(t *testing.T) *changelogGateRepo {
|
|
t.Helper()
|
|
|
|
sourceRoot, err := filepath.Abs(filepath.Join("..", ".."))
|
|
if err != nil {
|
|
t.Fatalf("Abs(repo root) error = %v", err)
|
|
}
|
|
root := t.TempDir()
|
|
|
|
for _, path := range []string{
|
|
"LICENSE",
|
|
"NOTICE",
|
|
"README.md",
|
|
"CONTRIBUTING.md",
|
|
"SECURITY.md",
|
|
"CODE_OF_CONDUCT.md",
|
|
".env.example",
|
|
".github/workflows/ci.yml",
|
|
".github/PULL_REQUEST_TEMPLATE.md",
|
|
"docs/architecture.md",
|
|
"scripts/README.md",
|
|
"build/README.md",
|
|
} {
|
|
changelogGateWrite(t, root, path, "fixture\n", 0o644)
|
|
}
|
|
for _, path := range []string{
|
|
"scripts/policy/check-changelog-pr.sh",
|
|
"scripts/policy/open-source-audit.sh",
|
|
"scripts/release/release-lib.sh",
|
|
} {
|
|
data, err := os.ReadFile(filepath.Join(sourceRoot, path))
|
|
if err != nil {
|
|
t.Fatalf("ReadFile(%s) error = %v", path, err)
|
|
}
|
|
mode := os.FileMode(0o644)
|
|
if strings.HasSuffix(path, ".sh") {
|
|
mode = 0o755
|
|
}
|
|
changelogGateWrite(t, root, path, string(data), mode)
|
|
}
|
|
changelogGateWrite(t, root, "CHANGELOG.md", changelogGateBase, 0o644)
|
|
|
|
changelogGateGit(t, root, "init", "-b", "main")
|
|
changelogGateGit(t, root, "config", "user.name", "Changelog Gate Test")
|
|
changelogGateGit(t, root, "config", "user.email", "changelog-gate@example.com")
|
|
changelogGateGit(t, root, "add", ".")
|
|
changelogGateGit(t, root, "commit", "-m", "seed repository")
|
|
|
|
return &changelogGateRepo{
|
|
root: root,
|
|
gate: filepath.Join(root, "scripts", "policy", "check-changelog-pr.sh"),
|
|
base: strings.TrimSpace(changelogGateGit(t, root, "rev-parse", "HEAD")),
|
|
}
|
|
}
|
|
|
|
func changelogGateWrite(t *testing.T, root, path, content string, mode os.FileMode) {
|
|
t.Helper()
|
|
full := filepath.Join(root, path)
|
|
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
|
|
t.Fatalf("MkdirAll(%s) error = %v", filepath.Dir(full), err)
|
|
}
|
|
if err := os.WriteFile(full, []byte(content), mode); err != nil {
|
|
t.Fatalf("WriteFile(%s) error = %v", full, err)
|
|
}
|
|
}
|
|
|
|
func changelogGateGit(t *testing.T, root string, args ...string) string {
|
|
t.Helper()
|
|
cmd := exec.Command("git", args...)
|
|
cmd.Dir = root
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("git %s error = %v\noutput:\n%s", strings.Join(args, " "), err, output)
|
|
}
|
|
return string(output)
|
|
}
|
|
|
|
func (r *changelogGateRepo) commit(t *testing.T, message string) {
|
|
t.Helper()
|
|
changelogGateGit(t, r.root, "add", "-A")
|
|
changelogGateGit(t, r.root, "commit", "-m", message)
|
|
}
|
|
|
|
func (r *changelogGateRepo) run(t *testing.T) (string, error) {
|
|
t.Helper()
|
|
return r.runMode(t, "--fast-path")
|
|
}
|
|
|
|
func (r *changelogGateRepo) runMode(t *testing.T, mode string) (string, error) {
|
|
t.Helper()
|
|
return r.runRefs(t, mode, r.base, "HEAD")
|
|
}
|
|
|
|
func (r *changelogGateRepo) runRefs(t *testing.T, mode, base, head string) (string, error) {
|
|
t.Helper()
|
|
cmd := exec.Command("sh", r.gate, mode, base, head)
|
|
cmd.Dir = r.root
|
|
output, err := cmd.CombinedOutput()
|
|
return string(output), err
|
|
}
|
|
|
|
func TestChangelogPRGateAcceptsTargetedChanges(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
changelog string
|
|
}{
|
|
{
|
|
name: "new release section with lowercase todo product",
|
|
changelog: `# Changelog
|
|
|
|
## [Unreleased]
|
|
|
|
## [1.0.1-beta.1] - 2026-07-17
|
|
|
|
### Changed
|
|
|
|
- Improve the lowercase todo command family without leaving a placeholder.
|
|
|
|
## [1.0.0] - 2026-07-01
|
|
|
|
### Added
|
|
|
|
- Initial release.
|
|
`,
|
|
},
|
|
{
|
|
name: "unreleased note",
|
|
changelog: `# Changelog
|
|
|
|
## [Unreleased]
|
|
|
|
### Changed
|
|
|
|
- Document the next release candidate.
|
|
|
|
## [1.0.0] - 2026-07-01
|
|
|
|
### Added
|
|
|
|
- Initial release.
|
|
`,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
repo := newChangelogGateRepo(t)
|
|
changelogGateWrite(t, repo.root, "CHANGELOG.md", test.changelog, 0o644)
|
|
repo.commit(t, test.name)
|
|
|
|
output, err := repo.run(t)
|
|
if err != nil {
|
|
t.Fatalf("gate error = %v\noutput:\n%s", err, output)
|
|
}
|
|
if !strings.Contains(output, "CHANGELOG PR check: ok") {
|
|
t.Fatalf("gate output missing success marker:\n%s", output)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestChangelogPRContentOnlyAllowsOtherFiles(t *testing.T) {
|
|
repo := newChangelogGateRepo(t)
|
|
changelogGateWrite(t, repo.root, "CHANGELOG.md", changelogGateValidRelease, 0o644)
|
|
changelogGateWrite(t, repo.root, "internal/change.go", "package internal\n", 0o644)
|
|
repo.commit(t, "change code with release notes")
|
|
|
|
output, err := repo.runMode(t, "--content-only")
|
|
if err != nil {
|
|
t.Fatalf("content-only gate error = %v\noutput:\n%s", err, output)
|
|
}
|
|
if !strings.Contains(output, "CHANGELOG PR check: ok (mode=content-only") {
|
|
t.Fatalf("content-only gate output missing success marker:\n%s", output)
|
|
}
|
|
}
|
|
|
|
func TestChangelogPRContentOnlyStillValidatesContentWithOtherFiles(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
changelog string
|
|
wantOutput string
|
|
}{
|
|
{
|
|
name: "invalid calendar date",
|
|
changelog: strings.Replace(
|
|
changelogGateValidRelease,
|
|
"2026-07-17",
|
|
"2026-02-30",
|
|
1,
|
|
),
|
|
wantOutput: "invalid calendar date",
|
|
},
|
|
{
|
|
name: "placeholder",
|
|
changelog: strings.Replace(
|
|
changelogGateValidRelease,
|
|
"- Valid release note.",
|
|
"- TODO: write release notes.",
|
|
1,
|
|
),
|
|
wantOutput: "must not contain TODO/TBD",
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
repo := newChangelogGateRepo(t)
|
|
changelogGateWrite(t, repo.root, "CHANGELOG.md", test.changelog, 0o644)
|
|
changelogGateWrite(t, repo.root, "internal/change.go", "package internal\n", 0o644)
|
|
repo.commit(t, test.name)
|
|
|
|
output, err := repo.runMode(t, "--content-only")
|
|
if err == nil {
|
|
t.Fatalf("unsafe content-only change unexpectedly passed:\n%s", output)
|
|
}
|
|
if !strings.Contains(output, test.wantOutput) {
|
|
t.Fatalf("content-only gate output missing %q:\n%s", test.wantOutput, output)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestChangelogPRGateValidatesSyntheticMergeTree(t *testing.T) {
|
|
repo := newChangelogGateRepo(t)
|
|
commonChangelog := `# Changelog
|
|
|
|
## [Unreleased]
|
|
|
|
## [1.0.0] - 2026-07-01
|
|
|
|
### Added
|
|
|
|
- Initial release.
|
|
|
|
## [0.9.0] - 2026-06-01
|
|
|
|
### Added
|
|
|
|
- Earlier release.
|
|
`
|
|
changelogGateWrite(t, repo.root, "CHANGELOG.md", commonChangelog, 0o644)
|
|
repo.commit(t, "expand changelog history")
|
|
common := strings.TrimSpace(changelogGateGit(t, repo.root, "rev-parse", "HEAD"))
|
|
repo.base = common
|
|
|
|
changelogGateGit(t, repo.root, "switch", "-c", "feature")
|
|
featureChangelog := strings.Replace(
|
|
commonChangelog,
|
|
"## [1.0.0] - 2026-07-01",
|
|
`## [1.0.1-beta.1] - 2026-07-17
|
|
|
|
### Changed
|
|
|
|
- Feature branch release note.
|
|
|
|
## [1.0.0] - 2026-07-01`,
|
|
1,
|
|
)
|
|
changelogGateWrite(t, repo.root, "CHANGELOG.md", featureChangelog, 0o644)
|
|
repo.commit(t, "add feature release note")
|
|
featureHead := strings.TrimSpace(changelogGateGit(t, repo.root, "rev-parse", "HEAD"))
|
|
if output, err := repo.runRefs(t, "--fast-path", common, featureHead); err != nil {
|
|
t.Fatalf("feature head should be valid before merging: %v\n%s", err, output)
|
|
}
|
|
|
|
changelogGateGit(t, repo.root, "switch", "main")
|
|
mainChangelog := strings.Replace(
|
|
commonChangelog,
|
|
"## [0.9.0] - 2026-06-01",
|
|
`## [1.0.1-beta.1] - 2026-07-17
|
|
|
|
### Changed
|
|
|
|
- Main branch release note.
|
|
|
|
## [0.9.0] - 2026-06-01`,
|
|
1,
|
|
)
|
|
changelogGateWrite(t, repo.root, "CHANGELOG.md", mainChangelog, 0o644)
|
|
repo.commit(t, "add main release note")
|
|
mergeBase := strings.TrimSpace(changelogGateGit(t, repo.root, "rev-parse", "HEAD"))
|
|
changelogGateGit(t, repo.root, "merge", "--no-ff", "feature", "-m", "merge feature")
|
|
mergeHead := strings.TrimSpace(changelogGateGit(t, repo.root, "rev-parse", "HEAD"))
|
|
|
|
output, err := repo.runRefs(t, "--fast-path", mergeBase, mergeHead)
|
|
if err == nil {
|
|
t.Fatalf("synthetic merge with duplicate release heading unexpectedly passed:\n%s", output)
|
|
}
|
|
if !strings.Contains(output, "exactly one well-formed section") {
|
|
t.Fatalf("synthetic merge rejection missing duplicate-section evidence:\n%s", output)
|
|
}
|
|
}
|
|
|
|
func TestChangelogPRGateRejectsExecutableModeInBothModes(t *testing.T) {
|
|
for _, mode := range []string{"--fast-path", "--content-only"} {
|
|
t.Run(strings.TrimPrefix(mode, "--"), func(t *testing.T) {
|
|
repo := newChangelogGateRepo(t)
|
|
changelogGateWrite(t, repo.root, "CHANGELOG.md", changelogGateValidRelease, 0o644)
|
|
changelogGateGit(t, repo.root, "add", "CHANGELOG.md")
|
|
changelogGateGit(t, repo.root, "update-index", "--chmod=+x", "CHANGELOG.md")
|
|
changelogGateGit(t, repo.root, "commit", "-m", "make changelog executable")
|
|
|
|
output, err := repo.runMode(t, mode)
|
|
if err == nil {
|
|
t.Fatalf("executable CHANGELOG unexpectedly passed:\n%s", output)
|
|
}
|
|
if !strings.Contains(output, "regular 100644 blob at head") {
|
|
t.Fatalf("gate output missing regular-file rejection:\n%s", output)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestReleaseChangelogExtractionAllowsLowercaseTodoProductName(t *testing.T) {
|
|
sourceRoot, err := filepath.Abs(filepath.Join("..", ".."))
|
|
if err != nil {
|
|
t.Fatalf("Abs(repo root) error = %v", err)
|
|
}
|
|
changelog := filepath.Join(t.TempDir(), "CHANGELOG.md")
|
|
changelogGateWrite(t, filepath.Dir(changelog), filepath.Base(changelog), `# Changelog
|
|
|
|
## [1.0.1-beta.1] - 2026-07-17
|
|
|
|
### Changed
|
|
|
|
- Improve the lowercase todo command family.
|
|
`, 0o644)
|
|
|
|
cmd := exec.Command(
|
|
"sh",
|
|
"-c",
|
|
`. "$1"; release_extract_changelog "$2" 1.0.1-beta.1 -`,
|
|
"sh",
|
|
filepath.Join(sourceRoot, "scripts", "release", "release-lib.sh"),
|
|
changelog,
|
|
)
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("release_extract_changelog error = %v\noutput:\n%s", err, output)
|
|
}
|
|
}
|
|
|
|
func TestChangelogPRFastPathWorkflowContract(t *testing.T) {
|
|
root, err := filepath.Abs(filepath.Join("..", ".."))
|
|
if err != nil {
|
|
t.Fatalf("Abs(repo root) error = %v", err)
|
|
}
|
|
|
|
readWorkflow := func(path string) string {
|
|
t.Helper()
|
|
data, readErr := os.ReadFile(filepath.Join(root, path))
|
|
if readErr != nil {
|
|
t.Fatalf("ReadFile(%s) error = %v", path, readErr)
|
|
}
|
|
return string(data)
|
|
}
|
|
|
|
admission := readWorkflow(".github/workflows/ci.yml")
|
|
for _, want := range []string{
|
|
"name: CI",
|
|
"files.length === 1",
|
|
"files[0].filename === 'CHANGELOG.md'",
|
|
"files[0].status === 'modified'",
|
|
"!files[0].previous_filename",
|
|
"pre-classification",
|
|
"post-classification",
|
|
"before.changed_files !== files.length",
|
|
"after.changed_files !== files.length",
|
|
"context.ref !== 'refs/heads/main'",
|
|
"context.payload.ref !== 'refs/heads/main'",
|
|
"context.payload.created !== false",
|
|
"context.payload.deleted !== false",
|
|
"context.payload.forced !== false",
|
|
"expectedAfter !== context.sha",
|
|
"github.rest.repos.compareCommitsWithBasehead",
|
|
"comparison.merge_base_commit?.sha === expectedBefore",
|
|
"comparison.behind_by === 0",
|
|
"comparison.total_commits === comparison.ahead_by",
|
|
"run.app?.slug !== 'github-actions'",
|
|
"predecessor Code Admission is not fully successful",
|
|
`test "$(git rev-parse HEAD^1)" = "$PR_BASE_SHA"`,
|
|
`test "$(git rev-parse HEAD^2)" = "$PR_HEAD_SHA"`,
|
|
"Files API and synthetic merge tree disagree on CHANGELOG scope",
|
|
"mode=--content-only",
|
|
"mode=--fast-path",
|
|
`"$mode" "$PR_BASE_SHA" HEAD`,
|
|
`test "$(git rev-parse HEAD)" = "$PUSH_AFTER_SHA"`,
|
|
`--fast-path "$PUSH_BEFORE_SHA" "$PUSH_AFTER_SHA"`,
|
|
"needs.lint.outputs.platform_sensitive == 'true'",
|
|
`COVERAGE_TARGET: "100"`,
|
|
`COVERAGE_ENFORCE_OVERALL: "false"`,
|
|
`COVERAGE_OVERALL_TOLERANCE: "0"`,
|
|
`additional_profile=coverage-shortcut.txt`,
|
|
"run: make test-plan",
|
|
"run: make format-check",
|
|
"./scripts/ci/test-packages.sh list",
|
|
`package_output="$(./scripts/ci/test-packages.sh list`,
|
|
"docs_only: ${{ steps.classify.outputs.docs_only }}",
|
|
"full_suite: ${{ steps.classify.outputs.full_suite }}",
|
|
"classifyFiles(files.length < 300)",
|
|
"platformSensitive =",
|
|
"files.some(isNativeGoChange)",
|
|
"paths.some(isEditionSensitive)",
|
|
`/_(darwin|windows|linux|unix)(?:_[a-z0-9]+)*\.go$/`,
|
|
"filename.startsWith('build/')",
|
|
"filename === '.github/actionlint.yaml'",
|
|
"filename.startsWith('scripts/')",
|
|
"filename.startsWith('verify/')",
|
|
"filename === 'test/fixtures/cli-interface-baseline.txt'",
|
|
"filename.startsWith('internal/interfacesnapshot/')",
|
|
"filename.startsWith('internal/cobracmd/')",
|
|
"filename.startsWith('internal/i18n/')",
|
|
"filename.startsWith('pkg/cmdutil/')",
|
|
"filename === 'skills_embed.go'",
|
|
"filename.startsWith('test/mock_mcp/')",
|
|
"name: Test (changed packages)",
|
|
"changed-test-packages.sh",
|
|
"Verify authoritative synthetic merge",
|
|
`test "$(git rev-parse HEAD^1)" = "$PR_BASE_SHA"`,
|
|
`test "$(git rev-parse HEAD^2)" = "$PR_HEAD_SHA"`,
|
|
`echo "TEST_HEAD_REF=$(git rev-parse HEAD)"`,
|
|
"list \"$TEST_BASE_REF\" \"$TEST_HEAD_REF\"",
|
|
"needs.lint.outputs.full_suite != 'true'",
|
|
`name: "Test (race: ${{ matrix.shard }})"`,
|
|
"name: Test (workflow and release contracts)",
|
|
"needs.lint.outputs.full_suite == 'true' || needs.lint.outputs.release_sensitive == 'true'",
|
|
"needs.lint.outputs.full_suite == 'true'",
|
|
"-coverpkg=\"$coverpkg\"",
|
|
"changed \"$COVERAGE_BASE_REF\" \"$COVERAGE_HEAD_REF\"",
|
|
"list \"$COVERAGE_BASE_REF\" \"$COVERAGE_HEAD_REF\"",
|
|
"current_expected=success",
|
|
"supporting_expected=skipped",
|
|
"baseline_expected=success",
|
|
"Validate scoped policy",
|
|
"Interface Integrity is unaffected by this classified revision.",
|
|
"CLI Smoke is unaffected by this classified revision.",
|
|
"Mock MCP is unaffected by this classified revision.",
|
|
"Edition is unaffected by this classified revision.",
|
|
} {
|
|
if !strings.Contains(admission, want) {
|
|
t.Errorf("Code Admission workflow missing contract %q", want)
|
|
}
|
|
}
|
|
for _, context := range []string{
|
|
"Lint",
|
|
"Test",
|
|
"Coverage",
|
|
"Policy",
|
|
"Edition",
|
|
"Interface Integrity",
|
|
"CLI Smoke",
|
|
"Mock MCP",
|
|
} {
|
|
if !strings.Contains(admission, "\n name: "+context+"\n") {
|
|
t.Errorf("Code Admission workflow missing exact context %q", context)
|
|
}
|
|
}
|
|
for _, forbidden := range []string{
|
|
"name: CI Gate",
|
|
"name: Changelog Check",
|
|
"name: Policy Check",
|
|
"name: Edition Contract Tests",
|
|
"name: Mock MCP Smoke",
|
|
} {
|
|
if strings.Contains(admission, forbidden) {
|
|
t.Errorf("Code Admission workflow retains legacy context %q", forbidden)
|
|
}
|
|
}
|
|
if strings.Contains(admission, "paths-ignore:") {
|
|
t.Error("Code Admission must not suppress required contexts with paths-ignore")
|
|
}
|
|
|
|
focusedStart := strings.Index(admission, "\n test-focused:\n")
|
|
focusedEnd := strings.Index(admission, "\n test-race:\n")
|
|
if focusedStart < 0 || focusedEnd <= focusedStart {
|
|
t.Fatal("Code Admission workflow missing focused test job boundaries")
|
|
}
|
|
focusedJob := admission[focusedStart:focusedEnd]
|
|
if !strings.Contains(focusedJob, "timeout-minutes: 20") {
|
|
t.Error("focused test job must allow the scoped race suite up to 20 minutes")
|
|
}
|
|
if !strings.Contains(focusedJob, `go test -v -race -count=1 -timeout=15m "${packages[@]}"`) {
|
|
t.Error("focused race tests must retain enough package-level time for internal/app")
|
|
}
|
|
|
|
raceStart := focusedEnd
|
|
raceEnd := strings.Index(admission, "\n test-release-scripts:\n")
|
|
if raceEnd <= raceStart {
|
|
t.Fatal("Code Admission workflow missing race test job boundaries")
|
|
}
|
|
raceJob := admission[raceStart:raceEnd]
|
|
// Full race shards use a dynamic package-level timeout: default/floor 12m,
|
|
// with cli/smoke raised to 15m for NewRootCommand / Schema assembly under -race.
|
|
for _, want := range []string{
|
|
"timeout_budget=12m",
|
|
`if [ "$TEST_SHARD" = "cli" ] || [ "$TEST_SHARD" = "smoke" ]; then`,
|
|
"timeout_budget=15m",
|
|
`go test -v -race -count=1 -timeout="$timeout_budget" "${packages[@]}"`,
|
|
"- smoke",
|
|
} {
|
|
if !strings.Contains(raceJob, want) {
|
|
t.Errorf("full race shards must retain dynamic timeout budget contract %q", want)
|
|
}
|
|
}
|
|
|
|
darwinStart := strings.Index(admission, "\n test-darwin:\n")
|
|
darwinEnd := strings.Index(admission, "\n test-windows:\n")
|
|
if darwinStart < 0 || darwinEnd <= darwinStart {
|
|
t.Fatal("Code Admission workflow missing macOS test job boundaries")
|
|
}
|
|
darwinJob := admission[darwinStart:darwinEnd]
|
|
// The macOS job no longer runs ./internal/app as a whole package, so it does
|
|
// not need the package-level race budget the Ubuntu shard gets — the Ubuntu
|
|
// "race: app" shard already covers everything except the natively-gated
|
|
// tests. Pinning the two focused commands replaces that budget assertion:
|
|
// it locks the per-step timeouts and blocks a whole-package regression.
|
|
for _, want := range []string{
|
|
`go test -v -race -count=1 -timeout=6m ./internal/keychain ./internal/auth`,
|
|
`go test -v -race -count=1 -timeout=5m ./internal/app -run '^(TestValidateNewBinary_RecoversFromUnsignedDarwin|Test(CrossPlatformCoverage)?Auth(MigrateKeychain|StatusDiagnosticReportsCiphertextKeyMismatch))'`,
|
|
} {
|
|
if !strings.Contains(darwinJob, want) {
|
|
t.Errorf("macOS native test job missing focused auth contract %q", want)
|
|
}
|
|
}
|
|
if strings.Contains(darwinJob, "./internal/keychain ./internal/auth ./internal/app") {
|
|
t.Error("macOS native test job must not repeat the complete internal/app race shard")
|
|
}
|
|
if count := strings.Count(darwinJob, "./internal/app"); count != 1 {
|
|
t.Errorf("macOS native test job internal/app invocation count = %d, want 1 focused invocation", count)
|
|
}
|
|
|
|
coverageStart := strings.Index(admission, "\n coverage:\n")
|
|
coverageEnd := strings.Index(admission, "\n policy:\n")
|
|
if coverageStart < 0 || coverageEnd <= coverageStart {
|
|
t.Fatal("Code Admission workflow missing coverage job boundaries")
|
|
}
|
|
coverageJob := admission[coverageStart:coverageEnd]
|
|
for _, want := range []string{
|
|
`FULL_SUITE: ${{ needs.lint.outputs.full_suite }}`,
|
|
"policy_profile=coverage-policy.txt\n" +
|
|
` if [ "$FULL_SUITE" != true ]; then` + "\n" +
|
|
" policy_profile=\n" +
|
|
" fi",
|
|
`COVERAGE_DIFF_PROFILE="$policy_profile"`,
|
|
} {
|
|
if !strings.Contains(coverageJob, want) {
|
|
t.Errorf("Code Admission workflow missing scoped coverage contract %q", want)
|
|
}
|
|
}
|
|
|
|
notification := readWorkflow(".github/workflows/notify-wukong.yml")
|
|
if !strings.Contains(notification, "- CI") {
|
|
t.Error("Wukong notification must follow the renamed CI workflow")
|
|
}
|
|
if strings.Contains(notification, "Code Admission — PR 合入门禁") {
|
|
t.Error("Wukong notification still follows the retired workflow display name")
|
|
}
|
|
|
|
coverageGate := readWorkflow("scripts/policy/check-coverage-gate.sh")
|
|
if !strings.Contains(coverageGate, `TARGET="${COVERAGE_TARGET:-100}"`) {
|
|
t.Error("coverage gate must default to 100% changed-code coverage")
|
|
}
|
|
if !strings.Contains(coverageGate, `OVERALL_TOLERANCE="${COVERAGE_OVERALL_TOLERANCE:-0}"`) {
|
|
t.Error("coverage gate must reject any reported overall regression")
|
|
}
|
|
if !strings.Contains(coverageGate, `DIFF_PROFILE="${COVERAGE_DIFF_PROFILE-coverage-policy.txt}"`) {
|
|
t.Error("coverage gate must allow scoped CI to explicitly omit the supporting policy profile")
|
|
}
|
|
if !strings.Contains(coverageGate, `if [ -n "$DIFF_PROFILE" ]; then`) {
|
|
t.Error("coverage gate must add the supporting policy profile only when configured")
|
|
}
|
|
if !strings.Contains(coverageGate, `--baseline-profile "$BASELINE_PROFILE"`) {
|
|
t.Error("coverage gate must evaluate the merge-base profile with the candidate checker")
|
|
}
|
|
if strings.Contains(coverageGate, `--overall-profile "$ADDITIONAL_DIFF_PROFILE"`) {
|
|
t.Error("supporting changed-code coverage must not inflate candidate overall coverage")
|
|
}
|
|
if strings.Contains(coverageGate, `go tool cover -func="$BASELINE_PROFILE"`) {
|
|
t.Error("coverage baseline must not use a different coverage calculator")
|
|
}
|
|
|
|
aiBehavior := readWorkflow(".github/workflows/ai-behavior-check.yml")
|
|
for _, want := range []string{
|
|
"name: Code Admission — AI Behavior",
|
|
"\n name: AI Behavior\n",
|
|
"context: 'AI Behavior'",
|
|
"pull_request_target:",
|
|
"pull.head.sha !== expectedHead",
|
|
"pull.base.sha !== expectedBase",
|
|
} {
|
|
if !strings.Contains(aiBehavior, want) {
|
|
t.Errorf("AI Behavior workflow missing contract %q", want)
|
|
}
|
|
}
|
|
|
|
integration := readWorkflow(".github/workflows/multi-profile-e2e.yml")
|
|
for _, want := range []string{
|
|
"name: Main Integration — 主干集成",
|
|
"\n name: Multi-profile E2E\n",
|
|
"branches:",
|
|
"- main",
|
|
"workflow_dispatch:",
|
|
} {
|
|
if !strings.Contains(integration, want) {
|
|
t.Errorf("main integration workflow missing contract %q", want)
|
|
}
|
|
}
|
|
if strings.Contains(integration, "pull_request:") {
|
|
t.Error("complete Multi-profile E2E must not run as a pull-request admission context")
|
|
}
|
|
if !strings.Contains(integration, "include-hidden-files: true") {
|
|
t.Error("main integration must upload diagnostics stored below the hidden .tmp-bin directory")
|
|
}
|
|
|
|
integrationScript := readWorkflow("scripts/dev/test-multi-profile-e2e.sh")
|
|
if !strings.Contains(integrationScript, `GO_TEST_TIMEOUT="${MULTI_PROFILE_GO_TEST_TIMEOUT:-10m}"`) {
|
|
t.Error("multi-profile E2E must allow enough time for the complete internal/app regression suite")
|
|
}
|
|
if strings.Contains(integrationScript, "go test -timeout 180s") {
|
|
t.Error("multi-profile E2E must not retain the obsolete three-minute Go test budget")
|
|
}
|
|
}
|
|
|
|
func TestChangelogPRGateRejectsUnsafeChanges(t *testing.T) {
|
|
validRelease := `# Changelog
|
|
|
|
## [Unreleased]
|
|
|
|
## [1.0.1-beta.1] - 2026-07-17
|
|
|
|
### Changed
|
|
|
|
- Valid release note.
|
|
|
|
## [1.0.0] - 2026-07-01
|
|
|
|
### Added
|
|
|
|
- Initial release.
|
|
`
|
|
tests := []struct {
|
|
name string
|
|
changelog string
|
|
mutate func(*testing.T, *changelogGateRepo)
|
|
wantOutput string
|
|
}{
|
|
{
|
|
name: "second changed file",
|
|
changelog: validRelease,
|
|
mutate: func(t *testing.T, repo *changelogGateRepo) {
|
|
changelogGateWrite(t, repo.root, "extra.txt", "extra\n", 0o644)
|
|
},
|
|
wantOutput: "exactly one in-place modification",
|
|
},
|
|
{
|
|
name: "invalid calendar date",
|
|
changelog: strings.Replace(
|
|
validRelease,
|
|
"2026-07-17",
|
|
"2026-02-30",
|
|
1,
|
|
),
|
|
wantOutput: "invalid calendar date",
|
|
},
|
|
{
|
|
name: "duplicate release heading",
|
|
changelog: strings.Replace(
|
|
validRelease,
|
|
"## [1.0.0] - 2026-07-01",
|
|
"## [1.0.1-beta.1] - 2026-07-17\n\n- Duplicate.\n\n## [1.0.0] - 2026-07-01",
|
|
1,
|
|
),
|
|
wantOutput: "exactly one well-formed section",
|
|
},
|
|
{
|
|
name: "missing bullet",
|
|
changelog: strings.Replace(
|
|
validRelease,
|
|
"- Valid release note.",
|
|
"Valid release note.",
|
|
1,
|
|
),
|
|
wantOutput: "at least one bullet",
|
|
},
|
|
{
|
|
name: "placeholder",
|
|
changelog: strings.Replace(
|
|
validRelease,
|
|
"- Valid release note.",
|
|
"- TODO: write release notes.",
|
|
1,
|
|
),
|
|
wantOutput: "must not contain TODO/TBD",
|
|
},
|
|
{
|
|
name: "malformed duplicate unreleased heading",
|
|
changelog: strings.Replace(
|
|
validRelease,
|
|
"## [Unreleased]",
|
|
"## [Unreleased]\n\n## [Unreleased] junk",
|
|
1,
|
|
),
|
|
wantOutput: "exactly one heading",
|
|
},
|
|
{
|
|
name: "preamble change",
|
|
changelog: strings.Replace(
|
|
validRelease,
|
|
"# Changelog",
|
|
"# Release history",
|
|
1,
|
|
),
|
|
wantOutput: "only permits notes inside",
|
|
},
|
|
{
|
|
name: "rename changelog",
|
|
changelog: changelogGateBase,
|
|
mutate: func(t *testing.T, repo *changelogGateRepo) {
|
|
if err := os.Rename(
|
|
filepath.Join(repo.root, "CHANGELOG.md"),
|
|
filepath.Join(repo.root, "CHANGES.md"),
|
|
); err != nil {
|
|
t.Fatalf("Rename CHANGELOG.md error = %v", err)
|
|
}
|
|
},
|
|
wantOutput: "regular 100644 blob at head",
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
repo := newChangelogGateRepo(t)
|
|
changelogGateWrite(t, repo.root, "CHANGELOG.md", test.changelog, 0o644)
|
|
if test.mutate != nil {
|
|
test.mutate(t, repo)
|
|
}
|
|
repo.commit(t, test.name)
|
|
|
|
output, err := repo.run(t)
|
|
if err == nil {
|
|
t.Fatalf("unsafe change unexpectedly passed:\n%s", output)
|
|
}
|
|
if !strings.Contains(output, test.wantOutput) {
|
|
t.Fatalf("gate output missing %q:\n%s", test.wantOutput, output)
|
|
}
|
|
})
|
|
}
|
|
}
|