# /dev-gen-test — Sinh Dev Self-Check Tests

> **Scope — dev self-check (smoke), không phải bộ test chính thức.** Các test này để
> dev nhanh chóng kiểm chứng code mình sinh ra so với các BDD scenario. Đây là một
> self-check của dev, **không** phải bộ test authoritative của QC/dev-team (cái đó có
> flow riêng, implement ở nơi khác). Kết quả hiện lên dashboard Living Docs như một tín hiệu
> **dev self-test** để QC thấy dev đã tự chạy check của mình.

## Gate
{{include:steps/gate.md}}

*Lưu ý: Với lệnh này, target ở Bước 1 là một UC-ID hoặc path file `.feature`. Tìm file feature tại `{paths.specs_dir}/{domain}/*/bdd/**/{UC-ID}-*.feature` (glob khớp xuyên các PRD — filename gồm hậu tố slug) và các file implementation gắn tag `@trace.implements={UC-ID}`.*

## Context
{{include:steps/context-loader.md}}

---

## Service Detection

Đọc `@trace.service` và `@trace.module` từ header file feature.

| Điều kiện | Hành động |
|---|---|
| `@trace.module` có mặt | Dùng làm `active_module` |
| `@trace.module` vắng | Dùng `tech_stack.module` từ project-context.yaml |
| `@trace.service` có mặt | Lưu làm `active_service` |
| `@trace.service` vắng | Dùng domain của `{UC-ID}` làm fallback |

**Phân loại platform type:**

| Platform | Modules |
|---|---|
| `backend` | `java-spring`, `golang`, `dotnet`, `php-laravel`, `context-engineering` |
| `web-frontend` | `react`, `nextjs`, `vue`, `nuxt`, `angular` |
| `mobile` | `flutter`, `react-native`, `ios-swiftui`, `android-compose` |

---

## CHECKPOINT — Test Plan

Trước khi sinh, quét file feature tìm scenario và các file implementation tìm class/function. Hiện:

```
Test Plan — {UC-ID} ({active_module})
──────────────────────────────────────
Platform   : {backend | web-frontend | mobile}
Scenarios  : {N} scenario từ file .feature
Impl files : {danh sách file gắn tag @trace.implements={UC-ID}}

Test cần sinh:
  {danh sách riêng theo platform — xem template bên dưới}

Proceed? (Y/N)
```

Chờ "Y" rõ ràng trước khi sinh.

---

## Generate

### Nếu `platform_type = backend`

#### java-spring

```java
// @trace.verifies={UC-ID}
// @trace.service={active_service}
// @trace.test_type=unit
class {Resource}ServiceImplTest {
    @Mock {Repository} {repository};
    @InjectMocks {Resource}ServiceImpl service;

    // methodName_whenValid_shouldReturnExpected()
    //   Given — mock repository returns data
    //   When  — call service method
    //   Then  — assert result matches expected

    // methodName_whenNotFound_shouldThrowException()
    //   Given — mock returns Optional.empty()
    //   When & Then — assertThrows({NotFoundException}.class, ...)
}

// @trace.verifies={UC-ID}
// @trace.service={active_service}
// @trace.test_type=integration
@WebMvcTest({Resource}Controller.class)
class {Resource}ControllerTest {
    @MockBean {Facade | Service} facade;

    // endpoint_shouldReturn200WhenValid()
    // endpoint_shouldReturn400WhenInvalid()
    // endpoint_shouldReturn404WhenNotFound()
    // endpoint_shouldReturn401WhenUnauthenticated()
}
```

Rules:
- Unit test: mock ở layer Repository, test logic Service
- Integration test: mock ở layer Facade/Service, chỉ test HTTP contract
- Không bao giờ mock class đang được test
- Theo naming: `methodName_whenCondition_shouldOutcome` (từ CLAUDE.md §6)

#### golang

```go
// @trace.verifies={UC-ID}
// Unit: table-driven tests for service layer
func Test{Resource}Service_{Method}(t *testing.T) {
    tests := []struct {
        name    string
        input   {InputType}
        want    {OutputType}
        wantErr bool
    }{
        {"valid input", ..., ..., false},
        {"not found", ..., nil, true},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) { ... })
    }
}

// @trace.verifies={UC-ID}
// Integration: HTTP handler tests using httptest
func Test{Resource}Handler_{Endpoint}(t *testing.T) {
    // setup router, mock service, fire request
    // assert status code and response body
}
```

#### dotnet

```csharp
// @trace.verifies={UC-ID}
// Unit: xUnit + Moq
public class {Resource}ServiceTests {
    private readonly Mock<I{Repository}> _repoMock = new();
    private readonly {Resource}Service _sut;

    [Fact]
    public async Task {Method}_WhenValid_Returns{Expected}() { }

    [Fact]
    public async Task {Method}_WhenNotFound_ThrowsNotFoundException() { }
}

// @trace.verifies={UC-ID}
// Integration: WebApplicationFactory
public class {Resource}ControllerTests : IClassFixture<WebApplicationFactory<Program>> {
    [Fact]
    public async Task {Endpoint}_Returns200_WhenValid() { }

    [Fact]
    public async Task {Endpoint}_Returns400_WhenInvalid() { }
}
```

#### php-laravel

```php
// @trace.verifies={UC-ID}
// Unit: PHPUnit
class {Resource}ServiceTest extends TestCase {
    public function test_{method}_when_valid_should_return_expected(): void { }
    public function test_{method}_when_not_found_should_throw(): void { }
}

// @trace.verifies={UC-ID}
// Feature: Laravel HTTP tests
class {Resource}ControllerTest extends TestCase {
    use RefreshDatabase;
    public function test_{endpoint}_returns_200_when_valid(): void {
        $response = $this->getJson('/api/{resource}');
        $response->assertStatus(200)->assertJsonStructure([...]);
    }
}
```

#### context-engineering

Kiểm tra `tech_stack.language` từ project-context.yaml để chọn đúng cú pháp test:

**Nếu language = Python** (mặc định):

```python
# @trace.verifies={UC-ID}
# @trace.test_type=unit
# Unit: test prompt orchestration functions

import pytest
from unittest.mock import patch, MagicMock

class Test{Resource}Prompt:
    # test_{function}_when_valid_input_should_return_expected_output()
    #   Given — mock LLM client returns controlled response
    #   When  — call prompt function with valid input
    #   Then  — assert output matches expected structure/content

    # test_{function}_when_llm_unavailable_should_raise()
    #   Given — mock LLM client raises connection error
    #   When & Then — assert specific exception is raised

    # test_{function}_trace_assertions()
    #   Given — run function with trace capture enabled
    #   Then  — assert @trace.implements tag present in function definition
    #           assert output conforms to expected schema

    def test_{function}_when_valid_should_return_expected(self, mock_llm):
        # Arrange
        mock_llm.complete.return_value = "{expected response}"
        # Act
        result = {function}(input={test_input})
        # Assert
        assert result == {expected_output}
        mock_llm.complete.assert_called_once_with(...)

    def test_{function}_when_invalid_input_should_raise(self):
        with pytest.raises({ExpectedError}):
            {function}(input=None)
```

**Nếu language = TypeScript / JavaScript** (Node.js LangChain.js, v.v.):

```typescript
// @trace.verifies={UC-ID}
// @trace.test_type=unit
import { jest } from '@jest/globals'

describe('{Resource}Prompt', () => {
  const mockLlm = { complete: jest.fn() }

  it('{scenario description}', async () => {
    mockLlm.complete.mockResolvedValue('{expected response}')
    const result = await {function}({ input: '{test_input}', llm: mockLlm })
    expect(result).toEqual({expected_output})
    expect(mockLlm.complete).toHaveBeenCalledWith(expect.objectContaining({ ... }))
  })

  it('throws when input is invalid', async () => {
    await expect({function}({ input: null, llm: mockLlm })).rejects.toThrow('{ExpectedError}')
  })
})
```

**Nếu language = Java** (LangChain4j, v.v.): dùng JUnit 5 + Mockito, cùng pattern như unit test java-spring ở trên — mock interface `ChatLanguageModel`.

Rules:
- Mock LLM client ở boundary — không bao giờ gọi LLM thật trong unit test
- Validate input schema và output schema riêng biệt
- Mỗi scenario trong `.feature` map sang một test function
- Dùng parameterized test cho nhiều biến thể input

---

### Nếu `platform_type = web-frontend`

#### react / nextjs / vue / nuxt / angular

```typescript
// @trace.verifies={UC-ID}
// @trace.service={active_service}
// @trace.test_type=component

// Component tests (Vitest + Testing Library)
describe('{ComponentName}', () => {
  it('renders correctly when data is loaded', () => {
    render(<{ComponentName} {...props} />)
    expect(screen.getByText('...')).toBeInTheDocument()
  })

  it('shows loading state while fetching', () => { })

  it('shows error message on API failure', () => { })

  it('calls handler when user interacts', async () => {
    await userEvent.click(screen.getByRole('button', { name: '...' }))
    expect(mockHandler).toHaveBeenCalledWith(...)
  })
})

// @trace.verifies={UC-ID}
// @trace.test_type=e2e

// E2E tests (Playwright or Cypress — use whichever is in project)
test('{scenario from .feature}', async ({ page }) => {
  await page.goto('/{route}')
  await page.getByRole('button', { name: '...' }).click()
  await expect(page.getByText('...')).toBeVisible()
})
```

Rules:
- Một file component test cho mỗi component liên quan tới UC
- Một file E2E test cho mỗi UC, phủ happy path + các error scenario chính
- Mock API call ở network layer (MSW hoặc cy.intercept), không phải ở component props
- Dùng accessible query (`getByRole`, `getByLabelText`) — tránh `getByTestId` trừ khi cần
- Mỗi test map đúng một scenario trong file `.feature`

---

### Nếu `platform_type = mobile`

#### flutter

```dart
// @trace.verifies={UC-ID}
// @trace.service={active_service}
// @trace.test_type=widget

// Widget tests
group('{FeatureName} widget tests', () {
  testWidgets('renders correctly when state is loaded', (tester) async {
    await tester.pumpWidget(MaterialApp(home: {Widget}()));
    await tester.pumpAndSettle();
    expect(find.text('...'), findsOneWidget);
  });

  testWidgets('shows loading indicator while fetching', (tester) async { });
  testWidgets('shows error widget on failure', (tester) async { });
  testWidgets('calls handler on tap', (tester) async {
    await tester.tap(find.byType(ElevatedButton));
    await tester.pumpAndSettle();
    verify(() => mockBloc.add({Event}())).called(1);
  });
});

// @trace.verifies={UC-ID}
// @trace.test_type=integration
// Integration test (flutter_test / integration_test package)
void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
  testWidgets('{scenario from .feature}', (tester) async {
    app.main();
    await tester.pumpAndSettle();
    // Navigate, interact, assert
  });
}
```

#### react-native

```typescript
// @trace.verifies={UC-ID}
// @trace.service={active_service}
// @trace.test_type=component

// Jest + React Native Testing Library
describe('{ComponentName}', () => {
  it('{scenario description}', () => {
    const { getByText, getByRole } = render(<{ComponentName} {...props} />)
    expect(getByText('...')).toBeTruthy()
  })

  it('calls navigation on button press', () => {
    const mockNavigate = jest.fn()
    const { getByRole } = render(<{ComponentName} navigation={{ navigate: mockNavigate }} />)
    fireEvent.press(getByRole('button'))
    expect(mockNavigate).toHaveBeenCalledWith('...')
  })
})
```

#### ios-swiftui

```swift
// @trace.verifies={UC-ID}
// @trace.service={active_service}
// @trace.test_type=unit

// XCTest — ViewModel unit tests
@MainActor
final class {Feature}ViewModelTests: XCTestCase {
    var sut: {Feature}ViewModel!
    var mockRepo: Mock{Repository}!

    override func setUp() async throws {
        mockRepo = Mock{Repository}()
        sut = {Feature}ViewModel(repository: mockRepo)
    }

    func test_{method}_whenValid_shouldUpdate{State}() async throws {
        // Given
        mockRepo.stub{Method}Result = {expected}
        // When
        await sut.{method}()
        // Then
        XCTAssertEqual(sut.{state}, {expected})
    }

    func test_{method}_whenError_shouldSetErrorState() async throws { }
}
```

#### android-compose

```kotlin
// @trace.verifies={UC-ID}
// @trace.service={active_service}
// @trace.test_type=unit

// Unit test — ViewModel
class {Feature}ViewModelTest {
    @get:Rule val mainDispatcherRule = MainDispatcherRule()
    private val mockRepo: {Repository} = mockk()
    private lateinit var sut: {Feature}ViewModel

    @Before fun setup() { sut = {Feature}ViewModel(mockRepo) }

    @Test fun `{method} when valid should emit success state`() = runTest {
        coEvery { mockRepo.{method}(any()) } returns Result.success({data})
        sut.{method}({input})
        assertEquals(UiState.Success({data}), sut.uiState.value)
    }
}

// @trace.verifies={UC-ID}
// @trace.test_type=ui

// UI test — Compose
@HiltAndroidTest
class {Feature}ScreenTest {
    @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
    @get:Rule(order = 1) val composeRule = createAndroidComposeRule<MainActivity>()

    @Test fun {scenario}_displaysExpectedUi() {
        composeRule.onNodeWithText("...").assertIsDisplayed()
        composeRule.onNodeWithContentDescription("...").performClick()
        composeRule.onNodeWithText("...").assertIsDisplayed()
    }
}
```

---

## Checklist

**Mọi platform:**
- [ ] `@trace.verifies` trên mỗi test class / test group
- [ ] `@trace.service` trên mỗi test class / test group
- [ ] Mỗi scenario trong file `.feature` có ≥ 1 test tương ứng
- [ ] Phủ happy path
- [ ] Phủ các scenario error / edge case chính

**Chỉ backend:**
- [ ] Mock đúng layer (Repository trong unit test, Facade/Service trong controller test)
- [ ] Không gọi DB thật trong unit test
- [ ] Test naming theo `methodName_whenCondition_shouldOutcome` (CLAUDE.md §6)

**Chỉ Frontend / Mobile:**
- [ ] Không hardcode delay (`sleep`, `setTimeout`) trong test — dùng `waitFor` / `pumpAndSettle`
- [ ] Dùng accessible query (role, label) không phải selector phụ thuộc implementation
- [ ] Mock API call ở network layer, không phải ở component level

---

## Write Trace State

Sau khi sinh tất cả file test, cập nhật **sổ của platform đang test** `{paths.trace_dir}/{domain}/{prd-slug}/{UC-ID}-{platform}.tsv` (`{platform}` = platform của code/`.feature` đang test) — với mỗi scenario, tìm row có sẵn theo `sc_id` và cập nhật:

| Cột | Giá trị |
|--------|-------|
| `test_count` | số test method phủ SC này |
| `test_classes` | tên test class / describe-block, ngăn cách bởi dấu phẩy |
| `dev_selftest` | `not_run` (test giờ đã tồn tại nhưng chưa chạy — `/dev-run-test` set pass/fail) |
| `last_updated` | hôm nay `YYYY-MM-DD` |

Giữ nguyên mọi cột khác (gồm `dev_selftest_at`, do `/dev-run-test` sở hữu, và `qc_status`/`qc_run_at`, do `/qc-run-test` sở hữu).

---

## Refresh Panel Mirror
{{include:steps/trace-mirror.md}}

---

## Output

{{include:steps/report-footer.md}}

```
/dev-gen-test Hoàn tất — {UC-ID} ({active_module})
  ✅ {TestClass1} ({N} tests)
  ✅ {TestClass2} ({N} tests)
Trace: {paths.trace_dir}/{domain}/{prd-slug}/{UC-ID}-{platform}.tsv updated
Next: /dev-run-test {UC-ID}

📊 Living Docs: chạy /validate-traces (hoặc /sync) để push trace này lên dashboard spec-module.
```
