Feature-marker test-only

Multi-platform Test Only mode. Reads platform-context.json and routes to the correct test framework guidance (Swift Testing for iOS, Jest/Vitest for Node.js, cargo test for Rust, pytest for Python, go test for Go).

install
source · Clone the upstream repo
git clone https://github.com/Viniciuscarvalho/Feature-marker
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Viniciuscarvalho/Feature-marker "$T" && mkdir -p ~/.claude/skills && cp -r "$T/feature-marker-dist/feature-marker/resources/spec-workflow/skills/test-only" ~/.claude/skills/viniciuscarvalho-feature-marker-test-only && rm -rf "$T"
manifest: feature-marker-dist/feature-marker/resources/spec-workflow/skills/test-only/SKILL.md
source content

Test Only Mode — Multi-Platform

Routes test generation to the correct framework based on the platform detected by the Platform Detection Engine.

Entry conditions

  • EXECUTION_MODE=test-only
    or
    --mode test-only
    flag
  • platform-context.json
    must exist (run Platform Detection first if missing)
  • Skips Phases 0–2 (inputs gate, planning, implementation)

Unified flow

/feature-marker --mode test-only prd-{slug}
      ↓
1. Load platform-context.json
      ↓
2. Identify files without test coverage (platform-specific convention)
      ↓
3. Present to user: "Found N files without tests. Generate? [yes/no/select]"
      ↓
4. Generate tests in correct framework for platform
      ↓
5. Run platform test suite
      ↓
6. Report: coverage before/after, tests passing/failing
      ↓
7. Save to .claude/feature-state/{slug}/test-results.md

Step 2: Finding untested files by platform

🍎 iOS / Swift

Files to test:

*ViewModel.swift
,
*UseCase.swift
,
*Repository.swift
,
*Service.swift

# Production Swift files without a *Tests.swift counterpart
find . -name "*.swift" \
  ! -name "*Tests.swift" \
  ! -name "*Mock*.swift" \
  ! -name "*Stub*.swift" \
  ! -path "*/Tests/*" \
  ! -path "*/.build/*" \
  -type f

For each found file, check if a

{FileName}Tests.swift
exists anywhere in the project:

  • If no → candidate for test generation
  • If yes → check test count (
    grep -c "@Test\|func test" {TestFile}
    )

🟨 Node.js / TypeScript

# TypeScript files without *.test.ts or *.spec.ts counterpart
find src -name "*.ts" \
  ! -name "*.test.ts" \
  ! -name "*.spec.ts" \
  ! -name "*.d.ts" \
  ! -path "*/node_modules/*" \
  -type f

Check if

{file}.test.ts
or
__tests__/{file}.test.ts
exists.

🦀 Rust

# Rust source files — check if they contain #[cfg(test)]
find src -name "*.rs" -type f | while read f; do
  grep -q "#\[cfg(test)\]" "$f" || echo "$f (no tests)"
done

🐍 Python

# Python files without test_*.py counterpart
find . -name "*.py" \
  ! -name "test_*.py" \
  ! -name "*_test.py" \
  ! -path "*/tests/*" \
  ! -path "*/.venv/*" \
  -type f

Check for

tests/test_{filename}.py
or
test_{filename}.py
in same directory.

🐹 Go

# Go files without _test.go counterpart
find . -name "*.go" \
  ! -name "*_test.go" \
  ! -path "*/vendor/*" \
  -type f

Check if

{filename_without_ext}_test.go
exists in same directory.


Step 4: Test generation patterns by platform

🍎 iOS / Swift — Swift Testing

When to use Swift Testing vs XCTest: Read

test_framework
from
platform-context.json
.

  • "swift-testing"
    → use
    @Test
    ,
    @Suite
    ,
    #expect
  • "xctest"
    → use
    XCTestCase
    ,
    func test*()
    ,
    XCTAssert*

Default for new files: always Swift Testing.

// Swift Testing pattern
import Testing
@testable import {ModuleName}

@Suite("{TypeName}")
struct {TypeName}Tests {

    @Test("does X when Y")
    func testBehavior() async throws {
        // Arrange
        let sut = {TypeName}(dep: Mock{Dep}())
        // Act
        let result = await sut.action()
        // Assert
        #expect(result == expectedValue)
    }

    @Test("handles failure gracefully")
    func testFailurePath() async {
        let sut = {TypeName}(dep: Mock{Dep}(shouldFail: true))
        let result = await sut.action()
        #expect(result == nil)
    }
}

Run:

swift test --parallel

🟨 Node.js — Jest or Vitest

Detect from

platform-context.json → platforms[0].test_command
:

  • Contains
    jest
    → Jest pattern
  • Contains
    vitest
    → Vitest pattern (identical API, different imports)
// Jest / Vitest pattern
import { {TypeName} } from '../{filename}'

describe('{TypeName}', () => {
  let sut: {TypeName}
  let mockDep: jest.Mocked<{DepType}>

  beforeEach(() => {
    mockDep = {
      method: jest.fn().mockResolvedValue(expectedValue),
    }
    sut = new {TypeName}(mockDep)
  })

  it('returns expected value when called with valid input', async () => {
    const result = await sut.action('valid-input')
    expect(result).toEqual(expectedValue)
    expect(mockDep.method).toHaveBeenCalledWith('valid-input')
  })

  it('throws error when dep fails', async () => {
    mockDep.method.mockRejectedValue(new Error('dep error'))
    await expect(sut.action('input')).rejects.toThrow('dep error')
  })
})

Run:

jest --findRelatedTests {modified_files}
or
vitest run

🦀 Rust — inline #[cfg(test)]

Add tests inside the same file, at the bottom:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_action_returns_expected() {
        let result = action("input");
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), expected_value);
    }

    #[test]
    fn test_action_fails_on_invalid_input() {
        let result = action("");
        assert!(result.is_err());
    }

    #[tokio::test]  // for async functions
    async fn test_async_action() {
        let result = async_action("input").await;
        assert!(result.is_ok());
    }
}

Run:

cargo test -- --nocapture

🐍 Python — pytest

# tests/test_{filename}.py
import pytest
from {module} import {TypeName}

class Test{TypeName}:
    def test_action_returns_expected(self, mock_dep):
        sut = {TypeName}(dep=mock_dep)
        result = sut.action("valid-input")
        assert result == expected_value

    def test_action_fails_on_invalid_input(self, mock_dep):
        mock_dep.method.side_effect = ValueError("invalid")
        sut = {TypeName}(dep=mock_dep)
        with pytest.raises(ValueError, match="invalid"):
            sut.action("")

    @pytest.mark.asyncio
    async def test_async_action(self, mock_dep):
        sut = {TypeName}(dep=mock_dep)
        result = await sut.async_action("input")
        assert result is not None

Run:

pytest tests/test_{filename}.py --tb=short -v

🐹 Go — standard testing package

// {filename}_test.go
package {package}

import (
    "testing"
)

func Test{TypeName}Action(t *testing.T) {
    t.Run("returns expected value for valid input", func(t *testing.T) {
        sut := New{TypeName}(mockDep)
        got, err := sut.Action("valid-input")
        if err != nil {
            t.Fatalf("unexpected error: %v", err)
        }
        if got != expectedValue {
            t.Errorf("got %q, want %q", got, expectedValue)
        }
    })

    t.Run("returns error for empty input", func(t *testing.T) {
        sut := New{TypeName}(mockDep)
        _, err := sut.Action("")
        if err == nil {
            t.Error("expected error for empty input, got nil")
        }
    })
}

Run:

go test ./... -v -run Test{TypeName}


Step 5: Test execution

After generating tests, run the full test suite for the platform:

PlatformCommand
iOS
swift test --parallel
Node.js (jest)
jest --findRelatedTests {modified_files}
Node.js (vitest)
vitest run --reporter=verbose
Rust
cargo test -- --nocapture
Python
pytest --tb=short -v {test_files}
Go
go test ./... -v

Step 6: Report

## Test Only Mode — Results

**Platform:** iOS (Swift Package)
**Test framework:** Swift Testing

### Files analyzed
- PersonalTrainerViewModel.swift — had 0 tests → 5 tests generated
- ConnectTrainerUseCase.swift — had 0 tests → 3 tests generated
- TrainerRepository.swift — skipped (user declined)

### Test run
Running: swift test --parallel
✅ 8 passed, 0 failed

### Coverage delta
Before: 42% (estimated)
After: ~61% (estimated)

iOS extra step: XcodeBuildMCP

After running

swift test
, if
capabilities.xcodebuildmcp_available == true
:

/xcodebuildmcp build_run_sim
→ ✅ App running on iPhone 16 Simulator (iOS 18.3)
or
→ ⚠️ Simulator build not attempted (XcodeBuildMCP not available)

This step is always non-blocking.

/swift-testing skill integration (iOS only)

When

primary_platform == "ios"
, invoke the
/swift-testing
skill for enriched guidance:

  • Test structure with
    @Suite
    and
    @Test
    annotations
  • #expect
    /
    #require
    macro usage
  • Async testing patterns
  • Test organization and traits

For all other platforms, the Test Only skill provides built-in guidance without invoking an external skill.