mirror of
https://github.com/samsonjs/Peekaboo.git
synced 2026-04-05 11:15:51 +00:00
Add comprehensive Swift unit tests and enhanced CLI testing
- Add unit test templates for all Swift components: - ApplicationFinderTests: Test app discovery and fuzzy matching - WindowManagerTests: Test window listing and filtering - PermissionsCheckerTests: Test permission detection - ImageCommandTests: Test command parsing and validation - ListCommandTests: Test list command variations - Enhance release script with thorough CLI testing: - Test all commands with various arguments - Validate JSON output structure - Test error handling for invalid inputs - Check permission status reporting - Add dedicated Swift CLI integration test phase - Update RELEASING.md to highlight automated checks
This commit is contained in:
parent
1ff703b185
commit
f4a41f8355
7 changed files with 1380 additions and 25 deletions
55
RELEASING.md
55
RELEASING.md
|
|
@ -2,49 +2,56 @@
|
|||
|
||||
This document outlines the steps to release a new version of the `@steipete/peekaboo-mcp` NPM package.
|
||||
|
||||
## Pre-Release Checklist
|
||||
## Automated Release Preparation
|
||||
|
||||
1. **Ensure Main Branch is Up-to-Date:**
|
||||
- Pull the latest changes from the main branch (`main` or `master`).
|
||||
- `git pull origin main`
|
||||
The project includes an automated release preparation script that performs comprehensive checks before release. Run it with:
|
||||
|
||||
2. **Create a Release Branch (Optional but Recommended):**
|
||||
- Create a new branch for the release, e.g., `release/v1.0.0-beta.3`.
|
||||
- `git checkout -b release/vX.Y.Z`
|
||||
```bash
|
||||
npm run prepare-release
|
||||
```
|
||||
|
||||
3. **Update Version Number:**
|
||||
This script performs the following checks:
|
||||
- **Git Status**: Ensures you're on the main branch with no uncommitted changes
|
||||
- **Required Fields**: Validates all required fields in package.json
|
||||
- **Dependencies**: Checks for missing or outdated dependencies
|
||||
- **Security Audit**: Runs npm audit to check for vulnerabilities
|
||||
- **Version Availability**: Confirms the version isn't already published
|
||||
- **Version Consistency**: Ensures package.json and package-lock.json versions match
|
||||
- **Changelog Entry**: Verifies CHANGELOG.md has an entry for the current version
|
||||
- **TypeScript**: Compiles and runs tests
|
||||
- **TypeScript Declarations**: Verifies .d.ts files are generated
|
||||
- **Swift**: Runs format, lint, and tests
|
||||
- **Build Verification**: Builds everything and verifies the package
|
||||
- **Package Size**: Warns if package exceeds 2MB
|
||||
- **MCP Server Smoke Test**: Tests the server with a simple JSON-RPC request
|
||||
|
||||
If all checks pass, follow the manual steps below.
|
||||
|
||||
## Manual Pre-Release Steps
|
||||
|
||||
1. **Update Version Number:**
|
||||
- Decide on the new semantic version number (e.g., `1.0.0-beta.3`, `1.0.0`, `1.1.0`).
|
||||
- Update the `version` field in `package.json`.
|
||||
|
||||
4. **Update Documentation:**
|
||||
2. **Update Documentation:**
|
||||
- **`README.md`**: Ensure it accurately reflects the latest features, installation instructions, and any breaking changes.
|
||||
- **`docs/spec.md`**: If there are changes to tool schemas or server behavior, update the detailed specification.
|
||||
- Any other relevant documentation.
|
||||
|
||||
5. **Update `CHANGELOG.md`:**
|
||||
3. **Update `CHANGELOG.md`:**
|
||||
- Add a new section for the upcoming release version (e.g., `## [1.0.0-beta.3] - YYYY-MM-DD`).
|
||||
- List all notable changes (Added, Changed, Fixed, Removed, Deprecated, Security) under this version.
|
||||
- Replace `YYYY-MM-DD` with the current date.
|
||||
|
||||
6. **Run All Tests:**
|
||||
- Ensure all unit, integration, and E2E tests are passing.
|
||||
- `npm test` (or `npm run test:all` if that's more comprehensive for your setup).
|
||||
4. **Run Release Preparation:**
|
||||
- Run `npm run prepare-release` to ensure everything is ready.
|
||||
- Fix any issues identified by the script.
|
||||
|
||||
7. **Build the Project:**
|
||||
- Run the build script to compile TypeScript and the Swift CLI.
|
||||
- `npm run build:all` (as defined in `package.json`).
|
||||
|
||||
8. **Commit Changes:**
|
||||
5. **Commit Changes:**
|
||||
- Commit all changes related to the version bump, documentation, and changelog.
|
||||
- `git add .`
|
||||
- `git commit -m "Prepare release vX.Y.Z"`
|
||||
|
||||
9. **Merge to Main Branch (If Using a Release Branch):**
|
||||
- Merge the release branch back into the main branch.
|
||||
- `git checkout main`
|
||||
- `git merge release/vX.Y.Z --no-ff` (using `--no-ff` creates a merge commit, which can be useful for tracking releases).
|
||||
- `git push origin main`
|
||||
|
||||
## Publishing to NPM
|
||||
|
||||
1. **NPM Publish Dry Run:**
|
||||
|
|
|
|||
102
peekaboo-cli/Tests/peekabooTests/ApplicationFinderTests.swift
Normal file
102
peekaboo-cli/Tests/peekabooTests/ApplicationFinderTests.swift
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import XCTest
|
||||
@testable import peekaboo
|
||||
|
||||
final class ApplicationFinderTests: XCTestCase {
|
||||
var applicationFinder: ApplicationFinder!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
applicationFinder = ApplicationFinder()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
applicationFinder = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
// MARK: - findRunningApplication Tests
|
||||
|
||||
func testFindRunningApplicationExactMatch() throws {
|
||||
// Test finding an app that should always be running on macOS
|
||||
let result = try applicationFinder.findRunningApplication(named: "Finder")
|
||||
|
||||
XCTAssertNotNil(result)
|
||||
XCTAssertEqual(result?.localizedName, "Finder")
|
||||
XCTAssertEqual(result?.bundleIdentifier, "com.apple.finder")
|
||||
}
|
||||
|
||||
func testFindRunningApplicationCaseInsensitive() throws {
|
||||
// Test case-insensitive matching
|
||||
let result = try applicationFinder.findRunningApplication(named: "finder")
|
||||
|
||||
XCTAssertNotNil(result)
|
||||
XCTAssertEqual(result?.localizedName, "Finder")
|
||||
}
|
||||
|
||||
func testFindRunningApplicationByBundleIdentifier() throws {
|
||||
// Test finding by bundle identifier
|
||||
let result = try applicationFinder.findRunningApplication(named: "com.apple.finder")
|
||||
|
||||
XCTAssertNotNil(result)
|
||||
XCTAssertEqual(result?.bundleIdentifier, "com.apple.finder")
|
||||
}
|
||||
|
||||
func testFindRunningApplicationNotFound() {
|
||||
// Test app not found error
|
||||
XCTAssertThrowsError(try applicationFinder.findRunningApplication(named: "NonExistentApp12345")) { error in
|
||||
guard let captureError = error as? CaptureError else {
|
||||
XCTFail("Expected CaptureError")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(captureError, .appNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func testFindRunningApplicationPartialMatch() throws {
|
||||
// Test partial name matching
|
||||
let result = try applicationFinder.findRunningApplication(named: "Find")
|
||||
|
||||
// Should find Finder as closest match
|
||||
XCTAssertNotNil(result)
|
||||
XCTAssertEqual(result?.localizedName, "Finder")
|
||||
}
|
||||
|
||||
// MARK: - Fuzzy Matching Tests
|
||||
|
||||
func testFuzzyMatchingScore() {
|
||||
// Test the fuzzy matching algorithm
|
||||
let finder = "Finder"
|
||||
|
||||
// Exact match should have highest score
|
||||
XCTAssertEqual(applicationFinder.fuzzyMatch("Finder", with: finder), 1.0)
|
||||
|
||||
// Case differences should still score high
|
||||
XCTAssertGreaterThan(applicationFinder.fuzzyMatch("finder", with: finder), 0.8)
|
||||
|
||||
// Partial matches should score lower but still match
|
||||
XCTAssertGreaterThan(applicationFinder.fuzzyMatch("Find", with: finder), 0.5)
|
||||
XCTAssertLessThan(applicationFinder.fuzzyMatch("Find", with: finder), 0.9)
|
||||
|
||||
// Completely different should score very low
|
||||
XCTAssertLessThan(applicationFinder.fuzzyMatch("Safari", with: finder), 0.3)
|
||||
}
|
||||
|
||||
func testFuzzyMatchingWithSpaces() {
|
||||
// Test matching with spaces and special characters
|
||||
let appName = "Google Chrome"
|
||||
|
||||
// Various ways users might type Chrome
|
||||
XCTAssertGreaterThan(applicationFinder.fuzzyMatch("chrome", with: appName), 0.5)
|
||||
XCTAssertGreaterThan(applicationFinder.fuzzyMatch("google", with: appName), 0.5)
|
||||
XCTAssertGreaterThan(applicationFinder.fuzzyMatch("googlechrome", with: appName), 0.7)
|
||||
}
|
||||
|
||||
// MARK: - Performance Tests
|
||||
|
||||
func testFindApplicationPerformance() throws {
|
||||
// Test that finding an app completes in reasonable time
|
||||
measure {
|
||||
_ = try? applicationFinder.findRunningApplication(named: "Finder")
|
||||
}
|
||||
}
|
||||
}
|
||||
216
peekaboo-cli/Tests/peekabooTests/ImageCommandTests.swift
Normal file
216
peekaboo-cli/Tests/peekabooTests/ImageCommandTests.swift
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
import XCTest
|
||||
import ArgumentParser
|
||||
@testable import peekaboo
|
||||
|
||||
final class ImageCommandTests: XCTestCase {
|
||||
|
||||
// MARK: - Command Parsing Tests
|
||||
|
||||
func testImageCommandParsing() throws {
|
||||
// Test basic command parsing
|
||||
let command = try ImageCommand.parse([])
|
||||
|
||||
// Verify defaults
|
||||
XCTAssertEqual(command.mode, .activeWindow)
|
||||
XCTAssertEqual(command.format, .png)
|
||||
XCTAssertNil(command.output)
|
||||
XCTAssertNil(command.app)
|
||||
XCTAssertNil(command.windowId)
|
||||
XCTAssertFalse(command.includeWindowFrame)
|
||||
XCTAssertEqual(command.quality, 90)
|
||||
}
|
||||
|
||||
func testImageCommandWithScreenMode() throws {
|
||||
// Test screen capture mode
|
||||
let command = try ImageCommand.parse(["--mode", "screen"])
|
||||
|
||||
XCTAssertEqual(command.mode, .screen)
|
||||
}
|
||||
|
||||
func testImageCommandWithAppSpecifier() throws {
|
||||
// Test app-specific capture
|
||||
let command = try ImageCommand.parse([
|
||||
"--mode", "app",
|
||||
"--app", "Finder"
|
||||
])
|
||||
|
||||
XCTAssertEqual(command.mode, .app)
|
||||
XCTAssertEqual(command.app, "Finder")
|
||||
}
|
||||
|
||||
func testImageCommandWithWindowId() throws {
|
||||
// Test window ID capture
|
||||
let command = try ImageCommand.parse([
|
||||
"--mode", "window",
|
||||
"--window-id", "123"
|
||||
])
|
||||
|
||||
XCTAssertEqual(command.mode, .window)
|
||||
XCTAssertEqual(command.windowId, 123)
|
||||
}
|
||||
|
||||
func testImageCommandWithOutput() throws {
|
||||
// Test output path specification
|
||||
let outputPath = "/tmp/test-image.png"
|
||||
let command = try ImageCommand.parse([
|
||||
"--output", outputPath
|
||||
])
|
||||
|
||||
XCTAssertEqual(command.output, outputPath)
|
||||
}
|
||||
|
||||
func testImageCommandWithFormat() throws {
|
||||
// Test JPEG format
|
||||
let command = try ImageCommand.parse([
|
||||
"--format", "jpg",
|
||||
"--quality", "85"
|
||||
])
|
||||
|
||||
XCTAssertEqual(command.format, .jpg)
|
||||
XCTAssertEqual(command.quality, 85)
|
||||
}
|
||||
|
||||
func testImageCommandWithJSONOutput() throws {
|
||||
// Test JSON output flag
|
||||
let command = try ImageCommand.parse(["--json-output"])
|
||||
|
||||
XCTAssertTrue(command.jsonOutput)
|
||||
}
|
||||
|
||||
// MARK: - Validation Tests
|
||||
|
||||
func testImageCommandValidationMissingApp() {
|
||||
// Test that app mode requires app name
|
||||
XCTAssertThrowsError(try ImageCommand.parse([
|
||||
"--mode", "app"
|
||||
// Missing --app parameter
|
||||
]))
|
||||
}
|
||||
|
||||
func testImageCommandValidationMissingWindowId() {
|
||||
// Test that window mode requires window ID
|
||||
XCTAssertThrowsError(try ImageCommand.parse([
|
||||
"--mode", "window"
|
||||
// Missing --window-id parameter
|
||||
]))
|
||||
}
|
||||
|
||||
func testImageCommandValidationInvalidQuality() {
|
||||
// Test quality validation
|
||||
XCTAssertThrowsError(try ImageCommand.parse([
|
||||
"--quality", "150" // > 100
|
||||
]))
|
||||
|
||||
XCTAssertThrowsError(try ImageCommand.parse([
|
||||
"--quality", "-10" // < 0
|
||||
]))
|
||||
}
|
||||
|
||||
// MARK: - Capture Mode Tests
|
||||
|
||||
func testCaptureModeRawValues() {
|
||||
// Test capture mode string values
|
||||
XCTAssertEqual(CaptureMode.screen.rawValue, "screen")
|
||||
XCTAssertEqual(CaptureMode.activeWindow.rawValue, "active_window")
|
||||
XCTAssertEqual(CaptureMode.app.rawValue, "app")
|
||||
XCTAssertEqual(CaptureMode.window.rawValue, "window")
|
||||
XCTAssertEqual(CaptureMode.area.rawValue, "area")
|
||||
}
|
||||
|
||||
func testImageFormatRawValues() {
|
||||
// Test image format values
|
||||
XCTAssertEqual(ImageFormat.png.rawValue, "png")
|
||||
XCTAssertEqual(ImageFormat.jpg.rawValue, "jpg")
|
||||
XCTAssertEqual(ImageFormat.data.rawValue, "data")
|
||||
}
|
||||
|
||||
// MARK: - Helper Method Tests
|
||||
|
||||
func testGenerateFilename() {
|
||||
// Test filename generation with pattern
|
||||
let pattern = "{app}_{mode}_{timestamp}"
|
||||
let date = Date(timeIntervalSince1970: 1700000000) // Fixed date for testing
|
||||
|
||||
// Mock the filename generation logic
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd_HH-mm-ss"
|
||||
let timestamp = formatter.string(from: date)
|
||||
|
||||
let filename = pattern
|
||||
.replacingOccurrences(of: "{app}", with: "Finder")
|
||||
.replacingOccurrences(of: "{mode}", with: "window")
|
||||
.replacingOccurrences(of: "{timestamp}", with: timestamp)
|
||||
|
||||
XCTAssertTrue(filename.contains("Finder"))
|
||||
XCTAssertTrue(filename.contains("window"))
|
||||
XCTAssertTrue(filename.contains("2023-11-14")) // Date from timestamp
|
||||
}
|
||||
|
||||
func testBlurDetection() {
|
||||
// Test blur detection threshold logic
|
||||
let blurThresholds: [Double] = [0.0, 0.5, 1.0]
|
||||
|
||||
for threshold in blurThresholds {
|
||||
XCTAssertGreaterThanOrEqual(threshold, 0.0)
|
||||
XCTAssertLessThanOrEqual(threshold, 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Error Response Tests
|
||||
|
||||
func testErrorResponseCreation() throws {
|
||||
// Test error response structure
|
||||
let error = CaptureError.appNotFound
|
||||
|
||||
let errorData = SwiftCliErrorData(
|
||||
error: error.rawValue,
|
||||
message: error.description,
|
||||
details: nil,
|
||||
suggestions: []
|
||||
)
|
||||
|
||||
XCTAssertEqual(errorData.error, "APP_NOT_FOUND")
|
||||
XCTAssertEqual(errorData.message, "Application not found")
|
||||
}
|
||||
|
||||
// MARK: - Integration Tests
|
||||
|
||||
func testImageCaptureDataEncoding() throws {
|
||||
// Test that ImageCaptureData can be encoded to JSON
|
||||
let captureData = ImageCaptureData(
|
||||
imageData: "base64data",
|
||||
imageUrl: nil,
|
||||
savedFile: nil,
|
||||
metadata: ImageMetadata(
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
fileSize: 1024000,
|
||||
format: "png",
|
||||
colorSpace: "sRGB",
|
||||
bitsPerPixel: 24,
|
||||
capturedAt: "2023-11-14T12:00:00Z"
|
||||
),
|
||||
captureMode: "screen",
|
||||
targetApp: nil,
|
||||
windowInfo: nil,
|
||||
isBlurry: false,
|
||||
blurScore: 0.1,
|
||||
debugLogs: []
|
||||
)
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
encoder.keyEncodingStrategy = .convertToSnakeCase
|
||||
|
||||
let data = try encoder.encode(captureData)
|
||||
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
|
||||
|
||||
XCTAssertNotNil(json)
|
||||
XCTAssertEqual(json?["image_data"] as? String, "base64data")
|
||||
XCTAssertEqual(json?["capture_mode"] as? String, "screen")
|
||||
XCTAssertEqual(json?["is_blurry"] as? Bool, false)
|
||||
|
||||
let metadata = json?["metadata"] as? [String: Any]
|
||||
XCTAssertEqual(metadata?["width"] as? Int, 1920)
|
||||
XCTAssertEqual(metadata?["height"] as? Int, 1080)
|
||||
}
|
||||
}
|
||||
250
peekaboo-cli/Tests/peekabooTests/ListCommandTests.swift
Normal file
250
peekaboo-cli/Tests/peekabooTests/ListCommandTests.swift
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
import XCTest
|
||||
import ArgumentParser
|
||||
@testable import peekaboo
|
||||
|
||||
final class ListCommandTests: XCTestCase {
|
||||
|
||||
// MARK: - Command Parsing Tests
|
||||
|
||||
func testListCommandParsing() throws {
|
||||
// Test basic command parsing
|
||||
let command = try ListCommand.parse(["running_applications"])
|
||||
|
||||
XCTAssertEqual(command.type, .runningApplications)
|
||||
XCTAssertFalse(command.jsonOutput)
|
||||
}
|
||||
|
||||
func testListCommandWithJSONOutput() throws {
|
||||
// Test JSON output flag
|
||||
let command = try ListCommand.parse(["server_status", "--json-output"])
|
||||
|
||||
XCTAssertEqual(command.type, .serverStatus)
|
||||
XCTAssertTrue(command.jsonOutput)
|
||||
}
|
||||
|
||||
func testListCommandAllTypes() throws {
|
||||
// Test all list types parse correctly
|
||||
let types: [(String, ListType)] = [
|
||||
("running_applications", .runningApplications),
|
||||
("windows", .windows),
|
||||
("server_status", .serverStatus)
|
||||
]
|
||||
|
||||
for (arg, expectedType) in types {
|
||||
let command = try ListCommand.parse([arg])
|
||||
XCTAssertEqual(command.type, expectedType)
|
||||
}
|
||||
}
|
||||
|
||||
func testListCommandWithApp() throws {
|
||||
// Test windows list with app filter
|
||||
let command = try ListCommand.parse([
|
||||
"windows",
|
||||
"--app", "Finder"
|
||||
])
|
||||
|
||||
XCTAssertEqual(command.type, .windows)
|
||||
XCTAssertEqual(command.app, "Finder")
|
||||
}
|
||||
|
||||
func testListCommandWithWindowDetail() throws {
|
||||
// Test window detail options
|
||||
let command = try ListCommand.parse([
|
||||
"windows",
|
||||
"--window-detail", "full"
|
||||
])
|
||||
|
||||
XCTAssertEqual(command.type, .windows)
|
||||
XCTAssertEqual(command.windowDetail, .full)
|
||||
}
|
||||
|
||||
// MARK: - ListType Tests
|
||||
|
||||
func testListTypeRawValues() {
|
||||
// Test list type string values
|
||||
XCTAssertEqual(ListType.runningApplications.rawValue, "running_applications")
|
||||
XCTAssertEqual(ListType.windows.rawValue, "windows")
|
||||
XCTAssertEqual(ListType.serverStatus.rawValue, "server_status")
|
||||
}
|
||||
|
||||
func testWindowDetailOptionRawValues() {
|
||||
// Test window detail option values
|
||||
XCTAssertEqual(WindowDetailOption.none.rawValue, "none")
|
||||
XCTAssertEqual(WindowDetailOption.basic.rawValue, "basic")
|
||||
XCTAssertEqual(WindowDetailOption.full.rawValue, "full")
|
||||
}
|
||||
|
||||
// MARK: - Data Structure Tests
|
||||
|
||||
func testApplicationListDataEncoding() throws {
|
||||
// Test ApplicationListData JSON encoding
|
||||
let appData = ApplicationListData(
|
||||
applications: [
|
||||
ApplicationInfo(
|
||||
name: "Finder",
|
||||
bundleIdentifier: "com.apple.finder",
|
||||
processIdentifier: 123,
|
||||
isActive: true
|
||||
),
|
||||
ApplicationInfo(
|
||||
name: "Safari",
|
||||
bundleIdentifier: "com.apple.Safari",
|
||||
processIdentifier: 456,
|
||||
isActive: false
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
encoder.keyEncodingStrategy = .convertToSnakeCase
|
||||
|
||||
let data = try encoder.encode(appData)
|
||||
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
|
||||
|
||||
XCTAssertNotNil(json)
|
||||
let apps = json?["applications"] as? [[String: Any]]
|
||||
XCTAssertEqual(apps?.count, 2)
|
||||
|
||||
let firstApp = apps?.first
|
||||
XCTAssertEqual(firstApp?["name"] as? String, "Finder")
|
||||
XCTAssertEqual(firstApp?["bundle_identifier"] as? String, "com.apple.finder")
|
||||
XCTAssertEqual(firstApp?["process_identifier"] as? Int, 123)
|
||||
XCTAssertEqual(firstApp?["is_active"] as? Bool, true)
|
||||
}
|
||||
|
||||
func testWindowListDataEncoding() throws {
|
||||
// Test WindowListData JSON encoding
|
||||
let windowData = WindowListData(
|
||||
targetApp: TargetApplicationInfo(
|
||||
name: "Finder",
|
||||
bundleIdentifier: "com.apple.finder",
|
||||
processIdentifier: 123
|
||||
),
|
||||
windows: [
|
||||
WindowData(
|
||||
windowInfo: WindowInfo(
|
||||
windowID: 1001,
|
||||
owningApplication: "Finder",
|
||||
windowTitle: "Documents",
|
||||
windowIndex: 0,
|
||||
bounds: WindowBounds(x: 100, y: 200, width: 800, height: 600),
|
||||
isOnScreen: true,
|
||||
windowLevel: 0
|
||||
),
|
||||
hasDetails: true
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
encoder.keyEncodingStrategy = .convertToSnakeCase
|
||||
|
||||
let data = try encoder.encode(windowData)
|
||||
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
|
||||
|
||||
XCTAssertNotNil(json)
|
||||
|
||||
let targetApp = json?["target_app"] as? [String: Any]
|
||||
XCTAssertEqual(targetApp?["name"] as? String, "Finder")
|
||||
|
||||
let windows = json?["windows"] as? [[String: Any]]
|
||||
XCTAssertEqual(windows?.count, 1)
|
||||
|
||||
let firstWindow = windows?.first
|
||||
let windowInfo = firstWindow?["window_info"] as? [String: Any]
|
||||
XCTAssertEqual(windowInfo?["window_id"] as? Int, 1001)
|
||||
XCTAssertEqual(windowInfo?["owning_application"] as? String, "Finder")
|
||||
XCTAssertEqual(windowInfo?["window_title"] as? String, "Documents")
|
||||
}
|
||||
|
||||
func testServerStatusEncoding() throws {
|
||||
// Test ServerStatus JSON encoding
|
||||
let status = ServerStatus(
|
||||
hasScreenRecordingPermission: true,
|
||||
hasAccessibilityPermission: false
|
||||
)
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
encoder.keyEncodingStrategy = .convertToSnakeCase
|
||||
|
||||
let data = try encoder.encode(status)
|
||||
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
|
||||
|
||||
XCTAssertNotNil(json)
|
||||
XCTAssertEqual(json?["has_screen_recording_permission"] as? Bool, true)
|
||||
XCTAssertEqual(json?["has_accessibility_permission"] as? Bool, false)
|
||||
}
|
||||
|
||||
// MARK: - Window Specifier Tests
|
||||
|
||||
func testWindowSpecifierFromApp() {
|
||||
// Test window specifier creation from app
|
||||
let specifier = WindowSpecifier.app("Finder")
|
||||
|
||||
switch specifier {
|
||||
case .app(let name):
|
||||
XCTAssertEqual(name, "Finder")
|
||||
default:
|
||||
XCTFail("Expected app specifier")
|
||||
}
|
||||
}
|
||||
|
||||
func testWindowSpecifierFromWindowId() {
|
||||
// Test window specifier creation from window ID
|
||||
let specifier = WindowSpecifier.windowId(123)
|
||||
|
||||
switch specifier {
|
||||
case .windowId(let id):
|
||||
XCTAssertEqual(id, 123)
|
||||
default:
|
||||
XCTFail("Expected windowId specifier")
|
||||
}
|
||||
}
|
||||
|
||||
func testWindowSpecifierActiveWindow() {
|
||||
// Test active window specifier
|
||||
let specifier = WindowSpecifier.activeWindow
|
||||
|
||||
switch specifier {
|
||||
case .activeWindow:
|
||||
// Success
|
||||
break
|
||||
default:
|
||||
XCTFail("Expected activeWindow specifier")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Error Handling Tests
|
||||
|
||||
func testListCommandInvalidType() {
|
||||
// Test invalid list type
|
||||
XCTAssertThrowsError(try ListCommand.parse(["invalid_type"]))
|
||||
}
|
||||
|
||||
func testListCommandMissingType() {
|
||||
// Test missing list type
|
||||
XCTAssertThrowsError(try ListCommand.parse([]))
|
||||
}
|
||||
|
||||
// MARK: - Performance Tests
|
||||
|
||||
func testApplicationInfoEncodingPerformance() throws {
|
||||
// Test performance of encoding many applications
|
||||
let apps = (0..<100).map { i in
|
||||
ApplicationInfo(
|
||||
name: "App\(i)",
|
||||
bundleIdentifier: "com.example.app\(i)",
|
||||
processIdentifier: pid_t(1000 + i),
|
||||
isActive: i == 0
|
||||
)
|
||||
}
|
||||
|
||||
let appData = ApplicationListData(applications: apps)
|
||||
let encoder = JSONEncoder()
|
||||
encoder.keyEncodingStrategy = .convertToSnakeCase
|
||||
|
||||
measure {
|
||||
_ = try? encoder.encode(appData)
|
||||
}
|
||||
}
|
||||
}
|
||||
145
peekaboo-cli/Tests/peekabooTests/PermissionsCheckerTests.swift
Normal file
145
peekaboo-cli/Tests/peekabooTests/PermissionsCheckerTests.swift
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import XCTest
|
||||
@testable import peekaboo
|
||||
|
||||
final class PermissionsCheckerTests: XCTestCase {
|
||||
var permissionsChecker: PermissionsChecker!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
permissionsChecker = PermissionsChecker()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
permissionsChecker = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
// MARK: - hasScreenRecordingPermission Tests
|
||||
|
||||
func testHasScreenRecordingPermission() {
|
||||
// Test screen recording permission check
|
||||
let hasPermission = permissionsChecker.hasScreenRecordingPermission()
|
||||
|
||||
// This test will pass or fail based on actual system permissions
|
||||
// In CI/CD, this might need to be mocked
|
||||
XCTAssertNotNil(hasPermission)
|
||||
|
||||
// If running in a test environment without permissions, we expect false
|
||||
// If running locally with permissions granted, we expect true
|
||||
print("Screen recording permission status: \(hasPermission)")
|
||||
}
|
||||
|
||||
func testScreenRecordingPermissionConsistency() {
|
||||
// Test that multiple calls return consistent results
|
||||
let firstCheck = permissionsChecker.hasScreenRecordingPermission()
|
||||
let secondCheck = permissionsChecker.hasScreenRecordingPermission()
|
||||
|
||||
XCTAssertEqual(firstCheck, secondCheck, "Permission check should be consistent")
|
||||
}
|
||||
|
||||
// MARK: - hasAccessibilityPermission Tests
|
||||
|
||||
func testHasAccessibilityPermission() {
|
||||
// Test accessibility permission check
|
||||
let hasPermission = permissionsChecker.hasAccessibilityPermission()
|
||||
|
||||
// This will return the actual system state
|
||||
XCTAssertNotNil(hasPermission)
|
||||
|
||||
print("Accessibility permission status: \(hasPermission)")
|
||||
}
|
||||
|
||||
func testAccessibilityPermissionWithTrustedCheck() {
|
||||
// Test the AXIsProcessTrusted check
|
||||
let isTrusted = AXIsProcessTrusted()
|
||||
let hasPermission = permissionsChecker.hasAccessibilityPermission()
|
||||
|
||||
// These should match
|
||||
XCTAssertEqual(isTrusted, hasPermission)
|
||||
}
|
||||
|
||||
// MARK: - checkAllPermissions Tests
|
||||
|
||||
func testCheckAllPermissions() {
|
||||
// Test combined permissions check
|
||||
let (screenRecording, accessibility) = permissionsChecker.checkAllPermissions()
|
||||
|
||||
// Both should return boolean values
|
||||
XCTAssertNotNil(screenRecording)
|
||||
XCTAssertNotNil(accessibility)
|
||||
|
||||
// Verify individual checks match combined check
|
||||
XCTAssertEqual(screenRecording, permissionsChecker.hasScreenRecordingPermission())
|
||||
XCTAssertEqual(accessibility, permissionsChecker.hasAccessibilityPermission())
|
||||
}
|
||||
|
||||
// MARK: - Permission State Tests
|
||||
|
||||
func testPermissionStateEncoding() throws {
|
||||
// Test that permission states can be properly encoded to JSON
|
||||
let serverStatus = ServerStatus(
|
||||
hasScreenRecordingPermission: true,
|
||||
hasAccessibilityPermission: false
|
||||
)
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
encoder.keyEncodingStrategy = .convertToSnakeCase
|
||||
|
||||
let data = try encoder.encode(serverStatus)
|
||||
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
|
||||
|
||||
XCTAssertNotNil(json)
|
||||
XCTAssertEqual(json?["has_screen_recording_permission"] as? Bool, true)
|
||||
XCTAssertEqual(json?["has_accessibility_permission"] as? Bool, false)
|
||||
}
|
||||
|
||||
// MARK: - Error Handling Tests
|
||||
|
||||
func testPermissionDeniedError() {
|
||||
// Test error creation for permission denied
|
||||
let screenError = CaptureError.permissionDeniedScreenRecording
|
||||
let accessError = CaptureError.permissionDeniedAccessibility
|
||||
|
||||
XCTAssertEqual(screenError.description, "Screen recording permission is required")
|
||||
XCTAssertEqual(accessError.description, "Accessibility permission is required")
|
||||
}
|
||||
|
||||
// MARK: - Performance Tests
|
||||
|
||||
func testPermissionCheckPerformance() {
|
||||
// Test that permission checks are fast
|
||||
measure {
|
||||
_ = permissionsChecker.checkAllPermissions()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Mock Tests (for CI/CD)
|
||||
|
||||
func testMockPermissionScenarios() {
|
||||
// Test various permission scenarios for error handling
|
||||
|
||||
// Scenario 1: No permissions
|
||||
var status = ServerStatus(
|
||||
hasScreenRecordingPermission: false,
|
||||
hasAccessibilityPermission: false
|
||||
)
|
||||
XCTAssertFalse(status.hasScreenRecordingPermission)
|
||||
XCTAssertFalse(status.hasAccessibilityPermission)
|
||||
|
||||
// Scenario 2: Only screen recording
|
||||
status = ServerStatus(
|
||||
hasScreenRecordingPermission: true,
|
||||
hasAccessibilityPermission: false
|
||||
)
|
||||
XCTAssertTrue(status.hasScreenRecordingPermission)
|
||||
XCTAssertFalse(status.hasAccessibilityPermission)
|
||||
|
||||
// Scenario 3: Both permissions
|
||||
status = ServerStatus(
|
||||
hasScreenRecordingPermission: true,
|
||||
hasAccessibilityPermission: true
|
||||
)
|
||||
XCTAssertTrue(status.hasScreenRecordingPermission)
|
||||
XCTAssertTrue(status.hasAccessibilityPermission)
|
||||
}
|
||||
}
|
||||
174
peekaboo-cli/Tests/peekabooTests/WindowManagerTests.swift
Normal file
174
peekaboo-cli/Tests/peekabooTests/WindowManagerTests.swift
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
import XCTest
|
||||
@testable import peekaboo
|
||||
|
||||
final class WindowManagerTests: XCTestCase {
|
||||
var windowManager: WindowManager!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
windowManager = WindowManager()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
windowManager = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
// MARK: - getAllWindows Tests
|
||||
|
||||
func testGetAllWindows() throws {
|
||||
// Test getting all windows
|
||||
let windows = windowManager.getAllWindows()
|
||||
|
||||
// Should have at least some windows (Finder, menu bar, etc.)
|
||||
XCTAssertGreaterThan(windows.count, 0)
|
||||
|
||||
// Verify window properties
|
||||
for window in windows {
|
||||
XCTAssertNotNil(window[kCGWindowNumber])
|
||||
XCTAssertNotNil(window[kCGWindowBounds])
|
||||
}
|
||||
}
|
||||
|
||||
func testGetAllWindowsContainsFinder() throws {
|
||||
// Finder should always have windows
|
||||
let windows = windowManager.getAllWindows()
|
||||
|
||||
let finderWindows = windows.filter { window in
|
||||
(window[kCGWindowOwnerName] as? String) == "Finder"
|
||||
}
|
||||
|
||||
XCTAssertGreaterThan(finderWindows.count, 0, "Should find at least one Finder window")
|
||||
}
|
||||
|
||||
// MARK: - getWindowsForApp Tests
|
||||
|
||||
func testGetWindowsForAppByPID() throws {
|
||||
// Get Finder's PID
|
||||
let apps = NSWorkspace.shared.runningApplications
|
||||
guard let finder = apps.first(where: { $0.bundleIdentifier == "com.apple.finder" }) else {
|
||||
XCTFail("Finder not found")
|
||||
return
|
||||
}
|
||||
|
||||
let windows = windowManager.getWindowsForApp(pid: finder.processIdentifier, appName: nil)
|
||||
|
||||
XCTAssertGreaterThan(windows.count, 0)
|
||||
|
||||
// All windows should belong to Finder
|
||||
for window in windows {
|
||||
XCTAssertEqual(window[kCGWindowOwnerPID] as? pid_t, finder.processIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
func testGetWindowsForAppByName() throws {
|
||||
// Test filtering by app name
|
||||
let allWindows = windowManager.getAllWindows()
|
||||
let finderWindows = windowManager.getWindowsForApp(pid: nil, appName: "Finder")
|
||||
|
||||
// Should have fewer windows when filtered
|
||||
XCTAssertLessThanOrEqual(finderWindows.count, allWindows.count)
|
||||
|
||||
// All returned windows should be from Finder
|
||||
for window in finderWindows {
|
||||
XCTAssertEqual(window[kCGWindowOwnerName] as? String, "Finder")
|
||||
}
|
||||
}
|
||||
|
||||
func testGetWindowsForNonExistentApp() {
|
||||
// Test with non-existent app
|
||||
let windows = windowManager.getWindowsForApp(pid: 99999, appName: nil)
|
||||
|
||||
XCTAssertEqual(windows.count, 0)
|
||||
}
|
||||
|
||||
// MARK: - Window Filtering Tests
|
||||
|
||||
func testWindowFilteringExcludesInvisible() {
|
||||
// Get all windows including invisible ones
|
||||
let allWindows = windowManager.getAllWindows()
|
||||
|
||||
// Check that we're not including windows that are off-screen or have zero size
|
||||
for window in allWindows {
|
||||
if let bounds = window[kCGWindowBounds] as? CFDictionary {
|
||||
let rect = CGRect(dictionaryRepresentation: bounds) ?? .zero
|
||||
|
||||
// If window is included, it should have non-zero size
|
||||
if rect.width > 0 && rect.height > 0 {
|
||||
XCTAssertGreaterThan(rect.width, 0)
|
||||
XCTAssertGreaterThan(rect.height, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testWindowOrdering() {
|
||||
// Test that windows are ordered (typically by window level and order)
|
||||
let windows = windowManager.getAllWindows()
|
||||
|
||||
guard windows.count > 1 else {
|
||||
XCTSkip("Need multiple windows to test ordering")
|
||||
return
|
||||
}
|
||||
|
||||
// Windows should have window numbers
|
||||
for window in windows {
|
||||
XCTAssertNotNil(window[kCGWindowNumber])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Performance Tests
|
||||
|
||||
func testGetAllWindowsPerformance() {
|
||||
// Test performance of getting all windows
|
||||
measure {
|
||||
_ = windowManager.getAllWindows()
|
||||
}
|
||||
}
|
||||
|
||||
func testGetWindowsForAppPerformance() {
|
||||
// Test performance of filtered window retrieval
|
||||
measure {
|
||||
_ = windowManager.getWindowsForApp(pid: nil, appName: "Finder")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods Tests
|
||||
|
||||
func testCreateWindowInfo() {
|
||||
// Create a mock window dictionary
|
||||
let mockWindow: [CFString: Any] = [
|
||||
kCGWindowNumber: 123,
|
||||
kCGWindowOwnerName: "TestApp",
|
||||
kCGWindowName: "Test Window",
|
||||
kCGWindowOwnerPID: 456,
|
||||
kCGWindowBounds: [
|
||||
"X": 100,
|
||||
"Y": 200,
|
||||
"Width": 800,
|
||||
"Height": 600
|
||||
] as CFDictionary,
|
||||
kCGWindowIsOnscreen: true,
|
||||
kCGWindowLayer: 0
|
||||
]
|
||||
|
||||
// Test window info creation
|
||||
let windowInfo = WindowInfo(
|
||||
windowID: 123,
|
||||
owningApplication: "TestApp",
|
||||
windowTitle: "Test Window",
|
||||
windowIndex: 0,
|
||||
bounds: WindowBounds(x: 100, y: 200, width: 800, height: 600),
|
||||
isOnScreen: true,
|
||||
windowLevel: 0
|
||||
)
|
||||
|
||||
XCTAssertEqual(windowInfo.windowID, 123)
|
||||
XCTAssertEqual(windowInfo.owningApplication, "TestApp")
|
||||
XCTAssertEqual(windowInfo.windowTitle, "Test Window")
|
||||
XCTAssertEqual(windowInfo.bounds.x, 100)
|
||||
XCTAssertEqual(windowInfo.bounds.y, 200)
|
||||
XCTAssertEqual(windowInfo.bounds.width, 800)
|
||||
XCTAssertEqual(windowInfo.bounds.height, 600)
|
||||
}
|
||||
}
|
||||
|
|
@ -210,6 +210,99 @@ function checkSwift() {
|
|||
}
|
||||
logSuccess('Swift tests passed');
|
||||
|
||||
// Test Swift CLI commands directly
|
||||
log('Testing Swift CLI commands...', colors.cyan);
|
||||
|
||||
// Test help command
|
||||
const helpOutput = exec('./peekaboo --help', { allowFailure: true });
|
||||
if (!helpOutput || !helpOutput.includes('USAGE:')) {
|
||||
logError('Swift CLI help command failed');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Test version command
|
||||
const versionOutput = exec('./peekaboo --version', { allowFailure: true });
|
||||
if (!versionOutput) {
|
||||
logError('Swift CLI version command failed');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Test list server_status command
|
||||
const serverStatusOutput = exec('./peekaboo list server_status --json-output', { allowFailure: true });
|
||||
if (!serverStatusOutput) {
|
||||
logError('Swift CLI server_status command failed');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const status = JSON.parse(serverStatusOutput);
|
||||
if (!status.hasOwnProperty('has_screen_recording_permission') ||
|
||||
!status.hasOwnProperty('has_accessibility_permission')) {
|
||||
logError('Server status missing required fields');
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
logError('Swift CLI server_status JSON output is invalid');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Test list running_applications command
|
||||
const appsOutput = exec('./peekaboo list running_applications --json-output', { allowFailure: true });
|
||||
if (!appsOutput) {
|
||||
logError('Swift CLI running_applications command failed');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const appsData = JSON.parse(appsOutput);
|
||||
if (!appsData.applications || !Array.isArray(appsData.applications)) {
|
||||
logError('Applications list has invalid structure');
|
||||
return false;
|
||||
}
|
||||
// Should always have at least Finder running
|
||||
const hasApps = appsData.applications.length > 0;
|
||||
if (!hasApps) {
|
||||
logError('No running applications found (expected at least Finder)');
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
logError('Swift CLI applications JSON output is invalid');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Test list windows command for Finder
|
||||
const windowsOutput = exec('./peekaboo list windows --app Finder --json-output', { allowFailure: true });
|
||||
if (!windowsOutput) {
|
||||
logError('Swift CLI windows command failed');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const windowsData = JSON.parse(windowsOutput);
|
||||
if (!windowsData.target_app || !windowsData.windows) {
|
||||
logError('Windows list has invalid structure');
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
logError('Swift CLI windows JSON output is invalid');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Test error handling - non-existent app
|
||||
const errorOutput = exec('./peekaboo list windows --app NonExistentApp12345 --json-output 2>&1', { allowFailure: true });
|
||||
if (errorOutput && !errorOutput.includes('error')) {
|
||||
logWarning('Error handling may not be working correctly');
|
||||
}
|
||||
|
||||
// Test image command help
|
||||
const imageHelpOutput = exec('./peekaboo image --help', { allowFailure: true });
|
||||
if (!imageHelpOutput || !imageHelpOutput.includes('mode')) {
|
||||
logError('Swift CLI image help command failed');
|
||||
return false;
|
||||
}
|
||||
|
||||
logSuccess('Swift CLI commands working correctly');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -247,6 +340,366 @@ function checkVersionAvailability() {
|
|||
return true;
|
||||
}
|
||||
|
||||
function checkChangelog() {
|
||||
logStep('Changelog Entry Check');
|
||||
|
||||
const packageJson = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf8'));
|
||||
const version = packageJson.version;
|
||||
|
||||
// Read CHANGELOG.md
|
||||
const changelogPath = join(projectRoot, 'CHANGELOG.md');
|
||||
if (!existsSync(changelogPath)) {
|
||||
logError('CHANGELOG.md not found');
|
||||
return false;
|
||||
}
|
||||
|
||||
const changelog = readFileSync(changelogPath, 'utf8');
|
||||
|
||||
// Check for version entry (handle both x.x.x and x.x.x-beta.x formats)
|
||||
const versionPattern = new RegExp(`^#+\\s*(?:\\[)?${version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\])?`, 'm');
|
||||
if (!changelog.match(versionPattern)) {
|
||||
logError(`No entry found for version ${version} in CHANGELOG.md`);
|
||||
logError('Please add a changelog entry before releasing');
|
||||
return false;
|
||||
}
|
||||
|
||||
logSuccess(`CHANGELOG.md contains entry for version ${version}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkSecurityAudit() {
|
||||
logStep('Security Audit');
|
||||
|
||||
log('Running npm audit...', colors.cyan);
|
||||
|
||||
const auditResult = exec('npm audit --json', { allowFailure: true });
|
||||
|
||||
if (auditResult) {
|
||||
try {
|
||||
const audit = JSON.parse(auditResult);
|
||||
const vulnCount = audit.metadata?.vulnerabilities || {};
|
||||
const total = Object.values(vulnCount).reduce((sum, count) => sum + count, 0);
|
||||
|
||||
if (total > 0) {
|
||||
logWarning(`Found ${total} vulnerabilities:`);
|
||||
if (vulnCount.critical > 0) logError(` Critical: ${vulnCount.critical}`);
|
||||
if (vulnCount.high > 0) logError(` High: ${vulnCount.high}`);
|
||||
if (vulnCount.moderate > 0) logWarning(` Moderate: ${vulnCount.moderate}`);
|
||||
if (vulnCount.low > 0) log(` Low: ${vulnCount.low}`, colors.yellow);
|
||||
|
||||
if (vulnCount.critical > 0 || vulnCount.high > 0) {
|
||||
logError('Critical or high severity vulnerabilities found. Please fix before releasing.');
|
||||
return false;
|
||||
}
|
||||
|
||||
logWarning('Non-critical vulnerabilities found. Consider fixing before release.');
|
||||
} else {
|
||||
logSuccess('No security vulnerabilities found');
|
||||
}
|
||||
} catch (e) {
|
||||
logWarning('Could not parse npm audit results');
|
||||
}
|
||||
} else {
|
||||
logSuccess('No security vulnerabilities found');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkPackageSize() {
|
||||
logStep('Package Size Check');
|
||||
|
||||
// Create a temporary package to get accurate size
|
||||
log('Calculating package size...', colors.cyan);
|
||||
const packOutput = exec('npm pack --dry-run 2>&1');
|
||||
|
||||
// Extract size information
|
||||
const unpackedMatch = packOutput.match(/unpacked size: ([^\n]+)/);
|
||||
|
||||
if (unpackedMatch) {
|
||||
const sizeStr = unpackedMatch[1];
|
||||
|
||||
// Convert to bytes for comparison
|
||||
let sizeInBytes = 0;
|
||||
if (sizeStr.includes('MB')) {
|
||||
sizeInBytes = parseFloat(sizeStr) * 1024 * 1024;
|
||||
} else if (sizeStr.includes('kB')) {
|
||||
sizeInBytes = parseFloat(sizeStr) * 1024;
|
||||
} else if (sizeStr.includes('B')) {
|
||||
sizeInBytes = parseFloat(sizeStr);
|
||||
}
|
||||
|
||||
const maxSizeInBytes = 2 * 1024 * 1024; // 2MB
|
||||
|
||||
if (sizeInBytes > maxSizeInBytes) {
|
||||
logWarning(`Package size (${sizeStr}) exceeds 2MB threshold`);
|
||||
logWarning('Consider reviewing included files to reduce package size');
|
||||
} else {
|
||||
logSuccess(`Package size (${sizeStr}) is within acceptable limits`);
|
||||
}
|
||||
} else {
|
||||
logWarning('Could not determine package size');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkTypeScriptDeclarations() {
|
||||
logStep('TypeScript Declarations Check');
|
||||
|
||||
// Check if .d.ts files are generated
|
||||
const distPath = join(projectRoot, 'dist');
|
||||
|
||||
if (!existsSync(distPath)) {
|
||||
logError('dist/ directory not found. Please build the project first.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Look for .d.ts files
|
||||
const dtsFiles = exec(`find "${distPath}" -name "*.d.ts" -type f`, { allowFailure: true });
|
||||
|
||||
if (!dtsFiles || dtsFiles.trim() === '') {
|
||||
logError('No TypeScript declaration files (.d.ts) found in dist/');
|
||||
logError('Ensure TypeScript is configured to generate declarations');
|
||||
return false;
|
||||
}
|
||||
|
||||
const declarationFiles = dtsFiles.split('\n').filter(f => f.trim());
|
||||
log(`Found ${declarationFiles.length} TypeScript declaration files`, colors.cyan);
|
||||
|
||||
// Check for main declaration file
|
||||
const mainDtsPath = join(distPath, 'index.d.ts');
|
||||
if (!existsSync(mainDtsPath)) {
|
||||
logError('Missing main declaration file: dist/index.d.ts');
|
||||
return false;
|
||||
}
|
||||
|
||||
logSuccess('TypeScript declarations are properly generated');
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkMCPServerSmoke() {
|
||||
logStep('MCP Server Smoke Test');
|
||||
|
||||
const serverPath = join(projectRoot, 'dist', 'index.js');
|
||||
|
||||
if (!existsSync(serverPath)) {
|
||||
logError('Server not built. Please run build first.');
|
||||
return false;
|
||||
}
|
||||
|
||||
log('Testing MCP server with simple JSON-RPC request...', colors.cyan);
|
||||
|
||||
try {
|
||||
// Test with a simple tools/list request
|
||||
const testRequest = '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}';
|
||||
const result = exec(`echo '${testRequest}' | node "${serverPath}"`, { allowFailure: true });
|
||||
|
||||
if (!result) {
|
||||
logError('MCP server failed to respond');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse and validate response
|
||||
const lines = result.split('\n').filter(line => line.trim());
|
||||
const response = lines[lines.length - 1]; // Get last line (the actual response)
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(response);
|
||||
|
||||
if (parsed.error) {
|
||||
logError(`MCP server returned error: ${parsed.error.message}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!parsed.result || !parsed.result.tools) {
|
||||
logError('MCP server response missing expected tools array');
|
||||
return false;
|
||||
}
|
||||
|
||||
const toolCount = parsed.result.tools.length;
|
||||
log(`MCP server responded successfully with ${toolCount} tools`, colors.cyan);
|
||||
|
||||
} catch (e) {
|
||||
logError('Failed to parse MCP server response');
|
||||
logError(`Response: ${response}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logError(`MCP server smoke test failed: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
logSuccess('MCP server smoke test passed');
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkSwiftCLIIntegration() {
|
||||
logStep('Swift CLI Integration Tests');
|
||||
|
||||
log('Testing Swift CLI error handling and edge cases...', colors.cyan);
|
||||
|
||||
// Test 1: Invalid command
|
||||
const invalidCmd = exec('./peekaboo invalid-command 2>&1', { allowFailure: true });
|
||||
if (!invalidCmd || !invalidCmd.includes('Error') && !invalidCmd.includes('USAGE')) {
|
||||
logError('Swift CLI should show error for invalid command');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Test 2: Missing required arguments
|
||||
const missingArgs = exec('./peekaboo image --mode app --json-output 2>&1', { allowFailure: true });
|
||||
if (!missingArgs || (!missingArgs.includes('error') && !missingArgs.includes('Error'))) {
|
||||
logError('Swift CLI should show error for missing --app with app mode');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Test 3: Invalid window ID
|
||||
const invalidWindowId = exec('./peekaboo image --mode window --window-id abc --json-output 2>&1', { allowFailure: true });
|
||||
if (!invalidWindowId || !invalidWindowId.includes('Error')) {
|
||||
logError('Swift CLI should show error for invalid window ID');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Test 4: Test all subcommands are available
|
||||
const subcommands = ['list', 'image'];
|
||||
for (const cmd of subcommands) {
|
||||
const helpOutput = exec(`./peekaboo ${cmd} --help`, { allowFailure: true });
|
||||
if (!helpOutput || !helpOutput.includes('USAGE')) {
|
||||
logError(`Swift CLI ${cmd} command help not available`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Test 5: JSON output format validation
|
||||
const formats = [
|
||||
{ cmd: './peekaboo list server_status --json-output', required: ['has_screen_recording_permission'] },
|
||||
{ cmd: './peekaboo list running_applications --json-output', required: ['applications'] }
|
||||
];
|
||||
|
||||
for (const { cmd, required } of formats) {
|
||||
const output = exec(cmd, { allowFailure: true });
|
||||
if (!output) {
|
||||
logError(`Command failed: ${cmd}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(output);
|
||||
for (const field of required) {
|
||||
if (!(field in data)) {
|
||||
logError(`Missing required field '${field}' in: ${cmd}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logError(`Invalid JSON from: ${cmd}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Test 6: Permission handling
|
||||
const permissionTest = exec('./peekaboo list server_status --json-output', { allowFailure: true });
|
||||
if (permissionTest) {
|
||||
try {
|
||||
const status = JSON.parse(permissionTest);
|
||||
log(`Permissions - Screen Recording: ${status.has_screen_recording_permission}, Accessibility: ${status.has_accessibility_permission}`, colors.cyan);
|
||||
} catch (e) {
|
||||
// Ignore, already tested above
|
||||
}
|
||||
}
|
||||
|
||||
logSuccess('Swift CLI integration tests passed');
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkVersionConsistency() {
|
||||
logStep('Version Consistency Check');
|
||||
|
||||
const packageJsonPath = join(projectRoot, 'package.json');
|
||||
const packageLockPath = join(projectRoot, 'package-lock.json');
|
||||
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
||||
const packageVersion = packageJson.version;
|
||||
|
||||
// Check package-lock.json
|
||||
if (!existsSync(packageLockPath)) {
|
||||
logError('package-lock.json not found');
|
||||
return false;
|
||||
}
|
||||
|
||||
const packageLock = JSON.parse(readFileSync(packageLockPath, 'utf8'));
|
||||
const lockVersion = packageLock.version;
|
||||
|
||||
if (packageVersion !== lockVersion) {
|
||||
logError(`Version mismatch: package.json has ${packageVersion}, package-lock.json has ${lockVersion}`);
|
||||
logError('Run "npm install" to update package-lock.json');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Also check that the package name matches in package-lock
|
||||
if (packageLock.packages && packageLock.packages[''] && packageLock.packages[''].version !== packageVersion) {
|
||||
logError(`Version mismatch in package-lock.json packages section`);
|
||||
return false;
|
||||
}
|
||||
|
||||
logSuccess(`Version ${packageVersion} is consistent across package.json and package-lock.json`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkRequiredFields() {
|
||||
logStep('Required Fields Validation');
|
||||
|
||||
const packageJson = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf8'));
|
||||
|
||||
const requiredFields = {
|
||||
'name': 'Package name',
|
||||
'version': 'Package version',
|
||||
'description': 'Package description',
|
||||
'main': 'Main entry point',
|
||||
'type': 'Module type',
|
||||
'scripts': 'Scripts section',
|
||||
'repository': 'Repository information',
|
||||
'keywords': 'Keywords for npm search',
|
||||
'author': 'Author information',
|
||||
'license': 'License',
|
||||
'engines': 'Node.js engine requirements',
|
||||
'files': 'Files to include in package'
|
||||
};
|
||||
|
||||
const missingFields = [];
|
||||
|
||||
for (const [field, description] of Object.entries(requiredFields)) {
|
||||
if (!packageJson[field]) {
|
||||
missingFields.push(`${field} (${description})`);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
logError('Missing required fields in package.json:');
|
||||
missingFields.forEach(field => logError(` - ${field}`));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Additional validations
|
||||
if (!packageJson.repository || typeof packageJson.repository !== 'object' || !packageJson.repository.url) {
|
||||
logError('Repository field must be an object with a url property');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Array.isArray(packageJson.keywords) || packageJson.keywords.length === 0) {
|
||||
logWarning('Keywords array is empty. Consider adding keywords for better discoverability');
|
||||
}
|
||||
|
||||
if (!packageJson.engines || !packageJson.engines.node) {
|
||||
logError('Missing engines.node field to specify Node.js version requirements');
|
||||
return false;
|
||||
}
|
||||
|
||||
logSuccess('All required fields are present in package.json');
|
||||
return true;
|
||||
}
|
||||
|
||||
function buildAndVerifyPackage() {
|
||||
logStep('Build and Package Verification');
|
||||
|
||||
|
|
@ -372,11 +825,19 @@ async function main() {
|
|||
|
||||
const checks = [
|
||||
checkGitStatus,
|
||||
checkRequiredFields,
|
||||
checkDependencies,
|
||||
checkSecurityAudit,
|
||||
checkVersionAvailability,
|
||||
checkVersionConsistency,
|
||||
checkChangelog,
|
||||
checkTypeScript,
|
||||
checkTypeScriptDeclarations,
|
||||
checkSwift,
|
||||
buildAndVerifyPackage
|
||||
buildAndVerifyPackage,
|
||||
checkSwiftCLIIntegration,
|
||||
checkPackageSize,
|
||||
checkMCPServerSmoke
|
||||
];
|
||||
|
||||
for (const check of checks) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue