fix(core): idempotency pre-check без схемы — единый источник (live)

Код-ревью (раунд 5), п.1/6: pre-check брал схему из GET /instanceOperations/default/{opId},
а payload строился по живой схеме ?fields=cfsParams — два источника. default может
расходиться с живой => риск ложного пропуска modify.

Решение: pre-check вообще не запрашивает схему.
- modifierDesiredEqualsLive(ctx, uid, desired): сравнение по live-кодам
  (live[lower(code)]), единственный источник — state.params.
- Значения: похожи на JSON ({/[) — смысловое сравнение; иначе скалярное с
  нормализацией (null/"" -> ""; true/false без учёта регистра) — закрывает и
  регистр bool.
- Fail-safe сохранён: пусто/нет кода/ошибка live => modify выполняется.
- Удалены: fetchOperationSchemaByID, modifierValuesEqual, lookupLiveParam больше
  не участвует в pre-check (остаётся для досылки).
- Тесты переписаны (RawValuesEqual_Scalars/JSON, DesiredEqualsLive: 4 кейса).

Документация: TOOLS/ARCHITECTURE.md -> новый раздел «Modifier Idempotency»
(5 правил контракта); HISTORY — журнал раунда 5.

Проверено: go build ./... OK; go test ./internal/... -short PASS.
This commit is contained in:
Repinoid
2026-09-30 21:07:49 +03:00
parent 4197a76aba
commit 99f963484b
7 changed files with 131 additions and 87 deletions
+19
View File
@@ -134,6 +134,25 @@ Each operation has a kind:
provider code nor the captured HAR contain `DELETE /instanceOperations/{uid}`.
- Transient 401 is retried for GET (see `isRetryable`).
### Modifier Idempotency (decided 2026-09-30)
Modifiers may opt into an idempotency pre-check (`RunInstanceOperationUniversalByIdempotent`,
used by `nubes_vc_nsxt_snat`). Rules:
- The pre-check runs **before** `POST /instanceOperations`. Otherwise a skipped modify
leaves a created-but-never-run operation ("draft", pending) and the next call waits
for idle until timeout.
- The **only** comparison source is the live instance state (`state.params` via
`instanceLiveParams`). The operation schema (`/instanceOperations/default/{opId}`,
`?fields=cfsParams`) is **deliberately not used**: before the operation exists only the
`default` schema is available, and it may diverge from the live one — comparing through
it risks a false skip. (Using the live schema would require creating the operation
first — which is exactly the regression above.)
- Values are matched by the code itself (`live[lower(code)]`).
- Fail-safe: empty live, code absent, or live read error → the modify runs (never skipped).
- JSON-looking values (`{`/`[`) are compared semantically (key order ignored); scalars are
normalized (`null`/`""` → empty; `true`/`false` case-insensitive).
### Generated Code Resilience
- **Partial state при ошибке create** (шаблон `instance.go`): если инстанс успел создаться