diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBrowser/HyperTwistBrowserBridgeObject.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBrowser/HyperTwistBrowserBridgeObject.cpp index ad3b714..69c2dd9 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBrowser/HyperTwistBrowserBridgeObject.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBrowser/HyperTwistBrowserBridgeObject.cpp @@ -36,6 +36,7 @@ void UHyperTwistBrowserBridgeObject::NotifyState(const FString& StateJson) void UHyperTwistBrowserBridgeObject::NotifyRuntimeReady(const FString& RuntimeReadyJson) { LastRuntimeReadyJson = RuntimeReadyJson; + OnRuntimeReady.Broadcast(RuntimeReadyJson); } void UHyperTwistBrowserBridgeObject::ResetReceivedMessages() diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBrowser/HyperTwistBrowserWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBrowser/HyperTwistBrowserWidget.cpp index 4a0db4c..4671049 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBrowser/HyperTwistBrowserWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBrowser/HyperTwistBrowserWidget.cpp @@ -26,6 +26,7 @@ void UHyperTwistBrowserWidget::LoadBundledBrowserShell() InitialUrl = ResolveBundledBrowserShellUrl(); if (BrowserWidget.IsValid()) { + ResetBrowserRuntimeHandshake(); BrowserWidget->LoadURL(InitialUrl); } } @@ -36,47 +37,47 @@ void UHyperTwistBrowserWidget::LoadBrowserUrl(const FString& NewUrl) InitialUrl = NewUrl; if (BrowserWidget.IsValid() && !NewUrl.IsEmpty()) { + ResetBrowserRuntimeHandshake(); BrowserWidget->LoadURL(NewUrl); } } void UHyperTwistBrowserWidget::DispatchCommandJson(const FString& CommandJson) { - if (!BrowserWidget.IsValid() || CommandJson.IsEmpty()) + if (CommandJson.IsEmpty()) { return; } - const FString EscapedJson = EscapeForJavaScriptSingleQuotedString(CommandJson); - BrowserWidget->ExecuteJavascript(FString::Printf( - TEXT("window.HyperTwistBrowserRuntime && window.HyperTwistBrowserRuntime.receiveCommand(JSON.parse('%s'));"), - *EscapedJson - )); + if (!IsBrowserRuntimeDispatchReady()) + { + QueueCommandJson(CommandJson); + return; + } + + ExecuteBrowserRuntimeScript(BuildCommandDispatchScript(CommandJson)); } void UHyperTwistBrowserWidget::PushShellStateJson(const FString& StateJson) { - if (!BrowserWidget.IsValid() || StateJson.IsEmpty()) + if (StateJson.IsEmpty()) { return; } - const FString EscapedJson = EscapeForJavaScriptSingleQuotedString(StateJson); - BrowserWidget->ExecuteJavascript(FString::Printf( - TEXT("window.HyperTwistBrowserRuntime && window.HyperTwistBrowserRuntime.setShellState(JSON.parse('%s'));"), - *EscapedJson - )); + if (!IsBrowserRuntimeDispatchReady()) + { + QueueShellStateJson(StateJson); + return; + } + + ExecuteBrowserRuntimeScript(BuildShellStateDispatchScript(StateJson)); } bool UHyperTwistBrowserWidget::PushTilingStateEnvelope( const FHyperTwistTrainingTilingStateEnvelope& StateEnvelope ) { - if (!BrowserWidget.IsValid()) - { - return false; - } - FString StateJson; if (!UHyperTwistTrainingTilingLibrary::TrySerializeStateEnvelopeToJson( StateEnvelope, @@ -104,6 +105,21 @@ FString UHyperTwistBrowserWidget::GetLastStateJson() const return BrowserBridgeObject != nullptr ? BrowserBridgeObject->LastStateJson : FString(); } +int32 UHyperTwistBrowserWidget::GetPendingCommandCount() const +{ + return PendingCommandJsonQueue.Num(); +} + +bool UHyperTwistBrowserWidget::HasPendingShellStateJson() const +{ + return bHasPendingShellStateJson; +} + +bool UHyperTwistBrowserWidget::IsBrowserRuntimeReady() const +{ + return bBrowserRuntimeReady; +} + bool UHyperTwistBrowserWidget::TryGetLastTilingEnvelope( FHyperTwistTrainingTilingStateEnvelope& OutStateEnvelope ) const @@ -142,9 +158,20 @@ TSharedRef UHyperTwistBrowserWidget::RebuildWidget() } else { + if (RuntimeReadyDelegateHandle.IsValid()) + { + BrowserBridgeObject->OnRuntimeReady.Remove(RuntimeReadyDelegateHandle); + RuntimeReadyDelegateHandle.Reset(); + } BrowserBridgeObject->ResetReceivedMessages(); } + RuntimeReadyDelegateHandle = BrowserBridgeObject->OnRuntimeReady.AddUObject( + this, + &UHyperTwistBrowserWidget::HandleBrowserRuntimeReady + ); + ResetBrowserRuntimeHandshake(); + BrowserWidget = SNew(SWebBrowser) .InitialURL(ResolveInitialUrl()) @@ -158,6 +185,12 @@ void UHyperTwistBrowserWidget::ReleaseSlateResources(const bool bReleaseChildren { Super::ReleaseSlateResources(bReleaseChildren); + if (BrowserBridgeObject != nullptr && RuntimeReadyDelegateHandle.IsValid()) + { + BrowserBridgeObject->OnRuntimeReady.Remove(RuntimeReadyDelegateHandle); + RuntimeReadyDelegateHandle.Reset(); + } + if (BrowserWidget.IsValid() && BrowserBridgeObject != nullptr) { BrowserWidget->UnbindUObject(TEXT("hypertwist"), BrowserBridgeObject, true); @@ -176,11 +209,112 @@ void UHyperTwistBrowserWidget::SynchronizeProperties() const FString ResolvedUrl = ResolveInitialUrl(); if (!ResolvedUrl.IsEmpty()) { + ResetBrowserRuntimeHandshake(); BrowserWidget->LoadURL(ResolvedUrl); } } } +bool UHyperTwistBrowserWidget::IsBrowserRuntimeDispatchReady() const +{ +#if WITH_AUTOMATION_TESTS + if (bForceBrowserRuntimeDispatchReadyForTesting && !BrowserWidget.IsValid()) + { + return bBrowserRuntimeReady; + } +#endif + + return BrowserWidget.IsValid() && bBrowserRuntimeReady; +} + +void UHyperTwistBrowserWidget::ExecuteBrowserRuntimeScript(const FString& Script) +{ +#if WITH_AUTOMATION_TESTS + if (bForceBrowserRuntimeDispatchReadyForTesting && !BrowserWidget.IsValid()) + { + ExecutedBrowserScriptsForTesting.Add(Script); + return; + } +#endif + + if (BrowserWidget.IsValid()) + { + BrowserWidget->ExecuteJavascript(Script); + } +} + +void UHyperTwistBrowserWidget::HandleBrowserRuntimeReady(const FString& RuntimeReadyJson) +{ + if (RuntimeReadyJson.IsEmpty()) + { + return; + } + + bBrowserRuntimeReady = true; + FlushPendingBrowserMessages(); +} + +void UHyperTwistBrowserWidget::FlushPendingBrowserMessages() +{ + if (!IsBrowserRuntimeDispatchReady()) + { + return; + } + + if (bHasPendingShellStateJson) + { + ExecuteBrowserRuntimeScript(BuildShellStateDispatchScript(PendingShellStateJson)); + PendingShellStateJson.Reset(); + bHasPendingShellStateJson = false; + } + + for (const FString& CommandJson : PendingCommandJsonQueue) + { + ExecuteBrowserRuntimeScript(BuildCommandDispatchScript(CommandJson)); + } + PendingCommandJsonQueue.Reset(); +} + +void UHyperTwistBrowserWidget::QueueCommandJson(const FString& CommandJson) +{ + if (!CommandJson.IsEmpty()) + { + PendingCommandJsonQueue.Add(CommandJson); + } +} + +void UHyperTwistBrowserWidget::QueueShellStateJson(const FString& StateJson) +{ + if (!StateJson.IsEmpty()) + { + PendingShellStateJson = StateJson; + bHasPendingShellStateJson = true; + } +} + +void UHyperTwistBrowserWidget::ResetBrowserRuntimeHandshake() +{ + bBrowserRuntimeReady = false; +} + +FString UHyperTwistBrowserWidget::BuildCommandDispatchScript(const FString& CommandJson) const +{ + const FString EscapedJson = EscapeForJavaScriptSingleQuotedString(CommandJson); + return FString::Printf( + TEXT("window.HyperTwistBrowserRuntime && window.HyperTwistBrowserRuntime.receiveCommand(JSON.parse('%s'));"), + *EscapedJson + ); +} + +FString UHyperTwistBrowserWidget::BuildShellStateDispatchScript(const FString& StateJson) const +{ + const FString EscapedJson = EscapeForJavaScriptSingleQuotedString(StateJson); + return FString::Printf( + TEXT("window.HyperTwistBrowserRuntime && window.HyperTwistBrowserRuntime.setShellState(JSON.parse('%s'));"), + *EscapedJson + ); +} + FString UHyperTwistBrowserWidget::EscapeForJavaScriptSingleQuotedString(const FString& Input) { FString Escaped = Input; @@ -206,3 +340,21 @@ void UHyperTwistBrowserWidget::EnsureBridgeBound() BrowserWidget->BindUObject(TEXT("hypertwist"), BrowserBridgeObject, true); } } + +#if WITH_AUTOMATION_TESTS +void UHyperTwistBrowserWidget::SetBrowserRuntimeDispatchReadyForTesting(const bool bInDispatchReady) +{ + bForceBrowserRuntimeDispatchReadyForTesting = bInDispatchReady; + ExecutedBrowserScriptsForTesting.Reset(); +} + +void UHyperTwistBrowserWidget::SimulateBrowserRuntimeReadyForTesting(const FString& RuntimeReadyJson) +{ + HandleBrowserRuntimeReady(RuntimeReadyJson); +} + +const TArray& UHyperTwistBrowserWidget::GetExecutedBrowserScriptsForTesting() const +{ + return ExecutedBrowserScriptsForTesting; +} +#endif diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBrowser/HyperTwistBrowserBridgeObject.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBrowser/HyperTwistBrowserBridgeObject.h index 133d443..dab67d9 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBrowser/HyperTwistBrowserBridgeObject.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBrowser/HyperTwistBrowserBridgeObject.h @@ -5,6 +5,8 @@ #include "UObject/Object.h" #include "HyperTwistBrowserBridgeObject.generated.h" +DECLARE_MULTICAST_DELEGATE_OneParam(FHyperTwistBrowserRuntimeReadyDelegate, const FString&); + UCLASS(BlueprintType) class UNREALHYPERTWIST_API UHyperTwistBrowserBridgeObject : public UObject { @@ -42,6 +44,8 @@ public: UPROPERTY(BlueprintReadOnly, Category = "HyperTwist|Browser") FString LastRuntimeReadyJson; + FHyperTwistBrowserRuntimeReadyDelegate OnRuntimeReady; + UPROPERTY(Transient) FHyperTwistTrainingTilingStateEnvelope LastTilingEnvelope; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBrowser/HyperTwistBrowserWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBrowser/HyperTwistBrowserWidget.h index 2022a59..3a63aad 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBrowser/HyperTwistBrowserWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBrowser/HyperTwistBrowserWidget.h @@ -47,6 +47,15 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Browser") FString GetLastStateJson() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Browser") + int32 GetPendingCommandCount() const; + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Browser") + bool HasPendingShellStateJson() const; + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Browser") + bool IsBrowserRuntimeReady() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Browser") bool TryGetLastTilingEnvelope( FHyperTwistTrainingTilingStateEnvelope& OutStateEnvelope @@ -67,8 +76,17 @@ protected: virtual TSharedRef RebuildWidget() override; virtual void ReleaseSlateResources(bool bReleaseChildren) override; virtual void SynchronizeProperties() override; + virtual bool IsBrowserRuntimeDispatchReady() const; + virtual void ExecuteBrowserRuntimeScript(const FString& Script); private: + void HandleBrowserRuntimeReady(const FString& RuntimeReadyJson); + void FlushPendingBrowserMessages(); + void QueueCommandJson(const FString& CommandJson); + void QueueShellStateJson(const FString& StateJson); + void ResetBrowserRuntimeHandshake(); + FString BuildCommandDispatchScript(const FString& CommandJson) const; + FString BuildShellStateDispatchScript(const FString& StateJson) const; static FString EscapeForJavaScriptSingleQuotedString(const FString& Input); FString ResolveInitialUrl() const; void EnsureBridgeBound(); @@ -76,5 +94,29 @@ private: UPROPERTY(Transient) TObjectPtr BrowserBridgeObject = nullptr; + UPROPERTY(Transient) + TArray PendingCommandJsonQueue; + + UPROPERTY(Transient) + FString PendingShellStateJson; + + UPROPERTY(Transient) + bool bHasPendingShellStateJson = false; + + UPROPERTY(Transient) + bool bBrowserRuntimeReady = false; + TSharedPtr BrowserWidget; + FDelegateHandle RuntimeReadyDelegateHandle; + +#if WITH_AUTOMATION_TESTS +public: + void SetBrowserRuntimeDispatchReadyForTesting(bool bInDispatchReady); + void SimulateBrowserRuntimeReadyForTesting(const FString& RuntimeReadyJson); + const TArray& GetExecutedBrowserScriptsForTesting() const; + +private: + bool bForceBrowserRuntimeDispatchReadyForTesting = false; + TArray ExecutedBrowserScriptsForTesting; +#endif }; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistBrowserBridgeObjectTest.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistBrowserBridgeObjectTest.cpp index a002ff4..7ac81e0 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistBrowserBridgeObjectTest.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistBrowserBridgeObjectTest.cpp @@ -84,6 +84,53 @@ bool FHyperTwistBrowserBridgeObjectNotificationTest::RunTest(const FString& Para return true; } +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FHyperTwistBrowserBridgeObjectRuntimeReadyDelegateTest, + "HyperTwist.Browser.Bridge.RuntimeReadyDelegate", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter +) + +bool FHyperTwistBrowserBridgeObjectRuntimeReadyDelegateTest::RunTest(const FString& Parameters) +{ + UHyperTwistBrowserBridgeObject* BridgeObject = NewObject(); + TestNotNull(TEXT("The browser bridge object must be constructible."), BridgeObject); + if (BridgeObject == nullptr) + { + return false; + } + + int32 RuntimeReadyBroadcastCount = 0; + FString LastRuntimeReadyPayload; + const FDelegateHandle DelegateHandle = BridgeObject->OnRuntimeReady.AddLambda( + [&RuntimeReadyBroadcastCount, &LastRuntimeReadyPayload](const FString& RuntimeReadyJson) + { + ++RuntimeReadyBroadcastCount; + LastRuntimeReadyPayload = RuntimeReadyJson; + } + ); + + BridgeObject->NotifyRuntimeReady(TEXT("{\"status\":\"ready\",\"mode\":\"fallback-js\"}")); + + TestEqual( + TEXT("Runtime-ready notification must be retained on the bridge object."), + BridgeObject->LastRuntimeReadyJson, + TEXT("{\"status\":\"ready\",\"mode\":\"fallback-js\"}") + ); + TestEqual( + TEXT("Runtime-ready notification must broadcast once to bridge listeners."), + RuntimeReadyBroadcastCount, + 1 + ); + TestEqual( + TEXT("Runtime-ready notification must preserve the broadcast payload."), + LastRuntimeReadyPayload, + TEXT("{\"status\":\"ready\",\"mode\":\"fallback-js\"}") + ); + + BridgeObject->OnRuntimeReady.Remove(DelegateHandle); + return true; +} + IMPLEMENT_SIMPLE_AUTOMATION_TEST( FHyperTwistBrowserBridgeObjectTilingStateEnvelopeDecodeTest, "HyperTwist.Browser.Bridge.TilingStateEnvelopeDecode", @@ -155,6 +202,81 @@ bool FHyperTwistBrowserBridgeObjectTilingStateEnvelopeDecodeTest::RunTest( return true; } +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FHyperTwistBrowserWidgetQueuedOutboundMessagesTest, + "HyperTwist.Browser.Widget.QueuedOutboundMessages", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter +) + +bool FHyperTwistBrowserWidgetQueuedOutboundMessagesTest::RunTest(const FString& Parameters) +{ + UHyperTwistBrowserWidget* BrowserWidget = NewObject(); + TestNotNull(TEXT("The browser widget must be constructible."), BrowserWidget); + if (BrowserWidget == nullptr) + { + return false; + } + + BrowserWidget->DispatchCommandJson(TEXT("{\"command\":\"alpha\"}")); + BrowserWidget->PushShellStateJson(TEXT("{\"phase\":\"7A\"}")); + BrowserWidget->DispatchCommandJson(TEXT("{\"command\":\"beta\"}")); + + TestEqual( + TEXT("Command payloads must queue while the browser runtime is not ready."), + BrowserWidget->GetPendingCommandCount(), + 2 + ); + TestTrue( + TEXT("The last shell-state payload must stay queued while the browser runtime is not ready."), + BrowserWidget->HasPendingShellStateJson() + ); + TestFalse( + TEXT("A fresh browser widget must begin with runtime-ready false."), + BrowserWidget->IsBrowserRuntimeReady() + ); + + BrowserWidget->SetBrowserRuntimeDispatchReadyForTesting(true); + BrowserWidget->SimulateBrowserRuntimeReadyForTesting(TEXT("{\"status\":\"ready\"}")); + + TestTrue( + TEXT("Runtime-ready simulation must mark the browser widget ready."), + BrowserWidget->IsBrowserRuntimeReady() + ); + TestEqual( + TEXT("Queued command payloads must flush when runtime-ready arrives."), + BrowserWidget->GetPendingCommandCount(), + 0 + ); + TestFalse( + TEXT("Queued shell-state payload must flush when runtime-ready arrives."), + BrowserWidget->HasPendingShellStateJson() + ); + + const TArray& ExecutedScripts = BrowserWidget->GetExecutedBrowserScriptsForTesting(); + TestEqual( + TEXT("Flushing the queued outbound payloads must execute one shell-state script plus both queued commands."), + ExecutedScripts.Num(), + 3 + ); + if (ExecutedScripts.Num() == 3) + { + TestTrue( + TEXT("Shell-state must flush before queued commands so the browser runtime sees the latest state first."), + ExecutedScripts[0].Contains(TEXT("setShellState")) + ); + TestTrue( + TEXT("The first queued command must flush through receiveCommand."), + ExecutedScripts[1].Contains(TEXT("receiveCommand")) + ); + TestTrue( + TEXT("The second queued command must also flush through receiveCommand."), + ExecutedScripts[2].Contains(TEXT("receiveCommand")) + ); + } + + return true; +} + IMPLEMENT_SIMPLE_AUTOMATION_TEST( FHyperTwistBrowserWidgetBundledShellUrlTest, "HyperTwist.Browser.Widget.BundledShellUrl", diff --git a/docs/arch/HYPERTWIST_PHASE8A_EMBEDDED_BROWSER_RUNTIME_HARDENING_IMPLEMENTATION_PACKET_2026-06-19.md b/docs/arch/HYPERTWIST_PHASE8A_EMBEDDED_BROWSER_RUNTIME_HARDENING_IMPLEMENTATION_PACKET_2026-06-19.md new file mode 100644 index 0000000..dd7c99c --- /dev/null +++ b/docs/arch/HYPERTWIST_PHASE8A_EMBEDDED_BROWSER_RUNTIME_HARDENING_IMPLEMENTATION_PACKET_2026-06-19.md @@ -0,0 +1,107 @@ +# HyperTwist Phase 8A embedded browser runtime hardening implementation packet + +Created on `2026-06-19` + +## Status + +- first-party HyperTwist packet +- bounded first-party `Phase 8A` continuation slice + +## Purpose + +This packet hardens the already-landed embedded browser shipping lane without +widening into a browser-first topology change or any native renderer branch. + +The landed slice is: + +- queue outbound Unreal command payloads until the embedded browser runtime + reports ready +- retain the latest outbound shell-state payload until runtime-ready, then + flush that shell state before queued commands +- broadcast browser runtime-ready notifications through the first-party bridge + object so the widget can complete a deterministic handshake +- add focused automation proving the queue, flush order, and runtime-ready + bridge signal + +It is not: + +- a full-browser client implementation +- a new browser authority inversion over Unreal runtime state +- a native `MagicTile` renderer widening packet + +## Current authority basis + +This implementation packet stands on: + +- `docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md` +- `docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md` +- `docs/v6_5_deep_manual_pack/HyperTwist/ARCHITECTURE.md` +- `docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md` +- `docs/arch/HYPERTWIST_PHASE7C_MAGICTILE_NATIVE_BEHAVIOR_PROOF_IMPLEMENTATION_PACKET_2026-06-19.md` +- `docs/arch/HYPERTWIST_PHASE7D_MAGICTILE_RENDERER_PORT_DECISION_PACKET_2026-06-19.md` +- `docs/arch/HYPERTWIST_PHASE8B_OPTIONAL_FULL_BROWSER_CLIENT_HTTP_BACKEND_PREPARATION_PACKET_2026-06-19.md` + +## Landed scope + +Current code now hardens the live embedded-browser lane through: + +- `UHyperTwistBrowserWidget` outbound queue ownership for: + - pending command JSON payloads + - latest pending shell-state JSON payload + - runtime-ready handshake state +- first-party flush ordering that applies shell state before queued commands + once the browser runtime is ready +- `UHyperTwistBrowserBridgeObject` runtime-ready delegate broadcast +- focused automation in `HyperTwistBrowserBridgeObjectTest.cpp` for: + - runtime-ready delegate retention/broadcast + - queued outbound browser messages and flush ordering + +## Why this hardening mattered + +The earlier browser-runtime bootstrap already queued messages once the browser +JavaScript runtime existed. The remaining deterministic gap was earlier in the +handshake: Unreal could still try to send command or shell-state payloads +before the embedded widget/runtime bridge was actually ready to receive them. + +This packet closes that gap on the Unreal side while preserving the already +landed first-party browser shell and bridge topology. + +## Validation + +Windows build validation: + +- `Build.bat UnrealHyperTwistEditor Win64 Development -Project='C:\HyperTwist_worktrees\phase10validate\UnrealHyperTwist\UnrealHyperTwist.uproject' -WaitMutex -NoHotReloadFromIDE -NoUba` +- result on `2026-06-19`: `Result: Succeeded` with UnrealBuildTool + `Total execution time: 96.96 seconds` on isolated worktree + `C:\HyperTwist_worktrees\phase10validate` + +Focused browser validation: + +- `Automation RunTests HyperTwist.Browser` +- result on `2026-06-19`: exported + `Saved\AutomationReports\Browser-Hardening-Verify` with all `6` + `HyperTwist.Browser.*` tests passing, including: + - `Bridge.RuntimeReadyDelegate` + - `Widget.QueuedOutboundMessages` + +Adjacent regression validation: + +- `Automation RunTests HyperTwist.FirstParty.MagicTile` +- `Automation RunTests HyperTwist.Permissive.MagicTile` +- result on `2026-06-19`: exported + `Saved\AutomationReports\Phase7-MagicTile-BrowserHardening` with all `7` + `HyperTwist.FirstParty.MagicTile.*` tests passing and + `Saved\AutomationReports\Phase7-Permissive-MagicTile-BrowserHardening` with + all `6` `HyperTwist.Permissive.MagicTile.*` tests passing + +## Queue effect + +This packet keeps the live embedded browser/CEF shipping lane intact, but it +raises its determinism and test coverage: + +- Unreal-side browser startup is now tolerant of early outbound traffic before + runtime-ready +- browser bridge readiness is now a first-party signal rather than a silent + assumption +- current browser/MagicTile validation grows from earlier green proof into a + stronger shipping-lane hardening packet without reopening renderer decisions diff --git a/docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md b/docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md index 66fff1d..00bf44a 100644 --- a/docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md +++ b/docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md @@ -420,6 +420,9 @@ Closure read: - [x] Add `WebBrowserWidget` plugin to `.uproject` - [x] Create `UHyperTwistBrowserWidget` wrapping `SWebBrowser` - [x] Load the authoritative local browser shell in `Content/Browser/index.html`, with bundled-runtime upgrade plus clean-checkout plain-JS fallback +- [x] Queue outbound Unreal command and shell-state payloads until the embedded browser runtime reports ready, then flush shell state before queued commands +- Hardening continuation on `2026-06-19`: current code now keeps the live embedded browser lane deterministic above the already-landed shell/bootstrap path by retaining pending widget outbound traffic until the first-party runtime-ready bridge fires, then flushing the latest shell-state payload before queued commands through the browser runtime +- Validation evidence on `2026-06-19`: the primary reverse-SSH `localhost:22022` lane rebuilt isolated worktree `C:\HyperTwist_worktrees\phase10validate` with `Result: Succeeded` and UnrealBuildTool `Total execution time: 96.96 seconds`, then exported green automation reports for `6` `HyperTwist.Browser.*` tests at `Saved\AutomationReports\Browser-Hardening-Verify`, `7` `HyperTwist.FirstParty.MagicTile.*` tests at `Saved\AutomationReports\Phase7-MagicTile-BrowserHardening`, and `6` `HyperTwist.Permissive.MagicTile.*` tests at `Saved\AutomationReports\Phase7-Permissive-MagicTile-BrowserHardening` ### 8B — State Synchronization - [x] Bridge UE cube state to browser via embedded Unreal bridge object plus JavaScript `postMessage` fallback diff --git a/docs/v6_5_deep_manual_pack/HyperTwist/ARCHITECTURE.md b/docs/v6_5_deep_manual_pack/HyperTwist/ARCHITECTURE.md index 09d7db0..6530463 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/ARCHITECTURE.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/ARCHITECTURE.md @@ -94,6 +94,11 @@ Current shipping browser posture remains the landed embedded browser/CEF shell inside Unreal through `UHyperTwistBrowserWidget` and the authoritative `Content/Browser/index.html` surface. +That live shipping lane was further hardened on `2026-06-19`: Unreal-side +outbound command and shell-state payloads now queue until the first-party +browser runtime-ready bridge arrives, then flush shell state before queued +commands so the embedded browser lane stays deterministic across startup. + The optional full-browser client path now exists only as a spec-defined first-party branch: diff --git a/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md b/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md index 5ac35be..80af231 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md @@ -144,7 +144,7 @@ Do not collapse those three tiers into one undifferentiated "features" voice. | First-party provider-neutral custom-endpoint runtime routing | Implemented now | landed first-party `Phase 6R-AI` | First-party per-session provider-profile endpoint selection, runtime routing, fallback-health reflection, and bounded provider-backed transport-failure posture are now live above the landed provider-profile/BYOK and provider-routing seams. Provider-specific overlays, payment execution, provider-portal ownership, and payload shipping remain deferred. | | Analytics/reporting surfaces | Implemented now | landed analytics/reporting packets | Reporting is real, but bounded to accepted retained slices. | | Rewritten training analytics and report reference grounding | Implemented now | `apache/echarts` retained permissive lane + first-party current code | Current live `Training Analytics` reference side includes four rewritten first-party targets grounded in retained `apache/echarts`: session outcome and progress reporting, analytics data-view and export, timing-trend history and overview interaction, and the optional richer explainer or sidecar boundary. This does not displace the landed `Phase 3R-C` first-party analytics/reporting owner or elevate `ecomfe/echarts-gl` and `ecomfe/zrender` beyond support-only sidecars. | -| Browser/spatial/media adjunct surfaces | Implemented now | landed `three.js`, `react-three-fiber`, `xr`, `model-viewer`, `remotion` packets | These are implemented bounded families, and the current Phase 1 browser-runtime landing now includes a first-party authoritative `Content/Browser/index.html` shell, bundled-runtime upgrade path, committed clean-checkout plain-JS fallback runtime, and embedded `UHyperTwistBrowserWidget` bridge. This still is not proof of unlimited browser-shell parity. | +| Browser/spatial/media adjunct surfaces | Implemented now | landed `three.js`, `react-three-fiber`, `xr`, `model-viewer`, `remotion` packets | These are implemented bounded families, and the current Phase 1 browser-runtime landing now includes a first-party authoritative `Content/Browser/index.html` shell, bundled-runtime upgrade path, committed clean-checkout plain-JS fallback runtime, embedded `UHyperTwistBrowserWidget` bridge, and the `2026-06-19` runtime-ready queue hardening that retains outbound Unreal shell traffic until the browser runtime is ready. This still is not proof of unlimited browser-shell parity. | | Optional full-browser client path above Unreal backend authority | Deep-source grounded retained | landed first-party `Phase 8B` preparation packet | This branch is now defined only as a spec-only first-party option: a browser-owned rendering and input shell may sit above explicit Unreal backend seams while Unreal remains authoritative for puzzle, training, timer, replay, and persistence state. The landed embedded browser/CEF shell remains the current shipping posture, and this branch stays separate from any native `MagicTile` behavior or renderer widening. | | Rewritten browser spatial scene and renderer reference grounding | Implemented now | `mrdoob/three.js` retained permissive lane + first-party current code | Current live `Browser 3D and XR Support` reference side includes one rewritten first-party target grounded in retained `mrdoob/three.js`: the browser spatial scene and renderer contract. This does not displace the landed `Phase 3R-F` first-party browser spatial owner trio or absorb the adjacent `react-three-fiber` renderer-bridge and `xr` session slices. | | Rewritten React-side browser renderer and event-bridge grounding | Implemented now | `pmndrs/react-three-fiber` retained permissive lane + first-party current code | Current live `Browser 3D and XR Support` reference side includes one rewritten first-party target grounded in retained `react-three-fiber`: the React scene renderer and event bridge. This does not displace the landed `Phase 3R-F` first-party browser spatial owner trio or absorb the adjacent `three.js` scene substrate and `xr` session slices. | diff --git a/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md b/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md index 57522a0..62b8eeb 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md @@ -107,6 +107,12 @@ Current consolidated milestone snapshot: scored against the gate and kept native renderer widening at explicit `No-Go` because no real browser-host failure or bounded renderer target is currently evidenced +- the current embedded browser shipping lane was also hardened on `2026-06-19` + so Unreal-side outbound browser traffic now queues until runtime-ready, + flushes shell state before commands, and remains live-validated on primary + reverse-SSH lane `localhost:22022` with `6` `HyperTwist.Browser.*`, `7` + `HyperTwist.FirstParty.MagicTile.*`, and `6` + `HyperTwist.Permissive.MagicTile.*` tests green - classic-cube `Phase 9A` replay recording is now closed through first-party runtime capture, `.json` replay persistence, local playback reconstruction, schema-light replay normalization across save/load/viewer import, and live