diff --git a/TEST-SCENARIOS.md b/TEST-SCENARIOS.md index a1a8519..ea19df1 100644 --- a/TEST-SCENARIOS.md +++ b/TEST-SCENARIOS.md @@ -18,7 +18,7 @@ geçerliliğini yitiren senaryo **silinmez**, `deprecated` işaretlenip sebebi y | **instance-queries** | Instance data persistence → parameterized JSONB filtering; GraphQL/legacy value limits; HTTP SQL pagination, requested ordering, ID tie-breaks, hydration and HATEOAS next links | vnext #934 / #933, PR #987 (2026-09-15/16) | Locally built runtime + isolated PostgreSQL/OpenObserve | [test_instance_queries.py](api-tests/instance-queries/test_instance_queries.py), [pagination_checks.py](api-tests/instance-queries/pagination_checks.py), [README](api-tests/instance-queries/README.md) | Value-limit **25/25**; pagination **611/611**, 192/192 traces on runtime `549722fe`. Baseline four legacy-newline failures and envelope-format limits remain documented; no index benchmark claimed | | **attribute-index-preparation** | Publish → no automatic jobs/DDL → offline CLI SQL → explicit DBA execution; replay; Master update | Manual DBA-owned index maintenance | Real locally built Orchestration + isolated PostgreSQL | [test_manual_indexes.py](api-tests/attribute-index-preparation/test_manual_indexes.py) | PASS: 4 checks (2026-09-14) | | **attribute-index-preparation (historical)** | Former directly:true automatic job experiment | Superseded by manual DBA decision | Historical evidence only | [README](api-tests/attribute-index-preparation/README.md) | DEPRECATED: previous 6 checks / 13 jobs remain recorded; automatic behavior removed 2026-09-14 | -| **timeout-lab** | Workflow seviyesi `timeout`: state function gövdesindeki **`timeout` bloğu** (`{ key, target, executeAtUtc }`, `transitions[]` girdisi DEĞİL) · bloğun **etkin** timeout'tan beslenmesi (`subFlow.overrides.timeout ?? workflow.timeout`, tek resolver) · `executeAtUtc`'nin kalıcı `InstanceJob.ExecuteAt`'ten okunması, arm anındaki instant · terminal instance'ta bloğun **asenkron cancel-cleanup zincirini beklemeden** düşmesi · bloğun yalnız poll edilen instance'ı anlatması (aktif subflow'a inmemek, parent'ın child'ın deadline'ını devralmaması) · `ResponseShapeVersion` v9→v10 · **ve fire yolunun override'ı onurlandırması**: child kendi tanımında `"timeout": null` taşırken parent'ın override'ının hedefine çekilmesi | İki ayrı kusur, tek fixture. (1) vnext-client-sdk-core#59: instant zaten job satırında duruyordu ama hiçbir okuma yüzeyi taşımıyordu, client geri sayım çizemiyordu. (2) Council `2026-09-21-state-timeout-surfacing` FACT-A: `subFlow.overrides.timeout` **yalnız arm anında** okunuyordu — job zamanında ateşleniyor, handler child'ın KENDİ tanımını okuyup `TimeoutConfigMissing` ile dönüyordu; yani zamanlanmış ama asla varamayan bir deadline. `subflow-orchestration` tam bu şekli taşıyor ve hiçbir test dokunmuyordu. Ayrıca repoda workflow timeout'unu sınayan **hiçbir** senaryo yoktu ve yazılı iki süre de `PT15M` — bir test koşusunda izlenemez (2026-09-21) | `Tests/TimeoutLab` (1 sınıf, 3 test) + `Tests/TimeoutLab/README.md` | — (bilinçli: doğruluk/regresyon senaryosu, gecikme iddiası yok) | ✅ **Aktif — 3/3 yeşil** (lokal runtime, 2026-09-21). **İlk koşuda kendini amorti etti:** senaryo kırmızı düştü ve konseyin kod okumasının kaçırdığı **dördüncü** bir `workflow.Timeout` okuyucusunu açığa çıkardı — `CreateTransitionRecordStep` `$timeout`'u **order 20**'de `Workflow.ResolveWellKnownKey` ile çözüyor ve override'lı child için `TimeoutNotConfiguredForWorkflowException` **fırlatıyor**, yani pipeline `ApplyTimeoutStateStep` (38) düzeltmeye gelemeden ölüyordu. `SetBusy` (19) çoktan commit ettiği için ölçülen belirti "timeout ateşlenmiyor" değil, child'ın **bekleme state'inde sonsuza dek Busy kalması** oldu (job satırı processed işaretli; `e48ac9e2…` tezgâhta 'öncesi' fotoğrafı olarak duruyor). Kanıt test özetinden değil postgres+loglardan: child `child-timedout`/`C`, audit anahtarı **`child-abandoned`** (PARENT'ın override key'i), armlanan `ExecuteAt`'ten ~54 ms sonra; root `root-timedout`/`C` via `root-abandoned`; logda sıfır `TimeoutConfigMissing`. Komşu senaryolar regresyonsuz: `SubflowOrchestration`+`ChainBusy`+`ScheduleAfterAuto` 39/39 | +| **timeout-lab** | Workflow seviyesi `timeout`: state function gövdesindeki **`timeout` bloğu** (`{ key, target, executeAtUtc, annotations }`, `transitions[]` girdisi DEĞİL) · bloğun **etkin** timeout'tan beslenmesi (`subFlow.overrides.timeout ?? workflow.timeout`, tek resolver) · `executeAtUtc`'nin kalıcı `InstanceJob.ExecuteAt`'ten okunması, arm anındaki instant · terminal instance'ta bloğun **asenkron cancel-cleanup zincirini beklemeden** düşmesi · bloğun yalnız poll edilen instance'ı anlatması (aktif subflow'a inmemek, parent'ın child'ın deadline'ını devralmaması) · `ResponseShapeVersion` v9→v10 · **ve fire yolunun override'ı onurlandırması**: child kendi tanımında `"timeout": null` taşırken parent'ın override'ının hedefine çekilmesi | İki ayrı kusur, tek fixture. (1) vnext-client-sdk-core#59: instant zaten job satırında duruyordu ama hiçbir okuma yüzeyi taşımıyordu, client geri sayım çizemiyordu. (2) Council `2026-09-21-state-timeout-surfacing` FACT-A: `subFlow.overrides.timeout` **yalnız arm anında** okunuyordu — job zamanında ateşleniyor, handler child'ın KENDİ tanımını okuyup `TimeoutConfigMissing` ile dönüyordu; yani zamanlanmış ama asla varamayan bir deadline. `subflow-orchestration` tam bu şekli taşıyor ve hiçbir test dokunmuyordu. Ayrıca repoda workflow timeout'unu sınayan **hiçbir** senaryo yoktu ve yazılı iki süre de `PT15M` — bir test koşusunda izlenemez (2026-09-21) | `Tests/TimeoutLab` (1 sınıf, 3 test) + `Tests/TimeoutLab/README.md` | — (bilinçli: doğruluk/regresyon senaryosu, gecikme iddiası yok) | ✅ **Aktif — 3/3 yeşil** (lokal runtime, 2026-09-21). **İlk koşuda kendini amorti etti:** senaryo kırmızı düştü ve konseyin kod okumasının kaçırdığı **dördüncü** bir `workflow.Timeout` okuyucusunu açığa çıkardı — `CreateTransitionRecordStep` `$timeout`'u **order 20**'de `Workflow.ResolveWellKnownKey` ile çözüyor ve override'lı child için `TimeoutNotConfiguredForWorkflowException` **fırlatıyor**, yani pipeline `ApplyTimeoutStateStep` (38) düzeltmeye gelemeden ölüyordu. `SetBusy` (19) çoktan commit ettiği için ölçülen belirti "timeout ateşlenmiyor" değil, child'ın **bekleme state'inde sonsuza dek Busy kalması** oldu (job satırı processed işaretli; `e48ac9e2…` tezgâhta 'öncesi' fotoğrafı olarak duruyor). Kanıt test özetinden değil postgres+loglardan: child `child-timedout`/`C`, audit anahtarı **`child-abandoned`** (PARENT'ın override key'i), armlanan `ExecuteAt`'ten ~54 ms sonra; root `root-timedout`/`C` via `root-abandoned`; logda sıfır `TimeoutConfigMissing`. Komşu senaryolar regresyonsuz: `SubflowOrchestration`+`ChainBusy`+`ScheduleAfterAuto` 39/39. **Annotations (2026-09-24, akışlar 1.0.1, 4/4 yeşil):** gövdenin listelediği her tür — state, shared, `cancel`, `exit`, `updateData` — ve `timeout` bloğu tanımdaki `annotations`'ı taşır; child'ın bloğu parent override'ının annotations'ını taşır (override kaydıyla gider, birleştirilmez). Komşular aynı runtime'da: `SubflowOrchestration`+`ChainBusy` 37/37 | | **human-task-chain** | `human-task` domain function: aday seçiminin instance'ın KENDİ kolonlarıyla yapılması (`Type IN ('R','P')`, `Status IN ('A','B') AND EffectiveStatus='A'`, `EffectiveStateSubType=6`) · **leaf descent**: seviye-bazında batch, `(domain, flow)` gruplaması, domain sınırı başına tek çağrı · yetkilendirmenin ve `humanTask` metninin **leaf'ten** gelmesi, satır kimliğinin **root'ta** kalması · **SubProcess kimliği**: `P` bağımsız bir akıştır, kendi `Id`'siyle ve **kendi domain'inin** cevabında listelenir, kendi subflow'larına root gibi iner · **yetkilendirme**: kararın leaf state'inin **queryRoles**'undan gelmesi (transition'lar düşürüldü), queryRoles tanımlı değilse **fail-closed** düşme, parent'ın `state_role_overrides`/`transition` override'larının canlı olması (aynı root flow'un iki instance'ı, sadece nerede dinlendikleriyle ayrışıyor), DENY'ın **yalnız başka izinli rol yokken** reddetmesi (`IsAnyRoleAllowed` ilk izinli rolde döner), response cache'inin bir caller'ın listesini diğerine servis etmemesi, SubProcess'in **kendi** leaf'iyle yetkilenmesi · **caller context**: `X-VNext-Cache-Override` ve korelasyon header'larının her domaine geçmesi, truncation'ın agregata yansıması · leaf'in kendi stamped `subflow.transition_role_overrides` haritasından rol çözümleme · `Effective*` invariant'ı: alt akış bitince dörtlünün (state/type/subType/status) parent'ın kendi state'ine dönmesi · projeksiyonun long-poll fingerprint materyali olması | vnext `feature/human-task-function-redesign`: bu uç hiç test edilmemişti ve dört kusur taşıyordu — `EffectiveStateSubType`'ın reset writer'ı yoktu (hayalet human task, herkese, kalıcı), mevcut reset `!HasActiveSubFlow` guard'ı taşımıyordu, descent tek seviye iniyor ve çocuğun tanımını **caller'ın** domain'inde arıyordu (cross-domain çocuk instance'ı listeden tamamen düşürüyordu), iptal edilmiş seviye canlı çocuğun `'A'`'sını ham kolonda tutuyordu (2026-09-17) | `Tests/HumanTaskChain` (2 sınıf, 20 test) | `api-tests/human-task-chain/morph-idm-aggregation-test.py` (20 kontrol, **20/20 geçti** 2026-09-18) + `morph-idm-aggregation.http` (elle sürmek için adım adım) | ✅ **Aktif — 20/20 yeşil** (+ morph-idm uçtan uca 30/30) (dört-domain cross-domain lab, art arda iki koşu, 2026-09-17). Senaryo 3 için lab **dördüncü domain** `credit` (offset 20 → :4221) ile genişletildi; `lab.sh verify` artık ikinci sınırı da (`partner → credit`) doğruluyor. Koşu üç runtime kusuru buldu: fan-out'un içine yerleştirilen özyinelemeli descent aynı DbContext'i paylaşıp 500 veriyordu — DbContext DI scope'una değil **UnitOfWork**'e bağlı ve şema ile anahtarlanıyor, paralel dallar da ambient UoW'yi miras alıyor; her dal artık kendi UoW'sini açıyor (`ExecuteInIsolatedUnitOfWorkAsync`) ve descent paralel kaldı (80 eşzamanlı tam fan-out: 80×200, sıfır istisna), her otomatik geçiş `triggerKind: 10` (DefaultAutoTransition) olmadan rule istiyor, ve rolsüz `cancel` her caller'ı yetkilendirdiği için negatif rol testi anlamsız kalıyordu. SubProcess turu dördüncü bir bulgu ortaya çıkardı, ama bu bir runtime kusuru değil çıktı: state seviyesindeki `subFlow.type: "P"` şekli **geçersiz authoring** olarak yeniden sınıflandırıldı, artık publish anında reddediliyor, ve `ht-a` bir `SubProcessTask`'a dönüştürüldü (bkz. Bilinen Kapsam Açıkları altındaki güncellenmiş satır) | | **error-boundary-lab** | Error boundary çözümlemesi: `CompiledBoundaryChain` Task→State→Global seviye baskınlığı · seviye içi sıra (`EffectivePriority` ASC → specificity DESC; default wildcard'ın 999'a düşmesi) · aksiyonlar abort/retry/rollback/notify/ignore/log · `TaskExecutionEngine` retry döngüsü (`1 + maxRetries`) ve tükenince `ResolveExcluding(Retry)` fallback'i · `BoundaryOutcomeHandler` → fault vs `RequestNextTransition` · `FinalizeTransitionStep`'in boundary transition'ı bitince incident'ı resolve etmesi · `InstanceIncidents` tablosu + denormalize `HasActiveIncident` · state function `incident` bloğu (link tabanlı, ResponseShapeVersion v9, ETag materyali) · `GET .../instances/{id}/incidents/active` (404 = açık arıza yok) · `GET .../instances/{id}/incidents` sayfalama · `metadata.incident` (state bloğuyla aynı şekil) · `POST .../retry` (400 `Instance:100027`, yeniden fault, veriyle kurtarma) · `queryRoles` kapısının incident geçmişine de uygulanması | vnext `feature/incident-table` (issue #865) incident'ları jsonb'den kendi tablosuna taşıdı ve iki yeni client yüzeyi ekledi; ayrıca error boundary bu repoda hiç senaryo olarak yoktu ve `POST .../retry` hiçbir testte çağrılmıyordu (2026-09-06) | `Tests/ErrorBoundaryLab` (6 sınıf, 31 test) | — (bilinçli: davranış/çözümleme senaryosu, eşzamanlılık iddiası yok) | ✅ **Aktif — 31/31 yeşil** (lokal runtime `feature/incident-table`, art arda iki koşu, ~1 dk 46 sn; ilk koşu 2026-09-06, üç davranışsal kusurun düzeltilmesinden sonra 2026-09-07'de yeniden) | | **event-driven-lab** | Olay tetiklemeli start + `triggerType: 3` transition · Dapr Subscription route'u · CloudEvent açma · `IEventMapping` ile iş anahtarı korelasyonu · eşleşmeyen olayın ack'lenmesi | `POST .../instances/events` ucunun **hiç testi yoktu** (council `2026-09-11-route-trace-coverage`, adı konmuş boşluk); ayrıca `Event.Intake` span'i bu ucu ölçüyor ve testsiz route'a span gönderilmez (2026-09-13) | `Tests/EventDrivenLab` (1 sınıf) | — | ✅ Aktif | @@ -43,7 +43,7 @@ geçerliliğini yitiren senaryo **silinmez**, `deprecated` işaretlenip sebebi y | **fan-out-config-matrix** | `FanOutTask` (TaskType 21) **konfigüre edilebilir yüzeyi**: dört `join.policy` (`all` / `allSettled` / `quorum` / `firstSuccess`) verdict'lerinin **iki** yanında da · `join.minSuccess` tutan / tutmayan · `FanOutJoinEvaluator` **boş-batch** kuralı (`all`+`allSettled` vacuously başarılı, `quorum`+`firstSuccess` eşiği geçemediği için başarısız) · `mode: "durable"` reddi (`FanOutTask.Configure`, rezerve) · **item bazlı `errorBoundary`** (`ignore` verdict'i çevirir: `join: all` altında başarısız item batch'i düşürmez; `retry` tükenmesi kendi item'ında kalır) · `execution.maxDegreeOfParallelism`'in gerçek eşzamanlılığı sıkıştırması (eşleştirilmiş kontrol kolu, tek fark tavan) · `itemTimeoutSeconds` ↔ `batchTimeoutSeconds` ayrımı: `FanOut:ItemTimeout` vs `FanOut:BatchTimeout` + `summary.timedOut`'un yalnız **batch** deadline'ında yükselmesi · başarısız join'in task'ı düşürüp instance'ı Faulted etmesi (akışta bilinçli olarak **hiç** errorBoundary yok) | FanOutTask'ın config yüzeyi uçtan uca **hiç** doğrulanmamıştı — unit testler geçiyor ve bir production domain'i yalnız mutlu yolu kullanıyordu; `join.policy` değişince, eşik tutmayınca, koleksiyon boş gelince, item boundary devredeyken ya da tavan gerçekten sıkıştırınca runtime'ın ne yaptığını hiçbir integration test görmüyordu (vnext `feature/fanout-task-design`, 2026-08-21) | `Tests/FanOut` (`FanOutConfigMatrixTests`, 16 test) | — (bilinçli: doğruluk/konfigürasyon senaryosu; eşzamanlılık iddiaları hata kodu + sayı üzerinden, duvar saati yok) | ✅ **Aktif — 18/18 yeşil** (2026-08-22, lokal runtime `ad72158b`, iki koşu üst üste; `--filter FanOut` bütünü 22/22). Dört join politikası verdict'lerinin iki yanında da, `minSuccess`, boş-batch kuralı, `itemTimeoutSeconds`, `batchTimeoutSeconds`, eşit-deadline sınırı, `maxDegreeOfParallelism`'in gerçek eşzamanlılığı sıkıştırması, item bazlı `retry` kapsaması ve `mode: durable` reddi **uçtan uca doğrulandı**. Bulunan ve **runtime'da düzeltilen** iki defect: **F1** (`b80be176`) uçuşta iptal edilen item kendi fan-out nedeni yerine `Task:Unknown::TaskCanceledException` taşıyordu — üç iptal nedeninde de (item deadline 0/1→**1/1**, batch deadline 1/2→**2/2**, early-stop 1/4→**4/4**); **F2** (`ad72158b`) `Configure`-time authoring hatası opak `500` yerine artık alanı adlandıran `400` (paylaşılan `ComponentValidatorProcessor`'da olduğu için tüm task tiplerini kapsıyor). Kendi filed ettiğim **"timedOut yükselticisi" iddiası ölçülüp GERİ ÇEKİLDİ** — `itemTO <= batchTO` zorunlu + `Classify` önce item deadline'ına baktığı için o şekil yapısal olarak imkânsız. Üç **fixture/tasarım** düzeltmesi: boundary yokken başarısız onEntry task'ı fault'lamıyor (fault temelli gözlem beş case'i sessizce geçiriyordu → global `rollback` + `case-failed`); **MockLab PREFIX eşlemesi** yavaş mock'u yutuyordu (`api/fan-out/slow-documents/process`'e taşındı); yük testinde **BULKHEAD metriği doygunlukta geçersiz** (`durationMs` kuyruk süresini içeriyor) ve **straggler eşiği bozuk fixture'a kalibreliydi**. Açık: **F3** item bazlı `ignore` semantiği, **C1** `minSuccess` non-quorum'da sessiz yoksayma (ikisi de karar bekliyor, test kırmızısı değil). Kanıt: [`docs/fanout-configurable-surface-findings.md`](docs/fanout-configurable-surface-findings.md). `npm run validate` 10 fan-out task bileşenini de reddediyor — `fan-out-documents` ile aynı bilinen şema açığı (enum `"20"`de bitiyor); publish şema validasyonunu baypas ettiği için engel değil | | **payload-modes** | Request sözleşmesi ↔ şema doğrulaması: payload-mode tespiti (`PayloadModeDetector` + `FormUrlEncodedJsonElementInputFormatter`) · standart zarf (`key`/`tags`/`stage`/`attributes`) ↔ serbest payload ayrımı · `startTransition.schema` **ve** `transition.schema` yollarının ikisi birden · zarf alanlarının iş verisine sızmaması (şemasız transition'da sessiz veri kirlenmesi) · `x-vnext-payload-mode: raw` override'ı · `attributes` eşleşmesinin case-insensitive olması | Şema tanımlı bir transition/start'ta payload'ın **hangi biçimde** gönderildiği validasyon sonucunu değiştiriyordu: mod tespiti tek bir case-sensitive `attributes` property'sine bakıyordu, oysa zarfın her alanı opsiyoneldir — `attributes` içermeyen geçerli bir zarf serbest payload sanılıp **tümüyle** `attributes` altına sarılıyor ve şema iş payload'ı yerine `key`/`tags` alanlarını doğruluyordu (`additionalProperties: false` şemalarda *"All values fail against the false schema"* 400'ü). Şemasız transition'da aynı hata sessizdi: zarf instance data'ya yazılıyordu (2026-08-22, vnext `PayloadEnvelope` ortak zarf sözlüğü) | `Tests/PayloadModes` (2 sınıf, 24 test) | — (bilinçli: request sözleşmesi doğruluk senaryosu, eşzamanlılık iddiası yok) | ✅ **Aktif — 24/24 yeşil** (2026-08-22, lokal runtime). Düzeltme öncesi runtime'a karşı **tam 4 test kırmızı** (start: envelope-only + PascalCase `Attributes`; transition: envelope-only + şemasız transition'da `key`'in instance data'ya yazılması) — regresyon iğnesi doğrulandı. Kullanıcının bildirdiği üç kanonik biçim (`{key,attributes}`, `{attributes}`, serbest gövde) düzeltme öncesinde de geçiyordu; testler "üçü de aynı sonucu üretir" sözleşmesini sabitler. Akış **hiç task içermez** — MockLab/execution host/worker bağımlılığı yok. **Aynı geliştirme altında ikinci bir defect düzeltildi:** kök düzeyindeki `required` hatası hiyerarşik ağaç düzleştirilirken düşüyordu (`JsonSchemaValidationMapper.FlattenErrors` bir düğümün *kendi* hatalarını, çocukları varsa atıyordu — `additionalProperties:false` + iç içe obje olan her şemada kök hatası TEK hataydı), istemci `"errors":{}` ile **hangi alanın hatalı olduğunu öğrenemiyordu**; boş hata listesi ayrıca yanıtı RFC7807 ProblemDetails'e düşürerek iki farklı gövde biçimi yaratıyordu. Artık tek biçim + alan düzeyinde `members`/`message` | | **script-perf-lab** | Script compile cache hit yolu (`CSharpEvaluator._typeCache`) · `scripts.helpers` çok üyeli helper set (A7) · instance-data append zinciri (`JsonData.Merge`/`NormalizedJson`, B9 O(n²) profili) · `FanOutTask` inline branch klonu (`CreateParallelBranch`, B6) · Katman 0 metrikleri (`script_compilations_total{result}`, `script_execution_duration_seconds{script_type}`) | Katman 0 ölçüm altyapısının makro baseline'ı — Katman 1-3 compiler/serialization optimizasyonlarının gerçek-yük önce/sonra referansı (2026-08-23, vnext `feature/script-perf-katman0`) | `Tests/ScriptPerfLab` (1 test) | `api-tests/script-perf-lab/perf-load.py` (soğuk/sıcak faz + p50/p95/p99 + /metrics snapshot) | ✅ Aktif — K1+K2 önce/sonra kayıtlı; COW+canonicalizer 37/37 integration + kill-switch canlı testli (2026-08-23) | -| **schedule-after-auto** | Pipeline epilogue sıralaması (`LifecycleOrder.Auto` 80 → `LifecycleOrder.Schedule` 90) · `ScheduleTransitionsStep`'in `Directives.NextTransition` guard'ı (auto kazanan varsa **hiç** arm etmez: Dapr job yok, `InstanceJob` satırı yok) · state function'ın `kind: "scheduled"` girdileri + `executeAtUtc` · auto kazanan yokken scheduled transition'ın eskisi gibi arm edilip **gerçekten ateşlenmesi** · `CancelScheduledJobsStep` (39) churn'ünün ortadan kalkması | Eski sıralamada Schedule (80) timer'ı arm ediyor, Auto (90) kazanan seçiyor, zincirlenen hop da o timer'ı hemen siliyordu — auto'nun kazandığı her hop'ta boşuna enqueue + persist + cancel. Sıralama takas edildi (vnext `feature/schedule-after-auto`, plan `docs/superpowers/plans/2026-09-02-schedule-after-auto.md`, 2026-09-03). Senaryo hem yeni davranışı hem de "auto kazanmazsa hiçbir şey değişmedi" tarafını pinler; iki sıralama **dinlenme durumunda ayırt edilemediği** için auto hop'unun `onExecute`'u bilinçli ~2.5 sn gecikir ve test o pencerede armed girdinin **hiç** oluşmadığını gözler | `Tests/ScheduleAfterAuto` (`ScheduleAfterAutoTests`, 2 test) | — (bilinçli: sıralama/doğruluk senaryosu; eşzamanlılık iddiası yok) | ✅ **Aktif — 2/2 yeşil** (2026-09-03, lokal runtime `702a03b6`, iki koşu üst üste, ~24 sn) | +| **schedule-after-auto** | Pipeline epilogue sıralaması (`LifecycleOrder.Auto` 80 → `LifecycleOrder.Schedule` 90) · `ScheduleTransitionsStep`'in `Directives.NextTransition` guard'ı (auto kazanan varsa **hiç** arm etmez: Dapr job yok, `InstanceJob` satırı yok) · state function'ın `kind: "scheduled"` girdileri + `executeAtUtc` · auto kazanan yokken scheduled transition'ın eskisi gibi arm edilip **gerçekten ateşlenmesi** · `CancelScheduledJobsStep` (39) churn'ünün ortadan kalkması | Eski sıralamada Schedule (80) timer'ı arm ediyor, Auto (90) kazanan seçiyor, zincirlenen hop da o timer'ı hemen siliyordu — auto'nun kazandığı her hop'ta boşuna enqueue + persist + cancel. Sıralama takas edildi (vnext `feature/schedule-after-auto`, plan `docs/superpowers/plans/2026-09-02-schedule-after-auto.md`, 2026-09-03). Senaryo hem yeni davranışı hem de "auto kazanmazsa hiçbir şey değişmedi" tarafını pinler; iki sıralama **dinlenme durumunda ayırt edilemediği** için auto hop'unun `onExecute`'u bilinçli ~2.5 sn gecikir ve test o pencerede armed girdinin **hiç** oluşmadığını gözler | `Tests/ScheduleAfterAuto` (`ScheduleAfterAutoTests`, 2 test) | — (bilinçli: sıralama/doğruluk senaryosu; eşzamanlılık iddiası yok) | ✅ **Aktif — 2/2 yeşil** (2026-09-03, lokal runtime `702a03b6`, iki koşu üst üste, ~24 sn). **2026-09-24 (akış 1.0.1):** `kind: "scheduled"` girdisinin transition tanımındaki `annotations`'ı taşıması da pinlendi (önceden düşürülüyordu) — 2/2 yeşil | | **cross-domain-lab** | Cross-domain transport: `ServiceDiscovery:Provider=dapr` (`DaprDomainDiscoveryProvider`, registry `appId` override → `vnext-app-partner`) · Dapr service invocation shell (`DaprRemoteTransport`) · cross-domain **SubFlow** start / `internal/subflow-forward` / parent resume (`ResumePipelineAsync`) · trigger task'ları **11 Start · 12 DirectTrigger · 13 GetInstanceData · 14 SubProcess · 15 GetInstances (`SetFilterSpec`) · 19 GetInstance** (`useDapr:true`, `config.domain:"partner"`) · fonksiyon descent'i `state` / `view` / `schema` / `authorize` (partner rol filtresi) / `data?extensions=` (`RemoteInstanceQueryAppService`) · `data` gövdesinin parent'ta kalması (pinlenmiş runtime kararı) · **discovery endpoint cache warm-up**: registry'nin Domain-scope `domain-list` function'ından tek okuma (`DiscoveryRegistryClient.ListAllAsync` → `DiscoveryCacheRefresher` → L1/L2), `POST utilities/discovery/refresh` | Cross-domain adres çözümlemesi Discovery HTTP'sinden Dapr Name Resolution'a taşındı (vnext `feature/dapr-name-resolution`, 2026-09-03); repoda hiç cross-domain örnek yoktu. İkinci domain (`partner/`, `vnext.partner.config.json`) ve üç-domain lokal lab (`labs/cross-domain/`) bu senaryoyla geldi. Plan: `labs/cross-domain/VNEXT-BUILD-PLAN.md` | `Tests/CrossDomainLab` (`SubflowDescentTests` 6, `TriggerTaskTests` 5 — `VNEXT_PARTNER_BASE_URL` yoksa **skip**; `DiscoveryWarmUpTests` 3 — `VNEXT_DISCOVERY_BASE_URL` yoksa veya cache kapalıysa **skip**) | — (yük testi sonraki faz) | ✅ **Aktif — 11/11 yeşil** (2026-09-03, lab: üç domain de lokal `dapr-nr` imajları + Dapr 1.18.0, ~1.7 dk). Rollback tatbikatı `VNEXT_LAB_DISCOVERY_PROVIDER=http` ile de 11/11 (2026-09-04; `Remote*` düz HTTP `vnext-app-partner:5000`, `useDapr` task'ları Dapr'da). **Discovery endpoint cache doğrulaması (2026-09-09, `http` provider, 11/11 yeşil):** core tek bulk okumayla 3 domain'i cache'e yazdı (`50002`), partner marker'ı görüp **hiç** bulk okuma yapmadı ama 13 çözümlemesinin 13'ünü paylaşılan cache'ten aldı — kümede pencere başına tek okuma. Elastic APM `Discovery.Resolve/*`: 79 span, cache 78 / registry 1; L2'den bir kayıt silinip L1 süresi beklendiğinde aynı domain **registry 100.7 ms → cache 0.07-0.6 ms**. `POST utilities/discovery/refresh` açık pencerenin içinde senkron yeniden okudu (`outcome: Refreshed`). Not: lab template'inin `AdditionalSources`'ında `BBT.Workflow.Pipeline` yok — eklenmezse `Discovery.Resolve` span'i sessizce düşer; template ayrıca yalnız OpenObserve'e export eder, Elastic için collector'a `otlp/elastic` + elasticsearch/apm-server gerekir. Bilinen: vnext-schema 0.0.52 `useDapr`'ı yalnız task 15/19'da tanır → `core/Tasks/cross-domain-lab/` 11/12/13/14 dosyaları `npm run validate`'te "then schema" hatası verir (runtime alanı okur, alan bilinçli korunuyor); `partner/` validate kapsamı dışında | **domain-list warm-up (2026-09-17, `DiscoveryWarmUpTests` 3/3 yeşil):** runtime'ın bulk okuması sayfalı instance listesinden registry'nin `domain-list` function'ına taşındı (vnext `feature/discovery-domain-list-bulk-read`, geri dönüş yolu yok). Lokal iki-domain kurulumu (core :4201 + discovery :4221, `run-docker.sh up`, lokal binary'ler, registry paketi `@burgan-tech/vnext-discovery-runtime@0.0.7`): giden istek tam olarak `GET /api/v1/discovery/functions/domain-list` — `page=`/`filter=` **yok** — ve `Refreshed` ile 11 domain Redis'e yazıldı (`vnext||discovery:domain:v1:*` + `discovery:bulk:v1:refreshed-at`). Negatif: registry süreci öldürülünce refresh `Failed` döndü ve 12 kayıt **silinmeden** kaldı (fail-open). Ortam kusuru: `Kestrel:GrpcPort` appsettings'te 4212'ye sabit ve offset'lenmiyor → ikinci domain'in Execution host'u core'unkiyle çakışır, `Kestrel__GrpcPort` elle verilmeli (offset 10 ayrıca kendi HTTP portuyla çakışır). | **task-invocation-lab** | Task invocation routing (`Workflow:TaskInvocation:Modes`/`DefaultMode`, `ITaskInvocationRouter.Resolve`'ın per-type host config → configured default → local-invoker capability gate sırası) · beş "wire" task tipinin Orchestration üzerinde in-process (Local) çalışması: Http (tip 6), DaprService (tip 3), Soap (tip 16), StateStore (tip 17, set/get round-trip aynı statik Dapr anahtarına), CacheAside (tip 18, `til-cache-source` üzerinden miss-then-hit round-trip) · error-boundary handler seçiminin task'ın SONUCUNDAN yapılması: HTTP 500 → task-level Notify, client `timeoutSeconds` vs. yavaş bir MockLab route → task-level Rollback (schema'da geçerli ama `CompiledBoundary.Compile`'ın hiç okumadığı `errorBoundary.onTimeout` yerine `onError`+`errorCodes:["Task:Http:Timeout"]` üzerinden), SOAP fault → task-level Abort (instance fault) · `TaskInvocationResult`/`StandardTaskResponse` şeklinin (`taskType`, `statusCode`, metadata anahtar kümesi) Local ve Remote routing arasında DEĞİŞMEMESİ — aynı test dosyasının host'u zorla Remote'a çevrilip yeniden koşturulmasıyla doğrulanır | vnext `1007-…` dalı beş task tipini ayrı bir Execution servisinden Orchestration host'una taşıdı; **DaprService, Soap ve StateStore/CacheAside'ın üçü de bu repoda daha önce HİÇ bileşeni yoktu** — ilk kez bu senaryoyla örnekleniyor. `tilTaskType` alanı ayrıca bu dalda routing'e bağlı olarak iki kere yanlış damgalanmıştı (build raporu) ve `InstanceTasks` günlüğüne serileştiriliyor, bu yüzden ayrı bir parity test sınıfı bu alanı hedefliyor | `Tests/TaskInvocationLab` (3 sınıf, `TaskTypeInvocationTests`/`ErrorBoundaryInteractionTests`/`ResultModelParityTests`) — `ResultModelParityTests`'in değeri iki ayrı koşudan gelir (Local varsayılan + `Workflow__TaskInvocation__Modes__*=Remote` ile zorlanmış), tek koşu yeterli kanıt değildir | — (bilinçli: routing/parity senaryosu, eşzamanlılık iddiası yok) | 🆕 **Yeni — henüz koşulmadı** (2026-09-19, testler yazıldı ve derlendi; `dotnet test` bilerek çalıştırılmadı, paylaşılan lokal runtime'da eşzamanlı başka bir entegrasyon koşusu vardı) | | **subflow-start-failure-lab** | Post-commit `StartSubflowJob` başarısız olduğunda parent'ın gerçekten kurtarılabilir olup olmadığı: `parent-subflow-state` (`stateType: 4`, `subFlow.type: "S"`) → `HandleSubFlowStep` (order 70) `InstanceCorrelation`'ı KENDİ UoW'unda commit'ler → post-commit `StartSubflowJobHandler`/`SubflowStarter` child'ın start transition'ını çağırır ve child'ın şeması zorunlu kıldığı `mustProvide` alanı (parent'ın `ISubFlowMapping`'i `Instance.Data`'dan kasıtlı göndermiyor) yüzünden schema validation'da başarısız olur → `TransitionRunner.CompensateFailedCoordinationAsync` parent'ı fault'lar → `POST .../retry` (düzeltilmiş `mustProvide` gövdede) child'ı gerçekten yaratıp yaratmadığı | Sibling dal `fix/stranded-busy-and-dead-flag-cleanup`'taki bir code review, post-commit `StartSubflowJob` hatasında parent'ı fault'lamanın dokümantasyonda "görünür ve **retry edilebilir**" diye tanımlanmasına karşı CRITICAL bir bulgu bildirdi: `HandleSubFlowStep`'in correlation'ı child hiç yaratılmadan ÖNCE commit'lemesi, eski `InstanceRetryAppService.RetryFaultedInstanceAsync`'in `instance.HasActiveSubFlow` dalının parent'ı unfault'layıp commit'lemesi, incident'larını resolve etmesi, SONRA var olmayan child'ı gateway üzerinden sorgulaması ve 404 alması. **Ölçüldü — CRITICAL'ın iki yarısı da doğrulandı, ama ayrı ayrı:** fault/incident yarısı ÇALIŞIYORDU, retry yarısı ÇALIŞMIYORDU (`HTTP 404 Instance:100013`, parent unfault'lanıp `parent-subflow-state`'te sıkışık kalıyordu, fault geçmişi siliniyordu). **Runtime'da düzeltildi (bu görev):** `InstanceRetryAppService` artık child'ı parent'a DOKUNMADAN ÖNCE prob'luyor; "not found" ise parent'ı yeniden arm ediyor (unfault → Busy, canlı bir bloklayan SubFlow parent'ının her zaman taşıdığı invariant'ı taklit ederek) ve AYNI correlation için subflow start'ı yeniden çalıştırıyor (`ISubflowStarter`'ın `StrictIdempotency`'siyle korelasyonun kendi önceden üretilmiş `SubFlowInstanceId`'si üzerinden idempotent — ikinci bir correlation yaratmıyor, parent'ın transition'ını tekrar çalıştırmıyor). Restart'ın kendisi başarısız olursa parent SESSİZCE Active bırakılmıyor: `PostCommitParentMutationService.FaultAsync` üzerinden (aynı `TransitionRunner.CompensateFailedCoordinationAsync`'in kullandığı yol) yeniden fault'lanıyor, taze bir incident'la — bir sonraki retry aynı yoldan tekrar dener. Test artık gerçek kurtarmayı kanıtlıyor: `mustProvide` `Instance.Data`'dan asla gelmiyor (ilk otomatik start hâlâ aynı şekilde başarısız oluyor), ama retry İSTEĞİNİN GÖVDESİNDEN (`{"attributes":{"mustProvide": ...}}`) okunuyor — `InstanceRetryAppService`'in restart yolu bunu `ScriptContext.Body`'ye kadar taşıyor, tıpkı başka bir transition'ın `OnExecute` task'larının retry-verisi göreceği yol gibi. **Ayrıca ölçüldü**: en ucuz form denendi ilk önce (`subFlow.process` gerçek key + hiç yayınlanmamış versiyon `9.9.9`) — bu senaryoyu YENİDEN ÜRETMEDİ (bkz. README), schema-validation formuna geçildi; parent `sync=true` ile başlatılırsa child'ın schema hatası PARENT'ın kendi start cevabına sızıyor, test bilerek `sync=false` kullanıyor. `failed-subflow-start-is-faulted-but-not-retryable` id'li bir `vnext-meta` known-issue artık AÇILMAMALI — açık kapandı; `pre-reserved-job-failure-can-strand-busy` hâlâ farklı, zaten düzeltilmiş bir kusuru belgeliyor | `Tests/SubflowStartFailureLab` (`SubflowStartFailureLabTests`, 2 test) | — (bilinçli: tek senaryo, yük/eşzamanlılık iddiası yok) | ✅ **YEŞİL — 2/2**, ve geniş regresyon seti **75/75** (2026-09-22, lokal core runtime `:4201`, rebuild sonrası). Test 1 fault+incident'ı kanıtlıyor. Test 2 (`RetryingAFaultedSubflowStartRecoversTheInstance`) artık GERÇEK kurtarmayı kanıtlıyor: retry `< 400` dönüyor, parent `F` değil ve `parent-subflow-state`'te (correlation açık, artık canlı bir child'ı bekliyor), state function child'a inip `state="child-initial"`/`status="A"` raporluyor. Postgres'te doğrulandı: parent `Status='B'`, `HasActiveIncident=false`; correlation `IsCompleted=false`, AYNI `SubFlowInstanceId`; child şemasında (`subflow_start_failure_lab_child.Instances`) o id ile `Status='A'`, `CurrentState='child-initial'` satırı artık MEVCUT | diff --git a/core/Workflows/schedule-after-auto/build-schedule-after-auto.py b/core/Workflows/schedule-after-auto/build-schedule-after-auto.py index 5f22eeb..88c14be 100644 --- a/core/Workflows/schedule-after-auto/build-schedule-after-auto.py +++ b/core/Workflows/schedule-after-auto/build-schedule-after-auto.py @@ -50,7 +50,7 @@ def hook(mapping_file, order=1): "flow": "sys-flows", "flowVersion": "1.0.0", "domain": "core", - "version": "1.0.0", + "version": "1.0.1", "tags": [ "integration-test", "schedule-after-auto", @@ -105,6 +105,9 @@ def hook(mapping_file, order=1): "labels": label("Gate Timeout (8s)"), "timer": timer("GateTimeoutTimer.csx"), "onExecutionTasks": [hook("TimeoutMapping.csx")], + # Surfaced on the state function's kind:"scheduled" entry (runtime resolves it + # from the job's source state); the test reads it back. + "annotations": {"ui/countdown": "gate-timeout"}, }, ], }, diff --git a/core/Workflows/schedule-after-auto/schedule-after-auto.json b/core/Workflows/schedule-after-auto/schedule-after-auto.json index 631df2e..304da94 100644 --- a/core/Workflows/schedule-after-auto/schedule-after-auto.json +++ b/core/Workflows/schedule-after-auto/schedule-after-auto.json @@ -3,7 +3,7 @@ "flow": "sys-flows", "flowVersion": "1.0.0", "domain": "core", - "version": "1.0.0", + "version": "1.0.1", "tags": [ "integration-test", "schedule-after-auto", @@ -128,7 +128,10 @@ "code": "dXNpbmcgU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWM7CnVzaW5nIFN5c3RlbS5EeW5hbWljOwp1c2luZyBTeXN0ZW0uVGhyZWFkaW5nLlRhc2tzOwp1c2luZyBCQlQuV29ya2Zsb3cuRGVmaW5pdGlvbnM7CnVzaW5nIEJCVC5Xb3JrZmxvdy5TY3JpcHRpbmc7CgovLy8gPHN1bW1hcnk+Ci8vLyBvbkV4ZWN1dGUgb2YgdGhlIGdhdGUncyBzY2hlZHVsZWQgdHJhbnNpdGlvbi4gSXRzIG9ubHkgam9iIGlzIHRvIGxlYXZlIHByb29mIGluIGluc3RhbmNlIGRhdGEKLy8vIHRoYXQgdGhlIHRpbWVyIHJlYWxseSBmaXJlZCDigJQgdGhlIHN0YXRlIGFsb25lIHdvdWxkIG5vdCBkaXN0aW5ndWlzaCAidGhlIHRpbWVyIGZpcmVkIiBmcm9tCi8vLyAic29tZXRoaW5nIGVsc2UgbW92ZWQgdGhlIGluc3RhbmNlIi4KLy8vIDwvc3VtbWFyeT4KcHVibGljIGNsYXNzIFRpbWVvdXRNYXBwaW5nIDogU2NyaXB0QmFzZSwgSU1hcHBpbmcKewogICAgcHVibGljIFRhc2s8U2NyaXB0UmVzcG9uc2U+IElucHV0SGFuZGxlcihXb3JrZmxvd1Rhc2sgdGFzaywgU2NyaXB0Q29udGV4dCBjb250ZXh0KQogICAgewogICAgICAgIHJldHVybiBUYXNrLkZyb21SZXN1bHQobmV3IFNjcmlwdFJlc3BvbnNlKCkpOwogICAgfQoKICAgIHB1YmxpYyBUYXNrPFNjcmlwdFJlc3BvbnNlPiBPdXRwdXRIYW5kbGVyKFNjcmlwdENvbnRleHQgY29udGV4dCkKICAgIHsKICAgICAgICB2YXIgZGF0YSA9IGNvbnRleHQuSW5zdGFuY2UuRGF0YSBhcyBJRGljdGlvbmFyeTxzdHJpbmcsIG9iamVjdD47CgogICAgICAgIHZhciBjdXJyZW50ID0gMDsKICAgICAgICBpZiAoZGF0YSAhPSBudWxsICYmIGRhdGEuVHJ5R2V0VmFsdWUoInRpbWVvdXRGaXJlZCIsIG91dCB2YXIgcmF3KSAmJiByYXcgIT0gbnVsbCkKICAgICAgICB7CiAgICAgICAgICAgIGludC5UcnlQYXJzZShyYXcuVG9TdHJpbmcoKSwgb3V0IGN1cnJlbnQpOwogICAgICAgIH0KCiAgICAgICAgZHluYW1pYyByZXN1bHQgPSBuZXcgRXhwYW5kb09iamVjdCgpOwogICAgICAgIHZhciB0YXJnZXQgPSAoSURpY3Rpb25hcnk8c3RyaW5nLCBvYmplY3Q+KXJlc3VsdDsKICAgICAgICB0YXJnZXRbInRpbWVvdXRGaXJlZCJdID0gY3VycmVudCArIDE7CgogICAgICAgIExvZ0luZm9ybWF0aW9uKCQiVGltZW91dE1hcHBpbmc6IHRpbWVvdXRGaXJlZCB7Y3VycmVudH0gLT4ge2N1cnJlbnQgKyAxfSIpOwogICAgICAgIHJldHVybiBUYXNrLkZyb21SZXN1bHQobmV3IFNjcmlwdFJlc3BvbnNlIHsgRGF0YSA9IHJlc3VsdCB9KTsKICAgIH0KfQo=" } } - ] + ], + "annotations": { + "ui/countdown": "gate-timeout" + } } ] }, @@ -228,4 +231,4 @@ ] } } -} \ No newline at end of file +} diff --git a/core/Workflows/timeout-lab/timeout-lab-parent.json b/core/Workflows/timeout-lab/timeout-lab-parent.json index 0d36c1f..b6aafc6 100644 --- a/core/Workflows/timeout-lab/timeout-lab-parent.json +++ b/core/Workflows/timeout-lab/timeout-lab-parent.json @@ -3,7 +3,7 @@ "flow": "sys-flows", "flowVersion": "1.0.0", "domain": "core", - "version": "1.0.0", + "version": "1.0.1", "tags": [ "integration-test", "timeout-lab", @@ -100,6 +100,9 @@ "timer": { "reset": "never", "duration": "PT20S" + }, + "annotations": { + "ui/countdown": "parent-override" } } } diff --git a/core/Workflows/timeout-lab/timeout-lab-root.json b/core/Workflows/timeout-lab/timeout-lab-root.json index c3ed2cc..5ebe61d 100644 --- a/core/Workflows/timeout-lab/timeout-lab-root.json +++ b/core/Workflows/timeout-lab/timeout-lab-root.json @@ -3,7 +3,7 @@ "flow": "sys-flows", "flowVersion": "1.0.0", "domain": "core", - "version": "1.0.0", + "version": "1.0.1", "tags": [ "integration-test", "timeout-lab", @@ -11,7 +11,7 @@ "state-function" ], "attributes": { - "_comment": "A workflow-level timeout short enough to fire inside a test run. Nothing else in this repo could exercise one: the only two authored timeouts are PT15M. The instance parks in root-waiting doing nothing, so the deadline is the only thing that moves it — which is what lets a test assert both halves of the contract in one run: the state function's `timeout` block while it is pending, and the instance actually landing on `target` when it fires. NOTE the duration is an absolute budget from START, not an idle window: timer.reset is required by the schema and read nowhere in the runtime.", + "_comment": "A workflow-level timeout short enough to fire inside a test run. Nothing else in this repo could exercise one: the only two authored timeouts are PT15M. The instance parks in root-waiting doing nothing, so the deadline is the only thing that moves it — which is what lets a test assert both halves of the contract in one run: the state function's `timeout` block while it is pending, and the instance actually landing on `target` when it fires. NOTE the duration is an absolute budget from START, not an idle window: timer.reset is required by the schema and read nowhere in the runtime. ANNOTATIONS (1.0.1): every entry the state body lists carries a distinct `ui/source` annotation — state (root-finish), shared (root-note), cancel, exit, updateData — and the timeout carries `ui/countdown`, so one read asserts that each kind and the `timeout` block pass their definition's annotations through. None of these transitions is ever called; the timeout still does the moving.", "type": "F", "timeout": { "key": "root-abandoned", @@ -20,6 +20,9 @@ "timer": { "reset": "never", "duration": "PT20S" + }, + "annotations": { + "ui/countdown": "root-deadline" } }, "labels": [ @@ -71,7 +74,10 @@ "language": "en-US", "label": "Finish before the deadline" } - ] + ], + "annotations": { + "ui/source": "state" + } } ] }, @@ -137,7 +143,60 @@ "language": "en-US", "label": "Cancel Timeout Lab Root" } - ] - } + ], + "annotations": { + "ui/source": "cancel" + } + }, + "exit": { + "key": "exit-timeout-lab-root", + "target": "root-completed", + "triggerType": 0, + "versionStrategy": "Major", + "labels": [ + { + "language": "en-US", + "label": "Exit Timeout Lab Root" + } + ], + "annotations": { + "ui/source": "exit" + } + }, + "updateData": { + "key": "update-timeout-lab-root", + "target": "$self", + "triggerType": 0, + "versionStrategy": "Major", + "labels": [ + { + "language": "en-US", + "label": "Update Timeout Lab Root Data" + } + ], + "annotations": { + "ui/source": "updateData" + } + }, + "sharedTransitions": [ + { + "key": "root-note", + "target": "$self", + "triggerType": 0, + "versionStrategy": "Minor", + "labels": [ + { + "language": "en-US", + "label": "Add a note (shared)" + } + ], + "availableIn": [ + "root-waiting" + ], + "annotations": { + "ui/source": "shared" + } + } + ] } } diff --git a/tests/Core.IntegrationTests/Tests/ScheduleAfterAuto/README.md b/tests/Core.IntegrationTests/Tests/ScheduleAfterAuto/README.md index 02b08cc..6b40217 100644 --- a/tests/Core.IntegrationTests/Tests/ScheduleAfterAuto/README.md +++ b/tests/Core.IntegrationTests/Tests/ScheduleAfterAuto/README.md @@ -102,10 +102,13 @@ MockLab gerekmez — akış yalnız script task (type 7) kullanır, HTTP task yo | Test | İddia | | --- | --- | | `AutoWinner_SuppressesTimerArming_AndTheTimerNeverFires` | `mode: "auto"` ile başlatılan instance `advanced`'e zincirlenir; zincir boyunca (≥2 poll) `gate-timeout` için **hiç** `kind: "scheduled"` girdisi görülmez; `autoAdvances == 1`; timer süresi (8 sn) + 5 sn geçtikten sonra da instance `advanced`'de, `timeoutFired == 0` ve armed girdi yok | -| `NoAutoWinner_ArmsTheScheduledTransition_AndItFires` | `mode: "park"` ile başlatılan instance `gate`'te bekler; state function `gate-timeout` girdisini makul bir `executeAtUtc` ile gösterir; timer ateşlenir, instance `gate-timedout`'a geçer, `timeoutFired == 1`, `autoAdvances == 0` | +| `NoAutoWinner_ArmsTheScheduledTransition_AndItFires` | `mode: "park"` ile başlatılan instance `gate`'te bekler; state function `gate-timeout` girdisini makul bir `executeAtUtc` ile **ve tanımdaki `annotations` ile** (`ui/countdown: gate-timeout`, akış 1.0.1) gösterir; timer ateşlenir, instance `gate-timedout`'a geçer, `timeoutFired == 1`, `autoAdvances == 0` | Doğrulama durumu: **2/2 yeşil** (2026-09-03, lokal runtime, vnext `feature/schedule-after-auto` -@ `702a03b6`), iki koşu üst üste, ~24 sn. +@ `702a03b6`), iki koşu üst üste, ~24 sn. **Annotations (2026-09-24):** 2/2 yeşil, lokal runtime +`claude/annotations-state-function-06d98e`. Scheduled girdileri önceden yalnız job satırından +kuruluyor ve transition'ın `annotations`'ını düşürüyordu; runtime artık onları job'un `SourceState`'i +üzerinden tanımdan çözüyor (postgres: `gate-timeout` job'u `SourceState=gate`). ### Bilinen kısıtlar diff --git a/tests/Core.IntegrationTests/Tests/ScheduleAfterAuto/ScheduleAfterAutoTests.cs b/tests/Core.IntegrationTests/Tests/ScheduleAfterAuto/ScheduleAfterAutoTests.cs index 1e18906..1052b56 100644 --- a/tests/Core.IntegrationTests/Tests/ScheduleAfterAuto/ScheduleAfterAutoTests.cs +++ b/tests/Core.IntegrationTests/Tests/ScheduleAfterAuto/ScheduleAfterAutoTests.cs @@ -63,7 +63,7 @@ await WaitUntilAsync( async () => { probes++; - var (present, executeAt) = await ScheduledEntryAsync(instanceId); + var (present, executeAt, _) = await ScheduledEntryAsync(instanceId); if (present) everSeenAt ??= executeAt ?? "(entry without executeAtUtc)"; var (state, status) = await GetInstanceStateAsync(Workflow, instanceId); @@ -108,11 +108,13 @@ public async Task NoAutoWinner_ArmsTheScheduledTransition_AndItFires() await WaitUntilSettledAsync(Workflow, instanceId); string? executeAtRaw = null; + string? countdownAnnotation = null; await WaitUntilAsync( async () => { - var (present, executeAt) = await ScheduledEntryAsync(instanceId); + var (present, executeAt, countdown) = await ScheduledEntryAsync(instanceId); executeAtRaw = executeAt; + countdownAnnotation = countdown; return present; }, $"no '{ScheduledTransition}' scheduled entry was exposed by the state function — " + @@ -121,6 +123,10 @@ await WaitUntilAsync( Assert.NotNull(executeAtRaw); + // The scheduled entry carries the transition definition's annotations, like every other + // transitions[] kind. It used to be built from the job row alone and dropped them. + Assert.Equal("gate-timeout", countdownAnnotation); + var executeAt = DateTimeOffset.Parse(executeAtRaw!, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal); @@ -167,8 +173,8 @@ private async Task StartAsyncModeAsync(string mode) } /// - /// Whether the state function currently exposes the gate's scheduled entry, and its - /// executeAtUtc. + /// Whether the state function currently exposes the gate's scheduled entry, its + /// executeAtUtc, and its annotations["ui/countdown"]. /// /// Presence and executeAtUtc are reported separately on purpose: /// WorkflowTestBase.GetScheduledExecuteAtAsync returns null both when no entry is armed @@ -176,19 +182,24 @@ private async Task StartAsyncModeAsync(string mode) /// the entry itself. /// /// - private async Task<(bool Present, string? ExecuteAt)> ScheduledEntryAsync(string instanceId) + private async Task<(bool Present, string? ExecuteAt, string? Countdown)> ScheduledEntryAsync(string instanceId) { var response = await Api.CallInstanceFunctionAsync(Workflow, instanceId, "state", headers: Headers()); - if (!response.Body.TryGetProperty("transitions", out var transitions)) return (false, null); + if (!response.Body.TryGetProperty("transitions", out var transitions)) return (false, null, null); foreach (var transition in transitions.EnumerateArray()) { if (!transition.TryGetProperty("kind", out var kind) || kind.GetString() != "scheduled") continue; if (!transition.TryGetProperty("name", out var name) || name.GetString() != ScheduledTransition) continue; - return (true, transition.TryGetProperty("executeAtUtc", out var at) ? at.GetString() : null); + var countdown = transition.TryGetProperty("annotations", out var annotations) + && annotations.ValueKind == JsonValueKind.Object + && annotations.TryGetProperty("ui/countdown", out var value) + ? value.GetString() + : null; + return (true, transition.TryGetProperty("executeAtUtc", out var at) ? at.GetString() : null, countdown); } - return (false, null); + return (false, null, null); } } diff --git a/tests/Core.IntegrationTests/Tests/TimeoutLab/README.md b/tests/Core.IntegrationTests/Tests/TimeoutLab/README.md index 016bcc2..5c20187 100644 --- a/tests/Core.IntegrationTests/Tests/TimeoutLab/README.md +++ b/tests/Core.IntegrationTests/Tests/TimeoutLab/README.md @@ -72,6 +72,19 @@ Requires the flows to be published (`wf domain use core && wf sync`) against a l 4. A child running under its parent's override reports **the override's** `key`/`target`, not its own (absent) definition, and is pulled to that target. 5. A parent does not inherit its child's deadline: the block describes the polled instance only. +6. **Annotations (flows 1.0.1).** Every entry the state body lists carries its definition's + `annotations` — `root-finish` (`stateTransition`), `root-note` (`sharedTransition`), and the + well-known `cancel`, `exit` and `updateData` — each with a distinct `ui/source` value, and the + `timeout` block carries `timeout.annotations` (`ui/countdown: root-deadline`). The child's block + carries the **override's** annotations (`parent-override`): the stamp the parent writes carries + them, and an override replaces the timeout as a whole. None of the added transitions is ever + called; the timeout is still the only thing that moves the instance. Scheduled entries are pinned + by `schedule-after-auto`. + + Status: **4/4 green** (2026-09-24, local runtime `claude/annotations-state-function-06d98e`, + `VNEXT_BASE_URL=http://localhost:4201`). Evidence beyond the summary: the live state body lists all + five kinds with their annotations and `timeout.annotations`; postgres shows the instances bound to + `1.0.1` and the child's `subflow.timeout_override` stamp carrying `"annotations":{"ui/countdown":"parent-override"}`. ## What it caught on its first run diff --git a/tests/Core.IntegrationTests/Tests/TimeoutLab/TimeoutLabTests.cs b/tests/Core.IntegrationTests/Tests/TimeoutLab/TimeoutLabTests.cs index 32f571d..5020cc7 100644 --- a/tests/Core.IntegrationTests/Tests/TimeoutLab/TimeoutLabTests.cs +++ b/tests/Core.IntegrationTests/Tests/TimeoutLab/TimeoutLabTests.cs @@ -132,6 +132,10 @@ await WaitUntilAsync( Assert.Equal("child-abandoned", timeout!.Value.GetProperty("key").GetString()); Assert.Equal(ChildTimedOutState, timeout.Value.GetProperty("target").GetString()); + // The override's annotations travel with it — the stamp the parent writes carries them, + // and they replace the child's (the child declares no timeout, so it has none of its own). + Assert.Equal("parent-override", Annotation(timeout.Value, "ui/countdown")); + // ── and the runtime moves it to exactly that target ────────────────────── await WaitUntilAsync( async () => @@ -145,6 +149,49 @@ await WaitUntilAsync( Deadline + TimeSpan.FromSeconds(40)); } + /// + /// Every entry the state body lists carries its definition's annotations: the state, + /// shared and the three well-known workflow-level transitions, and the timeout block. Each + /// fixture entry carries a distinct ui/source value, so a dropped or crossed annotation + /// names itself. (Scheduled entries are pinned by schedule-after-auto.) + /// + [Fact] + public async Task RootFlow_StateBodyCarriesTheAnnotationsOfEveryListedEntry() + { + var instanceId = await StartAsync(RootWorkflow, new { }); + + await WaitForInstanceStateAsync(RootWorkflow, instanceId, RootWaitingState); + + var response = await Api.CallInstanceFunctionAsync(RootWorkflow, instanceId, "state", headers: Headers()); + var body = response.Body; + + var bySource = new Dictionary(StringComparer.Ordinal); + foreach (var transition in body.GetProperty("transitions").EnumerateArray()) + { + var name = transition.GetProperty("name").GetString()!; + bySource[name] = (transition.GetProperty("kind").GetString()!, Annotation(transition, "ui/source")); + } + + var described = string.Join(", ", bySource.Select(kv => $"{kv.Key}={kv.Value.Kind}/{kv.Value.Source ?? "∅"}")); + Assert.Equal(("stateTransition", "state"), bySource.GetValueOrDefault("root-finish")); + Assert.Equal(("sharedTransition", "shared"), bySource.GetValueOrDefault("root-note")); + Assert.Equal(("cancel", "cancel"), bySource.GetValueOrDefault("cancel-timeout-lab-root")); + Assert.Equal(("exit", "exit"), bySource.GetValueOrDefault("exit-timeout-lab-root")); + Assert.True(bySource.GetValueOrDefault("update-timeout-lab-root") == ("updateData", "updateData"), + $"updateData entry missing or without its annotation — transitions: {described}"); + + Assert.True(body.TryGetProperty("timeout", out var timeout) && timeout.ValueKind == JsonValueKind.Object, + $"no `timeout` block — {await DescribeAsync(RootWorkflow, instanceId)}"); + Assert.Equal("root-deadline", Annotation(timeout, "ui/countdown")); + } + + private static string? Annotation(JsonElement element, string key) => + element.TryGetProperty("annotations", out var annotations) + && annotations.ValueKind == JsonValueKind.Object + && annotations.TryGetProperty(key, out var value) + ? value.GetString() + : null; + /// /// The timeout block describes the polled instance and nothing else: a parent whose child /// carries a deadline must not inherit it, and must not be given one it does not have.