diff --git a/TEST-SCENARIOS.md b/TEST-SCENARIOS.md index 0ee1145..4de799a 100644 --- a/TEST-SCENARIOS.md +++ b/TEST-SCENARIOS.md @@ -20,7 +20,7 @@ geçerliliğini yitiren senaryo **silinmez**, `deprecated` işaretlenip sebebi y | **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, 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ı · **SubFlow fault zinciri** (`eb-sf-root`→`eb-sf-mid`→`eb-sf-leaf`): leaf HTTP 400 abort → mid abort → root global notify ile error-end; root'ta incident'ın boundary verdict'iyle yazılıp **resolve** edilmesi, fault'lanan seviyelerde açık kalması | 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). SubFlow fault zinciri: preprod'dan "subflow'da HTTP 400, ana akış error-end'de, ana akışta incident yok" bildirimi (onboarding `kyc-main-workflow`, 0.0.93) — ölçüldü: incident kayıp değil, resolve (2026-09-24) | `Tests/ErrorBoundaryLab` (7 sınıf, 36 test) | — (bilinçli: davranış/çözümleme senaryosu, eşzamanlılık iddiası yok) | ✅ **Aktif** — `SubFlowFaultChainTests` 5/5 yeşil (vnext `master` @ `f6c5c828`, 2026-09-24); paket 35/36, tek kırmızı `SecuredIncidentsTests` 403 beklentisi vnext #1027 sonrası bayat. Önceki: 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) | +| **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ı · **SubFlow fault zinciri** (`eb-sf-root`→`eb-sf-mid`→`eb-sf-leaf`): leaf HTTP 400 abort → mid abort → root global notify ile error-end; root'ta incident'ın boundary verdict'iyle yazılıp **resolve** edilmesi, boundary handling içindeki mapping'in `context.Incident.ActiveIncident`'ı görmesi, fault'lanan seviyelerde açık kalması | 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). SubFlow fault zinciri: preprod'dan "subflow'da HTTP 400, ana akış error-end'de, ana akışta incident yok" bildirimi (onboarding `kyc-main-workflow`, 0.0.93) — ölçüldü: incident kayıp değil, resolve (2026-09-24) | `Tests/ErrorBoundaryLab` (7 sınıf, 36 test) | — (bilinçli: davranış/çözümleme senaryosu, eşzamanlılık iddiası yok) | ✅ **Aktif** — `SubFlowFaultChainTests` 5/5 yeşil (vnext `master` @ `f6c5c828`, 2026-09-24); paket 35/36, tek kırmızı `SecuredIncidentsTests` 403 beklentisi vnext #1027 sonrası bayat. Önceki: 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 | | **chain-busy** | Accept-time subflow chain reserve **ve başarısız forward'da geri alınması (E31)** · Busy-as-mutex · `$self` shared transition vs `updateData` lifecycle sınırı · start `initial → initial` semantiği · cancel propagasyonu (in-process ↕ distributed) · scheduled transition re-arm | `updateData`-only self-target profil sınırını pinlemek — `target: $self` "hook'ları atla" demek değil (2026-08-17). **E31 (2026-09-12):** post-commit forward istemci hatasıyla düşünce rezervasyon salınmıyordu; üretimde 30 günde 51 mahsur instance (council `2026-09-08-parent-notification-mechanism`, P0) | `Tests/ChainBusy` (5 sınıf) | `api-tests/chain-busy/chain-busy-behaviour-test.py`, `chain-busy-accept-test.py` | ✅ Aktif | | **script-race-lab** | Script engine: paylaşılan `AssemblyLoadContext`'te çift-derleme yarışı · `scripts.helpers` · subflow output mapping · parent kalıcı fault riski | `Script_XXXX already loaded` / `FileLoadException` yarışının fixture'ı; fix'siz runtime'da kaybedenler parent'ı kalıcı fault'lar (2026-08-18) | `Tests/ScriptRaceLab` | `api-tests/script-race-lab/race-load.py` (yük), `publish.py` | ✅ Aktif | diff --git a/core/Tasks/error-boundary-lab/eb-incident-probe-task-2.json b/core/Tasks/error-boundary-lab/eb-incident-probe-task-2.json new file mode 100644 index 0000000..44d0d6e --- /dev/null +++ b/core/Tasks/error-boundary-lab/eb-incident-probe-task-2.json @@ -0,0 +1,18 @@ +{ + "key": "eb-incident-probe-task-2", + "version": "1.0.0", + "domain": "core", + "flow": "sys-tasks", + "flowVersion": "1.0.0", + "tags": [ + "integration-test", + "error-boundary-lab", + "script-task", + "incident-probe", + "subflow-fault-chain" + ], + "attributes": { + "type": "7", + "config": {} + } +} diff --git a/core/Tasks/error-boundary-lab/eb-incident-probe-task.json b/core/Tasks/error-boundary-lab/eb-incident-probe-task.json new file mode 100644 index 0000000..591d810 --- /dev/null +++ b/core/Tasks/error-boundary-lab/eb-incident-probe-task.json @@ -0,0 +1,18 @@ +{ + "key": "eb-incident-probe-task", + "version": "1.0.0", + "domain": "core", + "flow": "sys-tasks", + "flowVersion": "1.0.0", + "tags": [ + "integration-test", + "error-boundary-lab", + "script-task", + "incident-probe", + "subflow-fault-chain" + ], + "attributes": { + "type": "7", + "config": {} + } +} diff --git a/core/Workflows/error-boundary-lab/build-error-boundary-lab.py b/core/Workflows/error-boundary-lab/build-error-boundary-lab.py index 55d6995..586e0c7 100644 --- a/core/Workflows/error-boundary-lab/build-error-boundary-lab.py +++ b/core/Workflows/error-boundary-lab/build-error-boundary-lab.py @@ -76,7 +76,9 @@ # handler runs on failed attempts too, so the old httpSucceeded stamp was true even when # the call had failed; httpAttempts/httpLastStatus are what actually discriminate, and # httpAttempts is how the retry tests prove a recovery without reading MockLab's logs. -VERSION = "1.1.0" +# 1.2.0 — eb-sf-root carries an incident probe on its boundary transition and error-end OnEntry +# (context.Incident as a mapping sees it while the root handles a subflow fault). +VERSION = "1.2.0" VIEWER_ROLE = "eb-lab.viewer" @@ -367,6 +369,7 @@ def terminal(key, text, sub_type): # afterwards — in particular the root's, which handled the fault instead of faulting. SF_RULE = "EbAlwaysTrueRule.csx" SF_MAPPING = "EbSubFlowPassMapping.csx" +PROBE_MAPPING = "EbIncidentProbeMapping.csx" def auto(key, target, text): @@ -450,13 +453,19 @@ def sf_workflow(key, text, states, start_target, boundary, extra=None): subflow_state("r-in-mid", "Root: waiting on the mid", "eb-sf-mid", [auto("r-mid-done", "r-done", "Root: mid finished")]), terminal("r-done", "Root done", 1), - terminal("r-error-end", "Root error end", 2), + # The OnEntry probe records what context.Incident shows AFTER the state change, still + # inside the boundary transition's hop (FinalizeTransitionStep resolves it after this). + dict(terminal("r-error-end", "Root error end", 2), + onEntries=[hook(1, "eb-incident-probe-task-2", PROBE_MAPPING)]), ], "r-initial", # Same shape as the preprod root: a notify with a transition, so the root HANDLES the fault. [{"action": 4, "transition": "r-has-error", "priority": 10}], extra={"sharedTransitions": [ - dict(transition("r-has-error", "r-error-end", "Root: a subflow faulted"), + # The OnExecute probe records what context.Incident shows to the boundary transition's + # own mapping — the place a domain developer branches on the subflow's failure. + dict(transition("r-has-error", "r-error-end", "Root: a subflow faulted", + [hook(1, "eb-incident-probe-task", PROBE_MAPPING)]), availableIn=["r-in-mid"]), ]}, ) diff --git a/core/Workflows/error-boundary-lab/eb-sf-leaf.json b/core/Workflows/error-boundary-lab/eb-sf-leaf.json index 6449fe7..034c055 100644 --- a/core/Workflows/error-boundary-lab/eb-sf-leaf.json +++ b/core/Workflows/error-boundary-lab/eb-sf-leaf.json @@ -3,7 +3,7 @@ "flow": "sys-flows", "flowVersion": "1.0.0", "domain": "core", - "version": "1.1.0", + "version": "1.2.0", "tags": [ "integration-test", "error-boundary-lab", diff --git a/core/Workflows/error-boundary-lab/eb-sf-mid.json b/core/Workflows/error-boundary-lab/eb-sf-mid.json index 07deb05..bbd0489 100644 --- a/core/Workflows/error-boundary-lab/eb-sf-mid.json +++ b/core/Workflows/error-boundary-lab/eb-sf-mid.json @@ -3,7 +3,7 @@ "flow": "sys-flows", "flowVersion": "1.0.0", "domain": "core", - "version": "1.1.0", + "version": "1.2.0", "tags": [ "integration-test", "error-boundary-lab", @@ -97,7 +97,7 @@ "process": { "key": "eb-sf-leaf", "domain": "core", - "version": "1.1.0", + "version": "1.2.0", "flow": "sys-flows" }, "mapping": { diff --git a/core/Workflows/error-boundary-lab/eb-sf-root.json b/core/Workflows/error-boundary-lab/eb-sf-root.json index 7b5c552..90e6e50 100644 --- a/core/Workflows/error-boundary-lab/eb-sf-root.json +++ b/core/Workflows/error-boundary-lab/eb-sf-root.json @@ -3,7 +3,7 @@ "flow": "sys-flows", "flowVersion": "1.0.0", "domain": "core", - "version": "1.1.0", + "version": "1.2.0", "tags": [ "integration-test", "error-boundary-lab", @@ -98,7 +98,7 @@ "process": { "key": "eb-sf-mid", "domain": "core", - "version": "1.1.0", + "version": "1.2.0", "flow": "sys-flows" }, "mapping": { @@ -158,7 +158,21 @@ ], "view": null, "subFlow": null, - "onEntries": [], + "onEntries": [ + { + "order": 1, + "task": { + "key": "eb-incident-probe-task-2", + "domain": "core", + "version": "1.0.0", + "flow": "sys-tasks" + }, + "mapping": { + "location": "./src/EbIncidentProbeMapping.csx", + "code": "dXNpbmcgU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWM7CnVzaW5nIFN5c3RlbS5EeW5hbWljOwp1c2luZyBTeXN0ZW0uVGhyZWFkaW5nLlRhc2tzOwp1c2luZyBCQlQuV29ya2Zsb3cuRGVmaW5pdGlvbnM7CnVzaW5nIEJCVC5Xb3JrZmxvdy5TY3JpcHRpbmc7CgovLy8gPHN1bW1hcnk+Ci8vLyBSZWNvcmRzIHdoYXQgPGM+Y29udGV4dC5JbmNpZGVudDwvYz4gc2hvd3MgdG8gYSBtYXBwaW5nIHJ1bm5pbmcgaW5zaWRlIHRoZSByb290J3MgZXJyb3ItYm91bmRhcnkKLy8vIGhhbmRsaW5nIG9mIGEgc3ViZmxvdyBmYXVsdCAoZWItc2Ytcm9vdDogdGhlIDxjPnItaGFzLWVycm9yPC9jPiB0cmFuc2l0aW9uIGFuZCB0aGUKLy8vIDxjPnItZXJyb3ItZW5kPC9jPiBPbkVudHJ5KS4gVGhlIHNsb3QgcHJlZml4IGNvbWVzIGZyb20gdGhlIHRhc2sga2V5IHNvIG9uZSBtYXBwaW5nIHNlcnZlcyBib3RoCi8vLyBzbG90cy4gVGhpcyBpcyBleGFjdGx5IHdoYXQgYSBkb21haW4gZGV2ZWxvcGVyIHJlYWRzIHRvIGJyYW5jaCBvbiB0aGUgc3ViZmxvdydzIGZhaWx1cmUuCi8vLyA8L3N1bW1hcnk+CnB1YmxpYyBjbGFzcyBFYkluY2lkZW50UHJvYmVNYXBwaW5nIDogU2NyaXB0QmFzZSwgSU1hcHBpbmcKewogICAgcHVibGljIFRhc2s8U2NyaXB0UmVzcG9uc2U+IElucHV0SGFuZGxlcihXb3JrZmxvd1Rhc2sgdGFzaywgU2NyaXB0Q29udGV4dCBjb250ZXh0KQogICAgewogICAgICAgIHJldHVybiBUYXNrLkZyb21SZXN1bHQobmV3IFNjcmlwdFJlc3BvbnNlKCkpOwogICAgfQoKICAgIHB1YmxpYyBUYXNrPFNjcmlwdFJlc3BvbnNlPiBPdXRwdXRIYW5kbGVyKFNjcmlwdENvbnRleHQgY29udGV4dCkKICAgIHsKICAgICAgICB2YXIgc2xvdCA9IGNvbnRleHQuSW5zdGFuY2U/LkN1cnJlbnRTdGF0ZSA9PSAici1lcnJvci1lbmQiID8gInByb2JlRW50cnkiIDogInByb2JlVHJhbnNpdGlvbiI7CiAgICAgICAgdmFyIGluZm8gPSBjb250ZXh0LkluY2lkZW50OwogICAgICAgIHZhciBhY3RpdmUgPSBpbmZvPy5BY3RpdmVJbmNpZGVudDsKCiAgICAgICAgZHluYW1pYyBwcm9iZSA9IG5ldyBFeHBhbmRvT2JqZWN0KCk7CiAgICAgICAgcHJvYmUuY29udGV4dEhhc0luY2lkZW50SW5mbyA9IGluZm8gIT0gbnVsbDsKICAgICAgICBwcm9iZS5oYXNBY3RpdmVJbmNpZGVudCA9IGluZm8/Lkhhc0FjdGl2ZUluY2lkZW50ID8/IGZhbHNlOwogICAgICAgIHByb2JlLmluY2lkZW50c0xvYWRlZCA9IGluZm8/LkluY2lkZW50c0xvYWRlZCA/PyBmYWxzZTsKICAgICAgICBwcm9iZS50b3RhbEluY2lkZW50Q291bnQgPSBpbmZvPy5Ub3RhbEluY2lkZW50Q291bnQgPz8gLTE7CiAgICAgICAgcHJvYmUuYWN0aXZlSW5jaWRlbnRQcmVzZW50ID0gYWN0aXZlICE9IG51bGw7CiAgICAgICAgcHJvYmUuYWN0aXZlRXJyb3JDb2RlID0gYWN0aXZlPy5FcnJvckNvZGU7CiAgICAgICAgcHJvYmUuYWN0aXZlU3RhdHVzQ29kZSA9IGFjdGl2ZT8uU3RhdHVzQ29kZTsKICAgICAgICBwcm9iZS5hY3RpdmVUYXNrID0gYWN0aXZlPy5UYXNrOwogICAgICAgIHByb2JlLmFjdGl2ZUJvdW5kYXJ5QWN0aW9uID0gYWN0aXZlPy5Cb3VuZGFyeUFjdGlvbjsKCiAgICAgICAgZHluYW1pYyByZXN1bHQgPSBuZXcgRXhwYW5kb09iamVjdCgpOwogICAgICAgICgoSURpY3Rpb25hcnk8c3RyaW5nLCBvYmplY3Q+KXJlc3VsdClbc2xvdF0gPSBwcm9iZTsKICAgICAgICBMb2dJbmZvcm1hdGlvbigkIkViSW5jaWRlbnRQcm9iZU1hcHBpbmdbe3Nsb3R9XTogaGFzPXtwcm9iZS5oYXNBY3RpdmVJbmNpZGVudH0gYWN0aXZlPXtwcm9iZS5hY3RpdmVJbmNpZGVudFByZXNlbnR9IGxvYWRlZD17cHJvYmUuaW5jaWRlbnRzTG9hZGVkfSBjb3VudD17cHJvYmUudG90YWxJbmNpZGVudENvdW50fSIpOwogICAgICAgIHJldHVybiBUYXNrLkZyb21SZXN1bHQobmV3IFNjcmlwdFJlc3BvbnNlIHsgRGF0YSA9IHJlc3VsdCB9KTsKICAgIH0KfQo=" + } + } + ], "onExits": [], "transitions": [] } @@ -175,7 +189,21 @@ "label": "Root: a subflow faulted" } ], - "onExecutionTasks": [], + "onExecutionTasks": [ + { + "order": 1, + "task": { + "key": "eb-incident-probe-task", + "domain": "core", + "version": "1.0.0", + "flow": "sys-tasks" + }, + "mapping": { + "location": "./src/EbIncidentProbeMapping.csx", + "code": "dXNpbmcgU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWM7CnVzaW5nIFN5c3RlbS5EeW5hbWljOwp1c2luZyBTeXN0ZW0uVGhyZWFkaW5nLlRhc2tzOwp1c2luZyBCQlQuV29ya2Zsb3cuRGVmaW5pdGlvbnM7CnVzaW5nIEJCVC5Xb3JrZmxvdy5TY3JpcHRpbmc7CgovLy8gPHN1bW1hcnk+Ci8vLyBSZWNvcmRzIHdoYXQgPGM+Y29udGV4dC5JbmNpZGVudDwvYz4gc2hvd3MgdG8gYSBtYXBwaW5nIHJ1bm5pbmcgaW5zaWRlIHRoZSByb290J3MgZXJyb3ItYm91bmRhcnkKLy8vIGhhbmRsaW5nIG9mIGEgc3ViZmxvdyBmYXVsdCAoZWItc2Ytcm9vdDogdGhlIDxjPnItaGFzLWVycm9yPC9jPiB0cmFuc2l0aW9uIGFuZCB0aGUKLy8vIDxjPnItZXJyb3ItZW5kPC9jPiBPbkVudHJ5KS4gVGhlIHNsb3QgcHJlZml4IGNvbWVzIGZyb20gdGhlIHRhc2sga2V5IHNvIG9uZSBtYXBwaW5nIHNlcnZlcyBib3RoCi8vLyBzbG90cy4gVGhpcyBpcyBleGFjdGx5IHdoYXQgYSBkb21haW4gZGV2ZWxvcGVyIHJlYWRzIHRvIGJyYW5jaCBvbiB0aGUgc3ViZmxvdydzIGZhaWx1cmUuCi8vLyA8L3N1bW1hcnk+CnB1YmxpYyBjbGFzcyBFYkluY2lkZW50UHJvYmVNYXBwaW5nIDogU2NyaXB0QmFzZSwgSU1hcHBpbmcKewogICAgcHVibGljIFRhc2s8U2NyaXB0UmVzcG9uc2U+IElucHV0SGFuZGxlcihXb3JrZmxvd1Rhc2sgdGFzaywgU2NyaXB0Q29udGV4dCBjb250ZXh0KQogICAgewogICAgICAgIHJldHVybiBUYXNrLkZyb21SZXN1bHQobmV3IFNjcmlwdFJlc3BvbnNlKCkpOwogICAgfQoKICAgIHB1YmxpYyBUYXNrPFNjcmlwdFJlc3BvbnNlPiBPdXRwdXRIYW5kbGVyKFNjcmlwdENvbnRleHQgY29udGV4dCkKICAgIHsKICAgICAgICB2YXIgc2xvdCA9IGNvbnRleHQuSW5zdGFuY2U/LkN1cnJlbnRTdGF0ZSA9PSAici1lcnJvci1lbmQiID8gInByb2JlRW50cnkiIDogInByb2JlVHJhbnNpdGlvbiI7CiAgICAgICAgdmFyIGluZm8gPSBjb250ZXh0LkluY2lkZW50OwogICAgICAgIHZhciBhY3RpdmUgPSBpbmZvPy5BY3RpdmVJbmNpZGVudDsKCiAgICAgICAgZHluYW1pYyBwcm9iZSA9IG5ldyBFeHBhbmRvT2JqZWN0KCk7CiAgICAgICAgcHJvYmUuY29udGV4dEhhc0luY2lkZW50SW5mbyA9IGluZm8gIT0gbnVsbDsKICAgICAgICBwcm9iZS5oYXNBY3RpdmVJbmNpZGVudCA9IGluZm8/Lkhhc0FjdGl2ZUluY2lkZW50ID8/IGZhbHNlOwogICAgICAgIHByb2JlLmluY2lkZW50c0xvYWRlZCA9IGluZm8/LkluY2lkZW50c0xvYWRlZCA/PyBmYWxzZTsKICAgICAgICBwcm9iZS50b3RhbEluY2lkZW50Q291bnQgPSBpbmZvPy5Ub3RhbEluY2lkZW50Q291bnQgPz8gLTE7CiAgICAgICAgcHJvYmUuYWN0aXZlSW5jaWRlbnRQcmVzZW50ID0gYWN0aXZlICE9IG51bGw7CiAgICAgICAgcHJvYmUuYWN0aXZlRXJyb3JDb2RlID0gYWN0aXZlPy5FcnJvckNvZGU7CiAgICAgICAgcHJvYmUuYWN0aXZlU3RhdHVzQ29kZSA9IGFjdGl2ZT8uU3RhdHVzQ29kZTsKICAgICAgICBwcm9iZS5hY3RpdmVUYXNrID0gYWN0aXZlPy5UYXNrOwogICAgICAgIHByb2JlLmFjdGl2ZUJvdW5kYXJ5QWN0aW9uID0gYWN0aXZlPy5Cb3VuZGFyeUFjdGlvbjsKCiAgICAgICAgZHluYW1pYyByZXN1bHQgPSBuZXcgRXhwYW5kb09iamVjdCgpOwogICAgICAgICgoSURpY3Rpb25hcnk8c3RyaW5nLCBvYmplY3Q+KXJlc3VsdClbc2xvdF0gPSBwcm9iZTsKICAgICAgICBMb2dJbmZvcm1hdGlvbigkIkViSW5jaWRlbnRQcm9iZU1hcHBpbmdbe3Nsb3R9XTogaGFzPXtwcm9iZS5oYXNBY3RpdmVJbmNpZGVudH0gYWN0aXZlPXtwcm9iZS5hY3RpdmVJbmNpZGVudFByZXNlbnR9IGxvYWRlZD17cHJvYmUuaW5jaWRlbnRzTG9hZGVkfSBjb3VudD17cHJvYmUudG90YWxJbmNpZGVudENvdW50fSIpOwogICAgICAgIHJldHVybiBUYXNrLkZyb21SZXN1bHQobmV3IFNjcmlwdFJlc3BvbnNlIHsgRGF0YSA9IHJlc3VsdCB9KTsKICAgIH0KfQo=" + } + } + ], "availableIn": [ "r-in-mid" ] diff --git a/core/Workflows/error-boundary-lab/error-boundary-lab-global.json b/core/Workflows/error-boundary-lab/error-boundary-lab-global.json index fd4718e..b9b8162 100644 --- a/core/Workflows/error-boundary-lab/error-boundary-lab-global.json +++ b/core/Workflows/error-boundary-lab/error-boundary-lab-global.json @@ -3,7 +3,7 @@ "flow": "sys-flows", "flowVersion": "1.0.0", "domain": "core", - "version": "1.1.0", + "version": "1.2.0", "tags": [ "integration-test", "error-boundary-lab", diff --git a/core/Workflows/error-boundary-lab/error-boundary-lab.json b/core/Workflows/error-boundary-lab/error-boundary-lab.json index 8ecbb01..302f185 100644 --- a/core/Workflows/error-boundary-lab/error-boundary-lab.json +++ b/core/Workflows/error-boundary-lab/error-boundary-lab.json @@ -3,7 +3,7 @@ "flow": "sys-flows", "flowVersion": "1.0.0", "domain": "core", - "version": "1.1.0", + "version": "1.2.0", "tags": [ "integration-test", "error-boundary-lab", diff --git a/core/Workflows/error-boundary-lab/src/EbIncidentProbeMapping.csx b/core/Workflows/error-boundary-lab/src/EbIncidentProbeMapping.csx new file mode 100644 index 0000000..dacb06d --- /dev/null +++ b/core/Workflows/error-boundary-lab/src/EbIncidentProbeMapping.csx @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Dynamic; +using System.Threading.Tasks; +using BBT.Workflow.Definitions; +using BBT.Workflow.Scripting; + +/// +/// Records what context.Incident shows to a mapping running inside the root's error-boundary +/// handling of a subflow fault (eb-sf-root: the r-has-error transition and the +/// r-error-end OnEntry). The slot prefix comes from the task key so one mapping serves both +/// slots. This is exactly what a domain developer reads to branch on the subflow's failure. +/// +public class EbIncidentProbeMapping : ScriptBase, IMapping +{ + public Task InputHandler(WorkflowTask task, ScriptContext context) + { + return Task.FromResult(new ScriptResponse()); + } + + public Task OutputHandler(ScriptContext context) + { + var slot = context.Instance?.CurrentState == "r-error-end" ? "probeEntry" : "probeTransition"; + var info = context.Incident; + var active = info?.ActiveIncident; + + dynamic probe = new ExpandoObject(); + probe.contextHasIncidentInfo = info != null; + probe.hasActiveIncident = info?.HasActiveIncident ?? false; + probe.incidentsLoaded = info?.IncidentsLoaded ?? false; + probe.totalIncidentCount = info?.TotalIncidentCount ?? -1; + probe.activeIncidentPresent = active != null; + probe.activeErrorCode = active?.ErrorCode; + probe.activeStatusCode = active?.StatusCode; + probe.activeTask = active?.Task; + probe.activeBoundaryAction = active?.BoundaryAction; + + dynamic result = new ExpandoObject(); + ((IDictionary)result)[slot] = probe; + LogInformation($"EbIncidentProbeMapping[{slot}]: has={probe.hasActiveIncident} active={probe.activeIncidentPresent} loaded={probe.incidentsLoaded} count={probe.totalIncidentCount}"); + return Task.FromResult(new ScriptResponse { Data = result }); + } +} diff --git a/tests/Core.IntegrationTests/Tests/ErrorBoundaryLab/README.md b/tests/Core.IntegrationTests/Tests/ErrorBoundaryLab/README.md index 496d478..e5b4144 100644 --- a/tests/Core.IntegrationTests/Tests/ErrorBoundaryLab/README.md +++ b/tests/Core.IntegrationTests/Tests/ErrorBoundaryLab/README.md @@ -146,6 +146,7 @@ kırmızıya döner — o zaman README, `TEST-SCENARIOS.md` ve testler birlikte | Bir abort **iki** incident yazıyordu | Boundary kendi verdict'ini yazıyor, pipeline fault'un kendisi için ikinci bir `Pipeline` katmanlı satır (`ErrorBoundaryAbort`) daha yazıyordu; faulted instance'ta `incident.active` bu ikinci satır olduğu için `boundaryAction` taşımıyordu | Üç task step'i (`RunOn{Execute,Entry,Exit}TasksStep`) incident'ı artık **save'den önce** kaydediyor, böylece `HasActiveIncident` bayrağı aynı save'le commit ediliyor ve `MarkInstanceFaultedAsync`'in yeniden yüklemesi fallback satırını atlıyor | | Yeniden fault eden retry `Active` olarak yerleşiyordu | Yanıt `"status":"F"` derken instance Active kalıyordu (ambient scope'ta tracked yüklenen aggregate, iç scope'un yazdığı F'i commit'te eziyor) ve instance bir daha retry edilemiyordu (`Instance:100027`) | `InstanceRetryAppService` aggregate'i **no-tracking** yüklüyor; unfault `IInstanceRepository.TryUnfaultAsync` ile tek `ExecuteUpdateAsync`'lik bir CAS. Whole-graph rewrite da böylece ortadan kalktı | | Blok incident içeriğini gömüyordu | State function en sıcak okuma yolunda incident tablosunu okuyordu, geçmiş endpoint'inin döndürdüğü veri kopyalanıyordu ve bir bayatlık deliği vardı: bir state'te A resolve edilip B açıldığında hiçbir fingerprint üyesi kımıldamadığı için `If-None-Match` ile doğrulayan client 304'te kalıp A'yı göstermeye devam ediyordu | Blok artık `{ hasActiveIncident, active: { href }, history: { href } }`; `ResponseShapeVersion` v9. State function ve instance GET'i hiç incident sorgusu atmıyor, liste görünümündeki sayfa başına batch sorgu tamamen kalktı, delik de kapandı | +| Script'te `context.Incident.ActiveIncident` hep `null`'dı | Root'un boundary transition'ındaki ve error-end OnEntry'sindeki mapping `hasActiveIncident: true`, `incidentsLoaded: true` görüyor ama `ActiveIncident = null`, `totalIncidentCount = 0` alıyordu; geliştirici alt akışın hatasına göre dallanamıyordu (preprod onboarding, 2026-10-02). Script context her zaman `Instance.CreateSnapshot()` tutuyor; snapshot incident'ları **detached** listeye koyuyor, `ScriptIncidentInfo` ise boş kalan EF navigation'ını okuyordu (#972'den, 0.0.93'ten beri) | `ScriptIncidentInfo.From(instance)` artık `GetLoadedIncidents()` okuyor (builder, `RefreshInstance`, paralel branch). `SubFlowFaultChainTests.AMappingInsideTheBoundaryHandling_SeesTheSubFlowIncident` (`eb-incident-probe-task` / `-2`) pinliyor | | Kurtulan instance hâlâ aktif incident bildiriyordu | Başarılı retry'dan sonra instance `landed`/`C` olmasına rağmen `hasActiveIncident` true kalıyordu; `HasActiveIncident` fingerprint materyali olduğu için long-poll eden client'a da yansıyordu | `Instance.ResolveActiveIncident` → `ResolveOpenIncidents`: yüklü kümedeki **tüm** açık satırları kapatır ve bayrağı yeniden hesaplar. Detached retry yolunda karşılığı `ResolveAllAsync` | ## Doğrulama durumu diff --git a/tests/Core.IntegrationTests/Tests/ErrorBoundaryLab/SubFlowFaultChainTests.cs b/tests/Core.IntegrationTests/Tests/ErrorBoundaryLab/SubFlowFaultChainTests.cs index a20cb78..2f4c696 100644 --- a/tests/Core.IntegrationTests/Tests/ErrorBoundaryLab/SubFlowFaultChainTests.cs +++ b/tests/Core.IntegrationTests/Tests/ErrorBoundaryLab/SubFlowFaultChainTests.cs @@ -75,6 +75,34 @@ public async Task TheRootRecordsTheSubFlowFault_AsAResolvedIncident() Assert.True(Flag(incident, "isResolved"), $"the root's incident is still open: {Summarize(incident)}"); } + /// + /// What a mapping running INSIDE the root's error-boundary handling reads from + /// context.Incident: the boundary transition's own task (probeTransition) and the + /// error end's OnEntry (probeEntry), both before FinalizeTransitionStep resolves + /// the incident. Before the fix both saw hasActiveIncident: true with no active incident + /// and a count of 0 — the script context read the snapshot's EF navigation, which is empty on a + /// snapshot. + /// + [Theory] + [InlineData("probeTransition")] + [InlineData("probeEntry")] + public async Task AMappingInsideTheBoundaryHandling_SeesTheSubFlowIncident(string slot) + { + var chain = await RunChainAsync(); + if (chain is null) return; + + var attributes = await GetAttributesAsync(Root, chain.Value.Root); + Assert.True(attributes.TryGetProperty(slot, out var probe), $"the {slot} mapping never ran: {attributes}"); + + Assert.True(Flag(probe, "hasActiveIncident"), $"{slot}: {probe}"); + Assert.True(Flag(probe, "activeIncidentPresent"), $"{slot} saw no active incident: {probe}"); + Assert.Equal(1, Number(probe, "totalIncidentCount")); + Assert.Equal("eb-http-400-task", Text(probe, "activeTask")); + Assert.Equal(400, Number(probe, "activeStatusCode")); + Assert.Equal("Notify", Text(probe, "activeBoundaryAction")); + Assert.EndsWith("Task:Http:eb-http-400-task:400", Text(probe, "activeErrorCode")); + } + [Fact] public async Task TheRootReportsNoActiveIncident_OnEverySurface() {