Skip to content

Add cross-platform CI, unit tests, and testing documentation#27

Open
shawntz wants to merge 21 commits intomainfrom
chore/testing
Open

Add cross-platform CI, unit tests, and testing documentation#27
shawntz wants to merge 21 commits intomainfrom
chore/testing

Conversation

@shawntz
Copy link
Copy Markdown
Owner

@shawntz shawntz commented Dec 19, 2025

Description

This PR significantly enhances the testing infrastructure for cassh by:

  1. Cross-platform CI: Refactored GitHub Actions workflow to run tests on macOS, Linux, and Windows with code coverage reporting
  2. Unit tests: Added comprehensive unit tests for key title generation and rotation logic in internal/key/
  3. Makefile improvements: Enhanced build system with granular test targets (test-unit, test-integration, test-coverage, test-watch)
  4. Documentation: Created TESTING.md with guidelines for writing and running tests
  5. Bug fix: Fixed null values in About dialog for version and commit hash display

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)

Related Issues

Closes #

How Has This Been Tested?

  • Unit tests - Added comprehensive tests for key title and rotation logic
  • Integration tests - Cross-platform CI validates on macOS, Linux, and Windows
  • Manual testing - Verified About dialog displays version correctly

Test environment:

  • OS: macOS 26.2 Beta (25C5037j)
  • Go version: 1.25.5 darwin/arm64

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published

Security Considerations

  • This PR does not introduce any security concerns
  • I have considered the security implications and addressed them - Testing infrastructure improvements only, no functional changes to security-critical code

Screenshots (if applicable)

Introduced TESTING.md with a comprehensive index of all tests, usage instructions, and CI integration details. Updated CLAUDE.md to require tests to be easily findable and a README.md to be maintained in the test directory.
Added multiple test targets to the Makefile for more granular control, including race detection, coverage, CI, and package-specific tests. Improved cross-platform support by differentiating test execution on macOS and Linux. Also added a target to list all test files and updated the default test and lint targets.
Introduces tests for getKeyTitle, getLegacyKeyTitle, and needsKeyRotation functions in cassh-menubar. These tests cover key title formatting, hostname truncation, key rotation timing, and consistency between legacy and new title formats.
Split tests into separate Linux and macOS jobs, ensuring platform-specific code is tested appropriately. Add concurrency control, aggregate coverage from both platforms, and upload to Codecov. Update build jobs to depend on both test jobs, and add a final CI success check for branch protection.
@shawntz shawntz self-assigned this Dec 19, 2025
Copilot AI review requested due to automatic review settings December 19, 2025 09:24
@shawntz shawntz added documentation Improvements or additions to documentation enhancement New feature or request ci/cd devops labels Dec 19, 2025
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to establish comprehensive testing infrastructure for cassh by adding cross-platform CI, unit tests for key management functionality, enhanced Makefile test targets, and centralized testing documentation. However, there is a critical compilation issue that must be resolved before the PR can be merged.

Key changes:

  • Adds unit tests for SSH key title generation and rotation logic in the menubar application
  • Refactors GitHub Actions workflow to run tests separately on Linux and macOS with combined coverage reporting
  • Expands Makefile with granular test targets (test-unit, test-coverage, test-ci, test-list, and package-specific targets)

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
cmd/cassh-menubar/keytitle_test.go Adds unit tests for key title generation and rotation logic (but references non-existent functions)
TESTING.md Creates comprehensive testing documentation with test index, CI/CD integration guide, and local testing instructions
Makefile Enhances build system with platform-aware test targets and granular package-specific test commands
CLAUDE.md Adds informal note about test indexing requirements
.github/workflows/build.yml Refactors CI to separate Linux/macOS test jobs with combined coverage reporting and adds ci-success gate job

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Makefile Outdated
Comment on lines +278 to +284
ifeq ($(shell uname),Darwin)
@echo "Running all tests (macOS)..."
CGO_ENABLED=1 go test -v ./...
else
@echo "Running cross-platform tests (Linux)..."
go test -v $$(go list ./... | grep -v /cmd/cassh-menubar)
endif
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Makefile uses uname to detect the platform, which works on Linux and macOS but will fail on Windows. While the PR description mentions Windows testing, the Makefile doesn't handle Windows at all (it would need different commands and uname isn't available by default). If Windows support is intended, consider adding detection for Windows or clarifying that Windows is not supported for local testing via the Makefile.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

@@ -0,0 +1,179 @@
# cassh Test Documentation
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description mentions "Bug fix: Fixed null values in About dialog for version and commit hash display" but there are no changes to about_darwin.go or the version/buildCommit variable initialization in main.go shown in this PR. Either the bug fix is missing from this PR, or it was already fixed previously and should not be mentioned in this PR's description.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

Comment on lines +1 to +233
//go:build darwin

package main

import (
"os"
"strings"
"testing"
"time"

"github.com/shawntz/cassh/internal/config"
)

func TestGetKeyTitle(t *testing.T) {
hostname, _ := os.Hostname()
expectedHostname := strings.ReplaceAll(hostname, ".", "-")
if len(expectedHostname) > 30 {
expectedHostname = expectedHostname[:30]
}

tests := []struct {
name string
connID string
contains []string
}{
{
name: "personal connection",
connID: "personal-1234567890",
contains: []string{"cassh-personal-1234567890@", expectedHostname},
},
{
name: "enterprise connection",
connID: "enterprise-9876543210",
contains: []string{"cassh-enterprise-9876543210@", expectedHostname},
},
{
name: "short ID",
connID: "test",
contains: []string{"cassh-test@", expectedHostname},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getKeyTitle(tt.connID)

for _, substr := range tt.contains {
if !strings.Contains(result, substr) {
t.Errorf("getKeyTitle(%q) = %q, should contain %q", tt.connID, result, substr)
}
}

// Verify format: cassh-{connID}@{hostname}
if !strings.HasPrefix(result, "cassh-") {
t.Errorf("getKeyTitle(%q) = %q, should start with 'cassh-'", tt.connID, result)
}

if !strings.Contains(result, "@") {
t.Errorf("getKeyTitle(%q) = %q, should contain '@'", tt.connID, result)
}
})
}
}

func TestGetLegacyKeyTitle(t *testing.T) {
tests := []struct {
name string
connID string
expected string
}{
{
name: "personal connection",
connID: "personal-1234567890",
expected: "cassh-personal-1234567890",
},
{
name: "enterprise connection",
connID: "enterprise-9876543210",
expected: "cassh-enterprise-9876543210",
},
{
name: "short ID",
connID: "test",
expected: "cassh-test",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getLegacyKeyTitle(tt.connID)
if result != tt.expected {
t.Errorf("getLegacyKeyTitle(%q) = %q, want %q", tt.connID, result, tt.expected)
}
})
}
}

func TestGetKeyTitleHostnameTruncation(t *testing.T) {
// Test that the function doesn't panic with any input
// and produces reasonable output
result := getKeyTitle("test-connection")

// Should not be empty
if result == "" {
t.Error("getKeyTitle() returned empty string")
}

// Should not exceed reasonable length (cassh- + connID + @ + 30 char hostname)
maxExpectedLen := len("cassh-test-connection@") + 30
if len(result) > maxExpectedLen+10 { // Allow some margin
t.Errorf("getKeyTitle() result too long: %d chars, expected max ~%d", len(result), maxExpectedLen)
}
}

func TestNeedsKeyRotation(t *testing.T) {
now := time.Now()

tests := []struct {
name string
conn *config.Connection
expected bool
}{
{
name: "enterprise connection - never needs rotation",
conn: &config.Connection{
Type: config.ConnectionTypeEnterprise,
KeyRotationHours: 4,
KeyCreatedAt: now.Add(-5 * time.Hour).Unix(),
},
expected: false,
},
{
name: "personal - no rotation configured",
conn: &config.Connection{
Type: config.ConnectionTypePersonal,
KeyRotationHours: 0,
KeyCreatedAt: now.Add(-100 * time.Hour).Unix(),
},
expected: false,
},
{
name: "personal - no creation time recorded",
conn: &config.Connection{
Type: config.ConnectionTypePersonal,
KeyRotationHours: 4,
KeyCreatedAt: 0,
},
expected: false,
},
{
name: "personal - key is fresh (created 1 hour ago, 4h rotation)",
conn: &config.Connection{
Type: config.ConnectionTypePersonal,
KeyRotationHours: 4,
KeyCreatedAt: now.Add(-1 * time.Hour).Unix(),
},
expected: false,
},
{
name: "personal - key needs rotation (created 5 hours ago, 4h rotation)",
conn: &config.Connection{
Type: config.ConnectionTypePersonal,
KeyRotationHours: 4,
KeyCreatedAt: now.Add(-5 * time.Hour).Unix(),
},
expected: true,
},
{
name: "personal - key exactly at rotation boundary",
conn: &config.Connection{
Type: config.ConnectionTypePersonal,
KeyRotationHours: 4,
KeyCreatedAt: now.Add(-4 * time.Hour).Unix(),
},
expected: true,
},
{
name: "personal - key created 1 day ago with 24h rotation",
conn: &config.Connection{
Type: config.ConnectionTypePersonal,
KeyRotationHours: 24,
KeyCreatedAt: now.Add(-24 * time.Hour).Unix(),
},
expected: true,
},
{
name: "personal - key created 23 hours ago with 24h rotation",
conn: &config.Connection{
Type: config.ConnectionTypePersonal,
KeyRotationHours: 24,
KeyCreatedAt: now.Add(-23 * time.Hour).Unix(),
},
expected: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := needsKeyRotation(tt.conn)
if result != tt.expected {
t.Errorf("needsKeyRotation() = %v, want %v", result, tt.expected)
}
})
}
}

func TestKeyTitleConsistency(t *testing.T) {
// Test that calling getKeyTitle multiple times returns the same result
connID := "personal-1234567890"

first := getKeyTitle(connID)
second := getKeyTitle(connID)

if first != second {
t.Errorf("getKeyTitle() not consistent: first=%q, second=%q", first, second)
}
}

func TestLegacyAndNewTitlesDifferent(t *testing.T) {
connID := "personal-1234567890"

newTitle := getKeyTitle(connID)
legacyTitle := getLegacyKeyTitle(connID)

if newTitle == legacyTitle {
t.Errorf("new and legacy titles should be different: new=%q, legacy=%q", newTitle, legacyTitle)
}

// Legacy should be a prefix of new (without the @hostname part)
if !strings.HasPrefix(newTitle, legacyTitle) {
t.Errorf("new title %q should start with legacy title %q", newTitle, legacyTitle)
}
}
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test file references functions that don't exist in the codebase. The tests call getKeyTitle(), getLegacyKeyTitle(), and needsKeyRotation(), but only needsKeyRotation() exists in main.go. The functions getKeyTitle() and getLegacyKeyTitle() are not defined anywhere, which will cause compilation failures. Either these functions need to be added to the codebase, or these tests should be removed/modified to test existing functionality.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

shawntz and others added 3 commits December 19, 2025 01:42
…n permissions

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented Dec 19, 2025

@shawntz I've opened a new pull request, #29, to work on those changes. Once the pull request is ready, I'll request review from you.

shawntz and others added 2 commits December 19, 2025 01:43
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented Dec 19, 2025

@shawntz I've opened a new pull request, #30, to work on those changes. Once the pull request is ready, I'll request review from you.

shawntz and others added 2 commits December 19, 2025 01:44
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented Dec 19, 2025

@shawntz I've opened a new pull request, #31, to work on those changes. Once the pull request is ready, I'll request review from you.

shawntz and others added 5 commits December 19, 2025 01:45
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: shawntz <5200266+shawntz@users.noreply.github.com>
Co-authored-by: shawntz <5200266+shawntz@users.noreply.github.com>
Co-authored-by: shawntz <5200266+shawntz@users.noreply.github.com>
Co-authored-by: shawntz <5200266+shawntz@users.noreply.github.com>
Address PR #27 feedback: About dialog bug fix claim is incorrect
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Remove test file referencing non-existent functions
@shawntz
Copy link
Copy Markdown
Owner Author

shawntz commented Jan 1, 2026

fizzy

shawntz and others added 3 commits January 11, 2026 00:52
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Document Windows platform limitations in Makefile and add helpful error messages
Copilot AI review requested due to automatic review settings January 11, 2026 08:52
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +94 to +111
| Test Function | Description |
|--------------|-------------|
| `TestGetKeyTitle` | Tests SSH key title generation with hostname |
| `TestGetLegacyKeyTitle` | Tests legacy key title format (without hostname) |
| `TestGetKeyTitleHostnameTruncation` | Tests hostname truncation to 30 chars |
| `TestNeedsKeyRotation` | Tests key rotation detection logic |
| `TestKeyTitleConsistency` | Tests that key title generation is deterministic |
| `TestLegacyAndNewTitlesDifferent` | Tests that new format differs from legacy |

**Test Cases for `TestNeedsKeyRotation`:**
- Enterprise connection (never rotates)
- Personal with no rotation configured
- Personal with no creation time recorded
- Personal with fresh key (within rotation window)
- Personal with expired key (past rotation window)
- Personal at exact rotation boundary
- Various rotation hour configurations (4h, 24h)

Copy link

Copilot AI Jan 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation references test file 'cmd/cassh-menubar/keytitle_test.go' and describes multiple test functions (TestGetKeyTitle, TestGetLegacyKeyTitle, TestGetKeyTitleHostnameTruncation, TestNeedsKeyRotation, TestKeyTitleConsistency, TestLegacyAndNewTitlesDifferent), but this test file does not exist in the repository. This section should either be removed or the actual test file should be added as part of this PR.

Suggested change
| Test Function | Description |
|--------------|-------------|
| `TestGetKeyTitle` | Tests SSH key title generation with hostname |
| `TestGetLegacyKeyTitle` | Tests legacy key title format (without hostname) |
| `TestGetKeyTitleHostnameTruncation` | Tests hostname truncation to 30 chars |
| `TestNeedsKeyRotation` | Tests key rotation detection logic |
| `TestKeyTitleConsistency` | Tests that key title generation is deterministic |
| `TestLegacyAndNewTitlesDifferent` | Tests that new format differs from legacy |
**Test Cases for `TestNeedsKeyRotation`:**
- Enterprise connection (never rotates)
- Personal with no rotation configured
- Personal with no creation time recorded
- Personal with fresh key (within rotation window)
- Personal with expired key (past rotation window)
- Personal at exact rotation boundary
- Various rotation hour configurations (4h, 24h)
_Note: Detailed tests for macOS menubar key title behavior are not currently present in the repository. This section will be updated when those tests are added._

Copilot uses AI. Check for mistakes.
## Testing Guidelines

- Ensure that all tests are easily discoverable and indexable.
- Maintain an up-to-date `README.md` in the test directory that documents existing tests and is updated whenever new tests are added. No newline at end of file
Copy link

Copilot AI Jan 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment indicates this guideline refers to maintaining 'README.md in the test directory', but this PR adds TESTING.md in the root directory. This should be updated to reference TESTING.md or clarify the intended location.

Suggested change
- Maintain an up-to-date `README.md` in the test directory that documents existing tests and is updated whenever new tests are added.
- Maintain an up-to-date `TESTING.md` in the repository root that documents existing tests and is updated whenever new tests are added.

Copilot uses AI. Check for mistakes.
---

*Last updated: 2024-12-10*
*Test count: 35 tests across 4 packages*
Copy link

Copilot AI Jan 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test count claims '35 tests across 4 packages', but the documented tests only include internal/ca (12 tests), internal/config (11 tests), internal/memes (6 tests), and the non-existent cmd/cassh-menubar/keytitle_test.go (6 tests). Without the menubar tests that don't exist, this would be 29 tests across 3 packages. The count should be verified and corrected.

Suggested change
*Test count: 35 tests across 4 packages*
*Test count: 29 tests across 3 packages*

Copilot uses AI. Check for mistakes.
| `make test-ca` | Certificate Authority |
| `make test-config` | Configuration |
| `make test-memes` | Memes/Quotes |
| `make test-menubar` | macOS Menubar (macOS only) |
Copy link

Copilot AI Jan 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Quick Reference table mentions 'make test-menubar' for testing the macOS Menubar package, but there are currently no test files in the cmd/cassh-menubar directory. While the Makefile target will succeed (go test returns 0 when there are no test files), users expecting to run menubar tests will not actually execute any tests. Consider either removing this entry until tests are added, or adding a note that no tests currently exist for this package.

Suggested change
| `make test-menubar` | macOS Menubar (macOS only) |
| `make test-menubar` | macOS Menubar (macOS only; no tests currently exist for this package) |

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd devops documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants