diff --git a/client/Packages/com.beamable/CHANGELOG.md b/client/Packages/com.beamable/CHANGELOG.md index dba40a5952..3467f35b49 100644 --- a/client/Packages/com.beamable/CHANGELOG.md +++ b/client/Packages/com.beamable/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Content fields with attribute `MustReferenceContent` will have display content picker inspector in Editor +- Added a QA Smoke Kit sample (internal smoke-test harness). ### Fixed diff --git a/client/Packages/com.beamable/Samples/QASmokeKit.meta b/client/Packages/com.beamable/Samples/QASmokeKit.meta new file mode 100644 index 0000000000..52490b43d5 --- /dev/null +++ b/client/Packages/com.beamable/Samples/QASmokeKit.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f60718293a4b5c6d7e8f90a1b2c3d4e5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Samples/QASmokeKit/QASmokeKit.unity b/client/Packages/com.beamable/Samples/QASmokeKit/QASmokeKit.unity new file mode 100644 index 0000000000..eb8261288e --- /dev/null +++ b/client/Packages/com.beamable/Samples/QASmokeKit/QASmokeKit.unity @@ -0,0 +1,169 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 3 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 0 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 2 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &519420028 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 519420029} + - component: {fileID: 519420030} + m_Layer: 0 + m_Name: QA Smoke Runner + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &519420029 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 519420028} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &519420030 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 519420028} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a1b2c3d4e5f60718293a4b5c6d7e8f90, type: 3} + m_Name: + m_EditorClassIdentifier: diff --git a/client/Packages/com.beamable/Samples/QASmokeKit/QASmokeKit.unity.meta b/client/Packages/com.beamable/Samples/QASmokeKit/QASmokeKit.unity.meta new file mode 100644 index 0000000000..cdc75923b4 --- /dev/null +++ b/client/Packages/com.beamable/Samples/QASmokeKit/QASmokeKit.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e5f60718293a4b5c6d7e8f90a1b2c3d4 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Samples/QASmokeKit/README.md b/client/Packages/com.beamable/Samples/QASmokeKit/README.md new file mode 100644 index 0000000000..5814ca47d1 --- /dev/null +++ b/client/Packages/com.beamable/Samples/QASmokeKit/README.md @@ -0,0 +1,98 @@ +# QA Smoke Kit + +A fast smoke-test harness bundled with the Beamable Unity SDK. It exercises the most +load-bearing SDK surfaces — `BeamContext` readiness, `Accounts.OnReady` resilience, the +`BeamContext` retry buffer, and the `MustReferenceContent` content picker — so a tester can +confirm a release behaves correctly in a few minutes. + +Because the kit ships inside `com.beamable`, it tracks the code it tests: when the SDK +changes, the harness imported alongside it stays in sync. + +> Always run against a disposable QA realm with no real player or content data. Several of +> the recipes below plant bad tokens and force failures. + +## How to read results + +Every line the harness emits is prefixed with `[SMOKE]`, and each check logs either +`[SMOKE] PASS ...` or `[SMOKE] FAIL ...`. Read the verdicts straight from the Unity Editor +log rather than relaying the Console by hand: + +``` +grep "\[SMOKE\]" +``` + +The Editor log lives at the platform-standard path (for example +`~/Library/Logs/Unity/Editor.log` on macOS). + +## The scripts + +A play-mode component does nothing until it lives on an active GameObject in the loaded +scene. The included `QASmokeKit.unity` scene is pre-wired with a single GameObject named +**QA Smoke Runner** that already has `SmokeRunner` attached — open it and press Play. + +### `SmokeRunner.cs` + +The happy-path readiness harness. Attach it to a GameObject and press Play (or just open the +included scene). It awaits `BeamContext.Default.OnReady`, logs the resolved +`cid` / `pid` / `playerId`, then awaits `Accounts.OnReady`. Touches only the most stable SDK +surface, so it compiles against the current release without edits. Extend the marked section +with stat / inventory / microservice assertions as needed. + +### `SmokeRetryWatcher.cs` + +A passive log watcher for fault-injection recipe B. While attached, it listens for +exceptions or errors during `BeamContext` init and emits `[SMOKE] FAIL` if it sees an +`IndexOutOfRangeException` or an overflow. + +### `SmokeReferenceContent.cs` + +Defines a `smoke_ref` content type that gives the `MustReferenceContent` picker an obvious, +top-level test surface. See the content-picker test below. + +## Fault-injection recipe A — `Accounts.OnReady` stale-token resilience + +`Accounts.OnReady` must resolve even when a stale or invalid **remembered** device token is +present, while still strictly rejecting an invalid **active** token. The relevant code is in +`Runtime/Player/PlayerAccounts.cs`. A happy-path pass will not exercise this — you must +plant a bad remembered token. + +1. Sign in normally so the SDK persists a remembered token. It is stored in `PlayerPrefs` + under a key of the form `{prefix}{cid}.{pid}.access_token` (and a matching `...refresh` + key). +2. Plant a bad remembered token: overwrite that `PlayerPrefs` entry with garbage, or sign in + a second account and then invalidate its server-side refresh token. +3. Re-init: restart the Editor, or re-enter Play with `SmokeRunner` attached. +4. **PASS** = `[SMOKE] PASS Accounts.OnReady resolved` appears while a genuinely bad + **active** token is still rejected. **FAIL** = `OnReady` hangs or throws. + +## Fault-injection recipe B — `BeamContext` retry-buffer overflow + +Pre-fix, `BeamContext.Try()` wrote `errors[attempt]` into a buffer sized to +`CoreConfiguration.ContextRetryDelays.Length`. With `EnableInfiniteContextRetries = true`, +once `attempt` passed the array length the buffer overflowed into an +`IndexOutOfRangeException`. The fix clamps the index to the last slot. The relevant code is +in `Runtime/BeamContext.cs`. + +1. In the Core Configuration asset (Beamable > Core Configuration, or the SDK settings + window): set `EnableInfiniteContextRetries = true` and shorten `ContextRetryDelays` to + e.g. `[1, 1]` so the array is exhausted in seconds. +2. Induce failure: turn off networking, or point config defaults at an unreachable host, so + init keeps failing. +3. Attach `SmokeRetryWatcher` to a GameObject, press Play, and let it retry well past the + array length (a dozen or more attempts). +4. **PASS** = retries keep cycling, the error buffer stays bounded, and no `[SMOKE] FAIL` + line appears. **FAIL** = an `IndexOutOfRangeException` surfaces during init. + +## Content-picker test — `MustReferenceContent` + +Once `SmokeReferenceContent.cs` compiles, the Content Manager offers a new `smoke_ref` +content type. Create one and inspect its two fields: + +- **`currency`** (`CurrencyRef`, `[MustReferenceContent]`) — the Editor shows a content + picker. Confirm the dropdown filters to `CurrencyContent` (e.g. the default + `currency.gems` / `currency.coins`). Test a valid pick, a cleared pick, and confirm + wrong-type content is not offered. +- **`currencyId`** (`string`, `[MustReferenceContent(true, typeof(CurrencyContent))]`) — type + a bogus id such as `currency.does_not_exist`. The invalid value must be **flagged** by + validation and **not** silently auto-rewritten or cleared. Confirm the typed text stays + put. diff --git a/client/Packages/com.beamable/Samples/QASmokeKit/README.md.meta b/client/Packages/com.beamable/Samples/QASmokeKit/README.md.meta new file mode 100644 index 0000000000..ecd716a7fd --- /dev/null +++ b/client/Packages/com.beamable/Samples/QASmokeKit/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d4e5f60718293a4b5c6d7e8f90a1b2c3 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Samples/QASmokeKit/SmokeReferenceContent.cs b/client/Packages/com.beamable/Samples/QASmokeKit/SmokeReferenceContent.cs new file mode 100644 index 0000000000..a9586bd6c2 --- /dev/null +++ b/client/Packages/com.beamable/Samples/QASmokeKit/SmokeReferenceContent.cs @@ -0,0 +1,35 @@ +using Beamable.Common.Content; +using Beamable.Common.Content.Validation; +using Beamable.Common.Inventory; +using UnityEngine; + +// Staged by the agent to give the 5.1.1 MustReferenceContent picker an obvious, +// top-level test surface (the built-in CurrencyContent only carries the attribute on a +// field buried inside a nested CurrencyReward list). +// +// After this compiles, the Content Manager should offer a new "smoke_ref" content type. +// Create one and inspect it: +// +// currency (CurrencyRef) -> [MustReferenceContent] picker. Expect a dropdown that +// filters to CurrencyContent (currency.gems / currency.coins). +// Test: valid pick, clear the pick, confirm wrong types are +// not offered. +// +// currencyId (string) -> [MustReferenceContent] on a string field. Type a bogus id +// (e.g. "currency.does_not_exist"). 5.1.1 change: the invalid +// value must be FLAGGED by validation and NOT silently +// auto-rewritten/cleared. Confirm your typed text stays put. +namespace BeamQA +{ + [ContentType("smoke_ref")] + public class SmokeReferenceContent : ContentObject + { + [Tooltip("Picker should filter to CurrencyContent.")] + [MustReferenceContent] + public CurrencyRef currency; + + [Tooltip("Invalid ids should be flagged, not auto-rewritten.")] + [MustReferenceContent(true, typeof(CurrencyContent))] + public string currencyId; + } +} diff --git a/client/Packages/com.beamable/Samples/QASmokeKit/SmokeReferenceContent.cs.meta b/client/Packages/com.beamable/Samples/QASmokeKit/SmokeReferenceContent.cs.meta new file mode 100644 index 0000000000..61d332834f --- /dev/null +++ b/client/Packages/com.beamable/Samples/QASmokeKit/SmokeReferenceContent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c3d4e5f60718293a4b5c6d7e8f90a1b2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Samples/QASmokeKit/SmokeRetryWatcher.cs b/client/Packages/com.beamable/Samples/QASmokeKit/SmokeRetryWatcher.cs new file mode 100644 index 0000000000..f69a2fb0d4 --- /dev/null +++ b/client/Packages/com.beamable/Samples/QASmokeKit/SmokeRetryWatcher.cs @@ -0,0 +1,38 @@ +using System; +using UnityEngine; + +// Fault-injection watcher for the 5.1.1 BeamContext retry-buffer fix. +// +// Pre-fix, BeamContext.Try() wrote errors[attempt] into a buffer sized to +// CoreConfiguration.ContextRetryDelays.Length. With EnableInfiniteContextRetries = true, +// once `attempt` passed the array length it overflowed -> IndexOutOfRangeException. +// The fix clamps the index to the last slot. +// +// HUMAN setup (Beamable > Core Configuration asset, or the SDK settings window): +// 1. EnableInfiniteContextRetries = true +// 2. Shorten ContextRetryDelays to e.g. [1, 1] so the array is exhausted in seconds. +// 3. Induce failure: turn off networking, or point config-defaults.txt at an +// unreachable host, so InitProcedure() keeps failing. +// 4. Attach this script to a GameObject and press Play. Let it retry well past the +// array length (a dozen+ attempts). +// +// PASS -> no [SMOKE] FAIL line below; retries keep cycling, error buffer stays bounded. +// FAIL -> an IndexOutOfRangeException (or "overflow") surfaces during init. +public class SmokeRetryWatcher : MonoBehaviour +{ + void OnEnable() => Application.logMessageReceived += OnLog; + void OnDisable() => Application.logMessageReceived -= OnLog; + + void Start() => Debug.Log("[SMOKE] SmokeRetryWatcher armed: watching init retries for buffer overflow"); + + void OnLog(string condition, string stackTrace, LogType type) + { + if (type != LogType.Exception && type != LogType.Error) return; + var text = condition + "\n" + stackTrace; + if (text.IndexOf("IndexOutOfRange", StringComparison.OrdinalIgnoreCase) >= 0 || + text.IndexOf("overflow", StringComparison.OrdinalIgnoreCase) >= 0) + { + Debug.LogError($"[SMOKE] FAIL retry-buffer overflow during BeamContext init: {condition}"); + } + } +} diff --git a/client/Packages/com.beamable/Samples/QASmokeKit/SmokeRetryWatcher.cs.meta b/client/Packages/com.beamable/Samples/QASmokeKit/SmokeRetryWatcher.cs.meta new file mode 100644 index 0000000000..95e0ddd4d6 --- /dev/null +++ b/client/Packages/com.beamable/Samples/QASmokeKit/SmokeRetryWatcher.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b2c3d4e5f60718293a4b5c6d7e8f90a1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Samples/QASmokeKit/SmokeRunner.cs b/client/Packages/com.beamable/Samples/QASmokeKit/SmokeRunner.cs new file mode 100644 index 0000000000..299b4b8eb7 --- /dev/null +++ b/client/Packages/com.beamable/Samples/QASmokeKit/SmokeRunner.cs @@ -0,0 +1,37 @@ +using System; +using Beamable; +using UnityEngine; + +// Staged by the coding agent; driven by a human (attach to a GameObject, press Play). +// All output is prefixed [SMOKE] so the agent can grep ~/Library/Logs/Unity/Editor.log +// for results without anyone relaying the Console by hand. +// +// This is the happy-path readiness harness. It deliberately touches only the most +// stable SDK surface so it compiles against 5.1.1 without edits. Extend the marked +// section with stat / inventory / microservice assertions once your service exists +// (see SMOKE_TEST_NOTES.md "Assertion snippets"). +public class SmokeRunner : MonoBehaviour +{ + async void Start() + { + Debug.Log("[SMOKE] SmokeRunner starting"); + try + { + var ctx = BeamContext.Default; + await ctx.OnReady; + Debug.Log($"[SMOKE] PASS BeamContext ready. cid={ctx.Cid} pid={ctx.Pid} player={ctx.PlayerId}"); + + // 5.1.1 regression guard (#4694): Accounts.OnReady must resolve even when a + // stale/invalid *remembered* device token is present. With a clean store this + // just confirms the path is intact; see the fault-injection recipe to make it bite. + await ctx.Accounts.OnReady; + Debug.Log("[SMOKE] PASS Accounts.OnReady resolved"); + + // ---- extend here: stats / inventory / microservice assertions ---- + } + catch (Exception e) + { + Debug.LogError($"[SMOKE] FAIL init: {e.GetType().Name}: {e.Message}"); + } + } +} diff --git a/client/Packages/com.beamable/Samples/QASmokeKit/SmokeRunner.cs.meta b/client/Packages/com.beamable/Samples/QASmokeKit/SmokeRunner.cs.meta new file mode 100644 index 0000000000..6c8f910058 --- /dev/null +++ b/client/Packages/com.beamable/Samples/QASmokeKit/SmokeRunner.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a1b2c3d4e5f60718293a4b5c6d7e8f90 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/package.json b/client/Packages/com.beamable/package.json index 985d210cef..a1f92c05c9 100644 --- a/client/Packages/com.beamable/package.json +++ b/client/Packages/com.beamable/package.json @@ -77,6 +77,14 @@ "texturePath": "Packages/com.beamable/Editor/UI/Common/Icons/BeamableWebIcons/CloudDataToMicroServices_Violet@3x.png", "docsUrl": "https://help.beamable.com/Unity-Latest/unity/samples/lightbeam-cloud/", "relativeMainAssetPath": "Sample_CloudSaving.unity" + }, + { + "displayName": "QA Smoke Kit", + "description": "Internal QA harness: BeamContext readiness, Accounts.OnReady and BeamContext retry-buffer fault checks, and a MustReferenceContent test content type. See the included README for the smoke-test procedure.", + "path": "SAMPLES_PATH/QASmokeKit", + "subText": "qa", + "texturePath": "Packages/com.beamable/Editor/UI/Common/Icons/BeamableWebIcons/CloudDataToMicroServices_Violet@3x.png", + "relativeMainAssetPath": "QASmokeKit.unity" } ] }