Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
46a7228
refactor: remove deprecated logging tools and manifests
cameroncooke Apr 8, 2026
d922fd7
refactor: add pipeline event types, xcodebuild parsers, and event bui…
cameroncooke Apr 8, 2026
255ecb4
fix: preserve suiteName in Swift Testing issue parser
cameroncooke Apr 9, 2026
904c110
fix: handle arbitrary nesting depth in parseRawTestName
cameroncooke Apr 9, 2026
8415cd7
fix: delete consumed pendingFailureDurations entries to prevent stale…
cameroncooke Apr 9, 2026
ccb2ed9
fix: correct parser patterns validated against real xcodebuild output
cameroncooke Apr 9, 2026
1f3cb73
fix: snapshot array mutation, overly broad noise regex, and Swift Tes…
cameroncooke Apr 9, 2026
ac3f9c1
fix: apply prettier formatting to Swift Testing parser files
cameroncooke Apr 9, 2026
5b1e785
fix: increment failedCount for failed results without pending issue
cameroncooke Apr 9, 2026
f7a685c
test: add failing tests for dedup key collision and parameterized cas…
cameroncooke Apr 9, 2026
e3e04ac
fix: include test name in dedup key and capture parameterized case count
cameroncooke Apr 9, 2026
fdad5b6
fix: handle colons in parameterized argument values and fix formatting
cameroncooke Apr 9, 2026
3c7fe5f
test: add failing test for parameterized caseCount in xcodebuild even…
cameroncooke Apr 9, 2026
ce10d30
fix: use caseCount for parameterized test progress in both event parsers
cameroncooke Apr 9, 2026
7c13ea9
fix: remove dead noise patterns and rename durationText to displayDur…
cameroncooke Apr 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 199 additions & 0 deletions src/types/pipeline-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
export type XcodebuildOperation = 'BUILD' | 'TEST';

export type XcodebuildStage =
| 'RESOLVING_PACKAGES'
| 'COMPILING'
| 'LINKING'
| 'PREPARING_TESTS'
| 'RUN_TESTS'
| 'ARCHIVING'
| 'COMPLETED';

export const STAGE_RANK: Record<XcodebuildStage, number> = {
RESOLVING_PACKAGES: 0,
COMPILING: 1,
LINKING: 2,
PREPARING_TESTS: 3,
RUN_TESTS: 4,
ARCHIVING: 5,
COMPLETED: 6,
};

interface BaseEvent {
timestamp: string;
}

// --- Canonical types (used by ALL tools) ---

export interface HeaderEvent extends BaseEvent {
type: 'header';
operation: string;
params: Array<{ label: string; value: string }>;
}

export interface StatusLineEvent extends BaseEvent {
type: 'status-line';
level: 'success' | 'error' | 'info' | 'warning';
message: string;
}

export interface SummaryEvent extends BaseEvent {
type: 'summary';
operation?: string;
status: 'SUCCEEDED' | 'FAILED';
totalTests?: number;
passedTests?: number;
failedTests?: number;
skippedTests?: number;
durationMs?: number;
}

export interface SectionEvent extends BaseEvent {
type: 'section';
title: string;
icon?: 'red-circle' | 'yellow-circle' | 'green-circle' | 'checkmark' | 'cross' | 'info';
lines: string[];
blankLineAfterTitle?: boolean;
}

export interface DetailTreeEvent extends BaseEvent {
type: 'detail-tree';
items: Array<{ label: string; value: string }>;
}

export interface TableEvent extends BaseEvent {
type: 'table';
heading?: string;
columns: string[];
rows: Array<Record<string, string>>;
}

export interface FileRefEvent extends BaseEvent {
type: 'file-ref';
label?: string;
path: string;
}

export interface NextStepsEvent extends BaseEvent {
type: 'next-steps';
steps: Array<{
label?: string;
tool?: string;
workflow?: string;
cliTool?: string;
params?: Record<string, string | number | boolean>;
}>;
runtime?: 'cli' | 'daemon' | 'mcp';
}

// --- Xcodebuild-specific types ---

export interface BuildStageEvent extends BaseEvent {
type: 'build-stage';
operation: XcodebuildOperation;
stage: XcodebuildStage;
message: string;
}

export interface CompilerWarningEvent extends BaseEvent {
type: 'compiler-warning';
operation: XcodebuildOperation;
message: string;
location?: string;
rawLine: string;
}

export interface CompilerErrorEvent extends BaseEvent {
type: 'compiler-error';
operation: XcodebuildOperation;
message: string;
location?: string;
rawLine: string;
}

export interface TestDiscoveryEvent extends BaseEvent {
type: 'test-discovery';
operation: 'TEST';
total: number;
tests: string[];
truncated: boolean;
}

export interface TestProgressEvent extends BaseEvent {
type: 'test-progress';
operation: 'TEST';
completed: number;
failed: number;
skipped: number;
}

export interface TestFailureEvent extends BaseEvent {
type: 'test-failure';
operation: 'TEST';
target?: string;
suite?: string;
test?: string;
message: string;
location?: string;
durationMs?: number;
}

// --- Union types ---

/** Generic UI/output events usable by any tool */
export type CommonPipelineEvent =
| HeaderEvent
| StatusLineEvent
| SummaryEvent
| SectionEvent
| DetailTreeEvent
| TableEvent
| FileRefEvent
| NextStepsEvent;

/** Build/test-specific events (xcodebuild, swift build/test/run) */
export type BuildTestPipelineEvent =
| BuildStageEvent
| CompilerWarningEvent
| CompilerErrorEvent
| TestDiscoveryEvent
| TestProgressEvent
| TestFailureEvent;

export type PipelineEvent = CommonPipelineEvent | BuildTestPipelineEvent;

// --- Build-run notice types (used by xcodebuild pipeline internals) ---

export type NoticeLevel = 'info' | 'success' | 'warning';

export type BuildRunStepName =
| 'resolve-app-path'
| 'resolve-simulator'
| 'boot-simulator'
| 'install-app'
| 'extract-bundle-id'
| 'launch-app';

export type BuildRunStepStatus = 'started' | 'succeeded';

export interface BuildRunStepNoticeData {
step: BuildRunStepName;
status: BuildRunStepStatus;
appPath?: string;
}

export interface BuildRunResultNoticeData {
scheme: string;
platform: string;
target: string;
appPath: string;
launchState: 'requested' | 'running';
bundleId?: string;
appId?: string;
processId?: number;
buildLogPath?: string;
runtimeLogPath?: string;
osLogPath?: string;
}

export type NoticeCode = 'build-run-step' | 'build-run-result';
157 changes: 157 additions & 0 deletions src/utils/__tests__/swift-testing-line-parsers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { describe, it, expect } from 'vitest';
import {
parseSwiftTestingResultLine,
parseSwiftTestingIssueLine,
parseSwiftTestingRunSummary,
parseSwiftTestingContinuationLine,
parseXcodebuildSwiftTestingLine,
} from '../swift-testing-line-parsers.ts';

describe('Swift Testing line parsers', () => {
describe('parseSwiftTestingResultLine', () => {
it('should parse a passed test', () => {
const result = parseSwiftTestingResultLine(
'✔ Test "Basic math operations" passed after 0.001 seconds.',
);
expect(result).toEqual({
status: 'passed',
rawName: 'Basic math operations',
testName: 'Basic math operations',
durationText: '0.001s',
});
});

it('should parse a failed test', () => {
const result = parseSwiftTestingResultLine(
'✘ Test "Expected failure" failed after 0.001 seconds with 1 issue.',
);
expect(result).toEqual({
status: 'failed',
rawName: 'Expected failure',
testName: 'Expected failure',
durationText: '0.001s',
});
});

it('should parse a skipped test', () => {
const result = parseSwiftTestingResultLine('◇ Test "Disabled test" skipped.');
expect(result).toEqual({
status: 'skipped',
rawName: 'Disabled test',
testName: 'Disabled test',
});
});

it('should return null for non-matching lines', () => {
expect(parseSwiftTestingResultLine('◇ Test "Foo" started.')).toBeNull();
expect(parseSwiftTestingResultLine('random text')).toBeNull();
});
});

describe('parseSwiftTestingIssueLine', () => {
it('should parse an issue with location', () => {
const result = parseSwiftTestingIssueLine(
'✘ Test "Expected failure" recorded an issue at SimpleTests.swift:48:5: Expectation failed: true == false',
);
expect(result).toEqual({
rawTestName: 'Expected failure',
testName: 'Expected failure',
location: 'SimpleTests.swift:48',
message: 'Expectation failed: true == false',
});
});

it('should parse an issue without location', () => {
const result = parseSwiftTestingIssueLine(
'✘ Test "Some test" recorded an issue: Something went wrong',
);
expect(result).toEqual({
rawTestName: 'Some test',
testName: 'Some test',
message: 'Something went wrong',
});
});

it('should return null for non-matching lines', () => {
expect(parseSwiftTestingIssueLine('✘ Test "Foo" failed after 0.001 seconds')).toBeNull();
});
});

describe('parseSwiftTestingRunSummary', () => {
it('should parse a failed run summary', () => {
const result = parseSwiftTestingRunSummary(
'✘ Test run with 6 tests in 0 suites failed after 0.001 seconds with 1 issue.',
);
expect(result).toEqual({
executed: 6,
failed: 1,
durationText: '0.001s',
});
});

it('should parse a passed run summary', () => {
const result = parseSwiftTestingRunSummary(
'✔ Test run with 5 tests in 2 suites passed after 0.003 seconds.',
);
expect(result).toEqual({
executed: 5,
failed: 0,
durationText: '0.003s',
});
});

it('should return null for non-matching lines', () => {
expect(parseSwiftTestingRunSummary('random text')).toBeNull();
});
});

describe('parseSwiftTestingContinuationLine', () => {
it('should parse a continuation line', () => {
expect(parseSwiftTestingContinuationLine('↳ This test should fail')).toBe(
'This test should fail',
);
});

it('should return null for non-continuation lines', () => {
expect(parseSwiftTestingContinuationLine('regular line')).toBeNull();
});
});

describe('parseXcodebuildSwiftTestingLine', () => {
it('should parse a passed test case', () => {
const result = parseXcodebuildSwiftTestingLine(
"Test case 'MCPTestTests/appNameIsCorrect()' passed on 'My Mac - MCPTest (78757)' (0.000 seconds)",
);
expect(result).toEqual({
status: 'passed',
rawName: 'MCPTestTests/appNameIsCorrect()',
suiteName: 'MCPTestTests',
testName: 'appNameIsCorrect()',
durationText: '0.000s',
});
});

it('should parse a failed test case', () => {
const result = parseXcodebuildSwiftTestingLine(
"Test case 'MCPTestTests/deliberateFailure()' failed on 'My Mac - MCPTest (78757)' (0.000 seconds)",
);
expect(result).toEqual({
status: 'failed',
rawName: 'MCPTestTests/deliberateFailure()',
suiteName: 'MCPTestTests',
testName: 'deliberateFailure()',
durationText: '0.000s',
});
});

it('should return null for XCTest format lines', () => {
expect(
parseXcodebuildSwiftTestingLine("Test Case '-[Suite test]' passed (0.001 seconds)."),
).toBeNull();
});

it('should return null for non-matching lines', () => {
expect(parseXcodebuildSwiftTestingLine('random text')).toBeNull();
});
});
});
Loading
Loading