feat: tests for stages output + testability refactoring
- add PollInterval, StagesWriter, RetryBaseDelay to UniversalClient - 5 unit tests for waitForOperationFinish (mock API, 0.25s total) - Sonnet/DeepSeek/Opus comparison answers archived
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
# Sonnet: ответы по тестированию stages без реальных инстансов
|
||||
|
||||
## 1. Мок API
|
||||
|
||||
Достаточно мокать только `/instanceOperations/{uid}?fields=...`.
|
||||
|
||||
`waitForOperationFinish` делает ровно один тип запроса — `doRequest GET /instanceOperations/{uid}?fields=...`. Создаёшь `httptest.NewServer`, настраиваешь нужные ответы, конструируешь клиент:
|
||||
|
||||
```go
|
||||
c := &UniversalClient{
|
||||
HttpClient: &http.Client{},
|
||||
ApiEndpoint: testServer.URL,
|
||||
ApiToken: "test-token",
|
||||
}
|
||||
```
|
||||
|
||||
Мокать create → params → run **не нужно** — вызываешь `waitForOperationFinish` напрямую с любым `opUid`.
|
||||
|
||||
## 2. Время
|
||||
|
||||
Проблема: `time.NewTicker(5 * time.Second)` захардкожен, таймаут 30 минут.
|
||||
|
||||
**Решение: добавить поле `PollInterval time.Duration` в `UniversalClient`.**
|
||||
Нулевое значение → default 5s. В тесте ставить 1ms.
|
||||
|
||||
`time.Now()` мокать **не нужно** — `timeout` уже параметр функции. В тесте передаёшь `200ms`, дедлайн считается `time.Now().Add(200ms)` — реальное время, работает нормально.
|
||||
|
||||
## 3. Перехват вывода tty
|
||||
|
||||
**Рекомендуемый подход: добавить поле `Writer io.Writer` в `UniversalClient`.**
|
||||
В `waitForOperationFinish` заменить `ttyOut()` на: `if c.Writer != nil { w = c.Writer } else { w = ttyOut() }`.
|
||||
В тесте: `c.Writer = &bytes.Buffer{}` → проверяешь что именно напечатано. В проде: оставляешь nil → поведение не меняется.
|
||||
|
||||
`os.Pipe()` + подмена `os.Stderr` **не рекомендует** — глобальное состояние, ломается при параллельных тестах.
|
||||
|
||||
## 4. Конкретные тест-кейсы
|
||||
|
||||
| # | Сценарий | Mock отдаёт | Ожидаем |
|
||||
|---|----------|-------------|---------|
|
||||
| 1 | Stages по одному | poll 1: 1 stage без dtFinish; poll 2: тот же stage с dtFinish; poll 3: операция с dtFinish+isSuccessful=true | `err == nil` |
|
||||
| 2 | Все stages сразу | poll 1: dtFinish + isSuccessful=true + все stages завершены | `err == nil`, Writer содержит `[OK]` для каждого |
|
||||
| 3 | Fail на этапе 3 | stages[0,1] OK, stages[2] isSuccessful=false, операция dtFinish + isSuccessful=false + errorLog="disk error" | `err` содержит `"disk error"` |
|
||||
| 4 | API 500 | всегда 500 (doRequest уже делает 3 retry внутри) | `err != nil` |
|
||||
| 5 | Таймаут | никогда не отдаёт dtFinish | `err` содержит `"не завершилась за установленный таймаут"` |
|
||||
| 6 | Отмена ctx | poll завис | `err` содержит `"отменена"` |
|
||||
|
||||
## 5. Рефакторинг
|
||||
|
||||
**Да, минимально — два новых поля в `UniversalClient`:**
|
||||
|
||||
```go
|
||||
type UniversalClient struct {
|
||||
// ... существующие поля ...
|
||||
PollInterval time.Duration // 0 = default 5s
|
||||
Writer io.Writer // nil = ttyOut()
|
||||
}
|
||||
```
|
||||
|
||||
В `waitForOperationFinish` (~3 строки правки):
|
||||
- `interval := c.PollInterval; if interval == 0 { interval = 5*time.Second }`
|
||||
- `w := c.Writer; if w == nil { w = ttyOut(); defer w.(*os.File).Close() }`
|
||||
|
||||
Не ломает существующие вызовы (оба поля zero-value совместимы с текущим поведением).
|
||||
|
||||
**Что НЕ нужно менять:** сигнатуру функции, логику дедлайна через `time.Now()`, retry в `doRequest`.
|
||||
@@ -0,0 +1,48 @@
|
||||
# DeepSeek Flash: ответы по тестированию stages
|
||||
|
||||
См. HISTORY/SONNET/stages_testing_question.md — исходные вопросы.
|
||||
|
||||
## 1. Мок API
|
||||
|
||||
**Да, достаточно httptest.NewServer.** Только `/instanceOperations/{uid}?fields=...`. Цепочку create→params→run не мокать.
|
||||
|
||||
```go
|
||||
client := &UniversalClient{
|
||||
HttpClient: srv.Client(),
|
||||
ApiEndpoint: srv.URL,
|
||||
ApiToken: "test",
|
||||
}
|
||||
```
|
||||
|
||||
## 2. Время
|
||||
|
||||
**Поле PollInterval time.Duration** в UniversalClient. 0 → default 5s. В тесте 1ms.
|
||||
|
||||
Таймаут: передать маленький timeout параметром (например 30ms). time.Now мокать не нужно.
|
||||
|
||||
## 3. Перехват tty
|
||||
|
||||
Вариант B — os.Pipe + подмена os.Stderr. Минус: не потокобезопасно (без t.Parallel() ок).
|
||||
|
||||
Вариант C (чище) — вынести печать в метод printStages(w io.Writer, ...), тогда ttyOut для продешкна, bytes.Buffer для теста.
|
||||
|
||||
## 4. Тест-кейсы (5 шт)
|
||||
|
||||
| # | Кейс | Выдача сервера |
|
||||
|---|------|----------------|
|
||||
| 1 | stages по одному | счётчик вызовов: stage1 без dtFinish → stage1 с dtFinish, stage2 без → всё dtFinish |
|
||||
| 2 | все сразу | первый ответ: всё dtFinish, isSuccessful=true |
|
||||
| 3 | fail на этапе 3 | stage3: isSuccessful=false, errorLog="boom" |
|
||||
| 4 | API 500 → retry | 500 → 200. ⚠️ doRequest retry 2s→4s→8s, нужен RetryBaseDelay |
|
||||
| 5 | таймаут | всегда isInProgress=true, dtFinish пустой |
|
||||
|
||||
## 5. Рефакторинг
|
||||
|
||||
**Да, минимально. НО:** в коде стоит ⛔ запрет на правку без оператора.
|
||||
|
||||
Безопасный минимум:
|
||||
- PollInterval (0 → 5s default)
|
||||
- printStages(w io.Writer) — вынос вывода
|
||||
- RetryBaseDelay (0 → 2s default) — для кейса 500
|
||||
|
||||
Альтернатива без рефакторинга: тестировать с реальными 5s тиками — медленно.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Sonnet v2: ответы по тестированию stages (после исправленного брифа)
|
||||
|
||||
## Q1. Мок API
|
||||
Только `/instanceOperations/{uid}?fields=...`. `httptest.NewServer` + `HttpClient`/`ApiEndpoint`.
|
||||
|
||||
## Q2. Время
|
||||
Два hardcoded таймера: `time.NewTicker(5s)` и `doRequest` delays (2s→4s→8s).
|
||||
- Без рефакторинга: тесты медленные (~5-19s)
|
||||
- С рефакторингом: поле `tickerInterval` — но ⛔ запрещает менять частоту опроса
|
||||
|
||||
## Q3. Перехват tty
|
||||
- Без рефакторинга: вывод в stderr, не ломает тесты
|
||||
- С рефакторингом: поле `ttyWriter io.Writer`
|
||||
|
||||
## Q4. Тест-кейсы (5 шт)
|
||||
1. stages по одному — `atomic.Int32` счётчик
|
||||
2. все сразу
|
||||
3. фейл — `IsSuccessful=false`, `ErrorLog="disk error"`
|
||||
4. **503 (не 500!)** — 500 не retryable. 503 — retryable. Тест ~19s без рефакторинга
|
||||
5. таймаут — `timeout=6s`
|
||||
|
||||
## Q5. Рефакторинг
|
||||
Без рефакторинга — всё тестируемо, но медленно.
|
||||
С рефакторингом — 6 строк, не в protected zone, но `tickerInterval` требует согласования (⛔).
|
||||
|
||||
## Итог
|
||||
| Тест | Без рефакторинга | С рефакторингом |
|
||||
|---|---|---|
|
||||
| sequential | 15s | 150ms |
|
||||
| all_at_once | 5s | 50ms |
|
||||
| fail | 5s | 50ms |
|
||||
| 503_retry | 19s | быстро |
|
||||
| timeout | 5s | 100ms |
|
||||
| tty capture | ❌ | ✅ |
|
||||
@@ -0,0 +1,53 @@
|
||||
# Sonnet: тестирование stages без реальных инстансов
|
||||
|
||||
Реализовали вывод этапов в `waitForOperationFinish()`. Код в репе.
|
||||
Нужно протестировать БЕЗ создания реальных инстансов в облаке.
|
||||
|
||||
---
|
||||
|
||||
## ⛔ Файлы — ПРОЧИТАТЬ ОБЯЗАТЕЛЬНО (иначе ответ будет неполным)
|
||||
|
||||
| # | Файл | Строки | Что искать |
|
||||
|---|------|--------|------------|
|
||||
| 1 | `provider/internal/core/client.go` | 29–55 | `UniversalClient` struct: поля `HttpClient`, `ApiEndpoint`, `ApiToken` |
|
||||
| 2 | `provider/internal/core/client.go` | 895–1010 | `waitForOperationFinish()` — ВСЯ функция от сигнатуры до закрывающей `}` |
|
||||
| 3 | `provider/internal/core/client.go` | 895–910 | ⛔ «ЗАПРЕЩЕНО ПРАВИТЬ ЭТОТ КОД БЕЗ ЯВНОГО СОГЛАСОВАНИЯ» |
|
||||
| 4 | `provider/internal/core/client.go` | 900–930 | `ttyOut()`, `opStage` struct, `operationStatusResponse` struct |
|
||||
| 5 | `provider/internal/core/client.go` | 1030–1140 | `doRequest()` — retry-логика, паузы **2s → 4s → 8s**, `maxRetries=3` |
|
||||
| 6 | `provider/internal/core/client_test.go` | весь файл | как создаётся `UniversalClient`, структура существующих тестов |
|
||||
|
||||
---
|
||||
|
||||
## Вопросы
|
||||
|
||||
### 1. Мок API
|
||||
Достаточно ли `httptest.NewServer` ТОЛЬКО для `/instanceOperations/{uid}?fields=...`?
|
||||
Или нужно мокать всю цепочку create → params → run → poll?
|
||||
|
||||
### 2. Время
|
||||
`time.NewTicker(5*time.Second)`, таймаут 30 мин. Как ускорить тест?
|
||||
Dependency injection? Mock `time.Now`?
|
||||
|
||||
### 3. Перехват tty
|
||||
`ttyOut()` → `/dev/tty` с fallback на `os.Stderr`. Как перехватить вывод?
|
||||
|
||||
### 4. Тест-кейсы
|
||||
Какие тесты написать:
|
||||
- stages по одному (прогресс)
|
||||
- stages все сразу
|
||||
- фейл на этапе N
|
||||
- API 500 → retry (**учти задержки в `doRequest`: 2s → 4s → 8s**)
|
||||
- таймаут операции
|
||||
|
||||
### 5. Рефакторинг
|
||||
Стоит ли рефакторить `waitForOperationFinish`?
|
||||
**Учти ⛔ запрет на правку (стр. 895–910).**
|
||||
|
||||
---
|
||||
|
||||
## Ожидаемый ответ
|
||||
|
||||
1. Конкретный ответ на каждый вопрос
|
||||
2. Unified diff минимальных изменений (если нужен рефакторинг)
|
||||
3. Псевдокод каждого тест-кейса
|
||||
4. Оценка: что можно без рефакторинга, что потребует правок
|
||||
Reference in New Issue
Block a user