diff --git a/docs/explanation/architecture-resource-roadmap.md b/docs/explanation/architecture-resource-roadmap.md index 396b1b89..0ecff5a1 100644 --- a/docs/explanation/architecture-resource-roadmap.md +++ b/docs/explanation/architecture-resource-roadmap.md @@ -93,8 +93,9 @@ The first native scope uses exact versions and existing incarnation selectors; it does not silently extend responsibility to a version family or every member of a resource. Retained directory information and its history remain distinct from authentication identities and external directory synchronization. Native -schema, backend admission and typed queries, CLI/MCP authoring and Studio review -preserve these exact selections. The acceptance reference links executable +schema, backend admission and typed queries, and CLI/MCP authoring and inspection +preserve these exact selections. Studio entity Details does not expose +responsibility query controls. The acceptance reference links executable coverage and records its deployment and acceptance limits. ## Decision and Document contracts @@ -128,7 +129,8 @@ identity and version mappings, provenance and guarded reconciliation. Its same-store reapply from migration into a new store, where incarnation-bound selectors require explicit rebinding. The [source control API](../reference/source-control-api.md) describes the concrete -typed boundary and read-only review surface. +typed boundary and read-only CLI/MCP inspection queries. Studio entity Details +does not include a source control console. Remote connectors and additional format compilers remain separate milestones. ## Native system relationship layer diff --git a/docs/how-to/verify-connected-architecture.md b/docs/how-to/verify-connected-architecture.md index 15892003..56791cbb 100644 --- a/docs/how-to/verify-connected-architecture.md +++ b/docs/how-to/verify-connected-architecture.md @@ -106,8 +106,9 @@ Use `?focus=data_product:/receipts`, `?focus=document:/delivery-note` and `?focus=channel:/delivery` to inspect those resources. Confirm the receipt title is `Source receipt` and its local documentation remains intact. -Inspect exact port responsibility, accepted rationale, document content and -anchor, repeated observation selections and source run records. +Inspect accepted rationale, document content and anchor, and repeated observation +selections in Studio. Use exact CLI/MCP queries for port responsibility and source +run records; these controls are not part of entity Details. Capture screenshots, rendered text and API/console diagnostics with the exact model identity and candidate build evidence. The fixture intentionally exposes diff --git a/docs/reference/responsibility-acceptance.md b/docs/reference/responsibility-acceptance.md index f0c88ae7..8e31d00d 100644 --- a/docs/reference/responsibility-acceptance.md +++ b/docs/reference/responsibility-acceptance.md @@ -128,7 +128,7 @@ infer success from a diagnostic name. | CLI native authoring | Create/update/read/export/reapply Team/Person with preserved presence and returned identities; reject stale/unknown IDs and unsupported selectors through the service. No handwritten client identity rematching. Use the commands in the contract's typed-query table. | | MCP native authoring | Exercise equivalent native operations and typed queries over the MCP transport, preserving request guards, structured error classes and exact selectors. Use the tools in the contract's typed-query table. | | Native copy/delete/branch commands | Prove preview/apply, safe/force deletion, clone mappings, merge/conflict identity and unchanged dangling landing through public surfaces. | -| Studio review | Render backend assignments by full subject and kind; separate Team contacts from direct Person assignments. Show stale/inaccessible/incomplete states, historical context and exact member/port/occurrence identity without client-inferred ownership or authorization. | +| Studio Details | Keep assignment filters, membership/contact query controls and responsibility backend lookups out of entity Details in current and historical views. Use CLI/MCP typed queries for responsibility inspection. | | Connected browser | Execute review with authorized deployed service/browser context and retain exact result evidence. Component-only rendering checks do not establish deployed behavior. | Implementation acceptance requires a case-to-evidence record identifying actual @@ -148,7 +148,7 @@ readiness. | [Complete store reference guards](../../rsworkspace/crates/trogon-atlas-store/tests/reference_guards.rs) | Uncapped final-overlay scan suppresses replaced/deleted owners, includes newly created owners, checks branch state and untyped slice precedence, and rejects duplicate/mismatched overlay keys. | | [Manifest round trips](../../rsworkspace/crates/trogon-atlas-client/tests/responsibility_manifest.rs), [typed transport](../../rsworkspace/crates/trogon-atlas-client/tests/responsibility_transport.rs) and [CLI integration](../../rsworkspace/crates/trogon-atlas-cli/tests/responsibility_cli_integration.rs) | Typed profiles/selectors, native authoring/export/reapply, query metadata/status preservation, structured budget failures, malformed requests and identity-stripping/unknown-ID refusal. | | [MCP integration](../../rsworkspace/crates/trogon-atlas-mcp/tests/mcp_integration.rs) | Native Team/Person authoring and typed queries preserve actual IDs, scope, exact reapply values and structured errors through the MCP transport. | -| [Studio inspector](../../tsworkspace/webapps/studio/src/components/ResponsibilityInspector.test.tsx), [subject selectors](../../tsworkspace/webapps/studio/src/lib/responsibility.test.ts), [gateway requests](../../tsworkspace/webapps/studio/server/index.test.mjs) and [production codec contract](../../tsworkspace/webapps/studio/server/responsibility-contract.test.mjs) | Read-only exact subject selection, assignee/membership/contact presentation, stale/inaccessible/incomplete states and historical context. Real serializer-boundary cases reject malformed enum, boolean, oneof and nested values before normalization can change query intent, while preserving valid typed requests. Component/transport coverage does not establish deployed-browser behavior. | +| [Studio Details](../../tsworkspace/webapps/studio/src/components/Inspector.render.test.tsx), [subject selectors](../../tsworkspace/webapps/studio/src/lib/responsibility.test.ts), [gateway requests](../../tsworkspace/webapps/studio/server/index.test.mjs) and [production codec contract](../../tsworkspace/webapps/studio/server/responsibility-contract.test.mjs) | Current and historical entity Details omit responsibility controls and backend lookups. Retained subject helpers and gateway contracts preserve exact selectors; real serializer-boundary cases reject malformed enum, boolean, oneof and nested values before normalization can change query intent, while preserving valid typed requests. Gateway coverage does not imply an exposed responsibility review UI or deployed-browser behavior. | These focused tests establish the behavior they exercise, not exhaustive proof of every requirement row. Exhaustive entity/connection inventory permutations, diff --git a/docs/reference/source-control-api.md b/docs/reference/source-control-api.md index 54e69da9..83d5192f 100644 --- a/docs/reference/source-control-api.md +++ b/docs/reference/source-control-api.md @@ -37,10 +37,11 @@ inventory pointers. `preview-source-run --native-owner category=source-key=manifest-path` supplies a complete native owner from each explicit manifest, using the connected tenant type context. -Studio is a read-only review surface. Its source queries inspect current -branch-scoped control state; a historical model view does not hydrate current -source records as historical evidence. Inspecting a run summary does not fetch -all operation or fidelity pages automatically. +Inspect source control state through CLI or MCP. Studio entity Details does not +include a source control console. Source queries inspect current branch-scoped +control state; a historical model view does not make current source records +historical evidence. Inspecting a run summary does not fetch all operation or +fidelity pages automatically. ## Registration and immutable snapshots diff --git a/rsworkspace/Cargo.lock b/rsworkspace/Cargo.lock index 31c08bc9..1dc97eb9 100644 --- a/rsworkspace/Cargo.lock +++ b/rsworkspace/Cargo.lock @@ -3051,9 +3051,9 @@ dependencies = [ [[package]] name = "rmcp" -version = "3.8.0" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8be15815186873ea063bf005011d16c356804973489e40c39116ae3a2878d1f" +checksum = "d24a7e92f4bcaa36b97fa93c4fa7ca8f3e669eced949345b3462dba333c7f909" dependencies = [ "async-trait", "base64 0.23.1", @@ -3073,6 +3073,7 @@ dependencies = [ "schemars 1.2.3", "serde", "serde_json", + "sha2 0.11.1", "sse-stream", "thiserror", "tokio", @@ -3085,9 +3086,9 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "3.8.0" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b0748e054a5f5598b3213cf72099ae6b2745ba4d996c59ccf455150a7dbb839" +checksum = "b3140f24a766f80f9ec1607bde85680959a69b83d46fa9ce87dadab7e67ab029" dependencies = [ "darling 0.24.1", "proc-macro2", diff --git a/rsworkspace/crates/trogon-atlas-mcp/Cargo.toml b/rsworkspace/crates/trogon-atlas-mcp/Cargo.toml index a7ee669b..8ffbeab6 100644 --- a/rsworkspace/crates/trogon-atlas-mcp/Cargo.toml +++ b/rsworkspace/crates/trogon-atlas-mcp/Cargo.toml @@ -17,7 +17,7 @@ tokio = { workspace = true, features = ["net"] } serde = { workspace = true } serde_json = { workspace = true } schemars = "1" -rmcp = { version = "3", features = ["server", "transport-streamable-http-server"] } +rmcp = { version = "3.9.0", features = ["server", "transport-streamable-http-server"] } axum = { version = "0.8", default-features = false, features = ["http1", "tokio"] } http = "1" tracing = { workspace = true } @@ -38,7 +38,7 @@ tonic = { workspace = true } tower = "0.5" toml = "1.1" http = "1" -rmcp = { version = "3", features = ["server", "client", "transport-streamable-http-client-reqwest"] } +rmcp = { version = "3.9.0", features = ["server", "client", "transport-streamable-http-client-reqwest"] } [lints] workspace = true diff --git a/rsworkspace/crates/trogon-atlas-server/Cargo.toml b/rsworkspace/crates/trogon-atlas-server/Cargo.toml index 7913d172..430665d7 100644 --- a/rsworkspace/crates/trogon-atlas-server/Cargo.toml +++ b/rsworkspace/crates/trogon-atlas-server/Cargo.toml @@ -71,7 +71,7 @@ tokio = { workspace = true, features = ["test-util"] } proptest = "1" async-trait = { workspace = true } tempfile = { workspace = true } -rmcp = { version = "3", features = ["client", "transport-streamable-http-client-reqwest"] } +rmcp = { version = "3.9.0", features = ["client", "transport-streamable-http-client-reqwest"] } opentelemetry_sdk = { version = "0.33", features = ["testing"] } [lints] diff --git a/rsworkspace/crates/trogon-atlas-server/fuzz/Cargo.lock b/rsworkspace/crates/trogon-atlas-server/fuzz/Cargo.lock index e8edfb5c..35198fe8 100644 --- a/rsworkspace/crates/trogon-atlas-server/fuzz/Cargo.lock +++ b/rsworkspace/crates/trogon-atlas-server/fuzz/Cargo.lock @@ -103,9 +103,9 @@ dependencies = [ [[package]] name = "async-nats" -version = "0.50.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83a251fa1a4c9d0fe6e816b7acd60549e473e08d14f27a1d992c2675abff05f" +checksum = "a4f92759f418774b78c36ef07885849150ce950c67d9bcdeff8a9c253f57d9a1" dependencies = [ "base64 0.22.1", "bytes", @@ -578,6 +578,7 @@ dependencies = [ "fiat-crypto", "rustc_version", "subtle", + "zeroize", ] [[package]] @@ -707,6 +708,7 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ + "pkcs8", "signature", ] @@ -718,9 +720,11 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", + "serde", "sha2 0.10.9", "signature", "subtle", + "zeroize", ] [[package]] @@ -1083,7 +1087,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.10", + "webpki-roots", ] [[package]] @@ -1349,9 +1353,9 @@ checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" [[package]] name = "libc" -version = "0.2.190" +version = "0.2.191" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" [[package]] name = "libfuzzer-sys" @@ -1377,9 +1381,9 @@ checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "lock_api" -version = "0.4.14" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +checksum = "65d91b22da179613dcf4d6ec8349563f52a363f6849e03b00dd0a185ec4a59af" dependencies = [ "scopeguard", ] @@ -1722,9 +1726,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.5" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +checksum = "a9cf937ca4e1d0b7578784559063422fc39ae7c5a00f02471f2614d5486fd226" dependencies = [ "lock_api", "parking_lot_core", @@ -1732,11 +1736,10 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.12" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +checksum = "9cfff060068cc18aa7815010fcaf80c68c7fb9e85fc7f5c0f9bf142c21d19826" dependencies = [ - "cfg-if", "libc", "redox_syscall", "smallvec", @@ -2123,9 +2126,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.18" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +checksum = "580c875845b41ecb1564ad1ae4703044cef774145e3d7a94dc2b3fa7c97868fd" dependencies = [ "bitflags", ] @@ -2214,7 +2217,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.10", + "webpki-roots", ] [[package]] @@ -2264,9 +2267,9 @@ dependencies = [ [[package]] name = "rmcp" -version = "3.5.1" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5023804cbe0304cbe06197ac16e645a0a0d0ffa2953bf735c15c2fa0213350" +checksum = "d24a7e92f4bcaa36b97fa93c4fa7ca8f3e669eced949345b3462dba333c7f909" dependencies = [ "async-trait", "base64 0.23.1", @@ -2277,6 +2280,7 @@ dependencies = [ "http-body", "http-body-util", "indexmap", + "libc", "pastey", "pin-project-lite", "rand 0.10.3", @@ -2284,6 +2288,7 @@ dependencies = [ "schemars", "serde", "serde_json", + "sha2 0.11.1", "sse-stream", "thiserror", "tokio", @@ -2296,9 +2301,9 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "3.5.1" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1da498700030856ba63114bceefa30ba21f5e278cb975e5b9de3529fe842b40" +checksum = "b3140f24a766f80f9ec1607bde85680959a69b83d46fa9ce87dadab7e67ab029" dependencies = [ "darling", "proc-macro2", @@ -2592,6 +2597,12 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.9" @@ -2661,6 +2672,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "similar" version = "3.2.0" @@ -2687,9 +2704,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.16.3" +version = "1.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" +checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8" [[package]] name = "socket2" @@ -3088,9 +3105,9 @@ dependencies = [ [[package]] name = "tokio-websockets" -version = "0.10.1" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f591660438b3038dd04d16c938271c79e7e06260ad2ea2885a4861bfb238605d" +checksum = "d52efb639344a7c6adb8e62c6f3d2c19c001ff1b79a5041ba1c6ed42e19c6aa5" dependencies = [ "base64 0.22.1", "bytes", @@ -3098,13 +3115,13 @@ dependencies = [ "futures-sink", "http", "httparse", - "rand 0.8.8", - "ring", + "rand 0.10.3", "rustls-pki-types", + "sha1_smol", + "simdutf8", "tokio", "tokio-rustls", "tokio-util", - "webpki-roots 0.26.11", ] [[package]] @@ -3393,6 +3410,7 @@ name = "trogon-atlas-core" version = "0.1.0" dependencies = [ "chrono", + "ed25519-dalek", "prost", "prost-reflect", "prost-types", @@ -3419,6 +3437,7 @@ dependencies = [ "schemars", "serde", "serde_json", + "sha2 0.11.1", "thiserror", "tokio", "tonic", @@ -3458,6 +3477,7 @@ dependencies = [ "chacha20poly1305", "chrono", "clap", + "futures", "http", "http-body", "opentelemetry", @@ -3789,15 +3809,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.10", -] - [[package]] name = "webpki-roots" version = "1.0.10" diff --git a/tsworkspace/pnpm-lock.yaml b/tsworkspace/pnpm-lock.yaml index 4254b61c..992df185 100644 --- a/tsworkspace/pnpm-lock.yaml +++ b/tsworkspace/pnpm-lock.yaml @@ -83,6 +83,9 @@ importers: uuid: specifier: 14.0.3 version: 14.0.3 + zod: + specifier: 4.6.7 + version: 4.6.7 devDependencies: '@playwright/test': specifier: ^1.64.1 diff --git a/tsworkspace/webapps/studio/README.md b/tsworkspace/webapps/studio/README.md index f61b62b7..166ca632 100644 --- a/tsworkspace/webapps/studio/README.md +++ b/tsworkspace/webapps/studio/README.md @@ -19,6 +19,20 @@ swimlane below), rendered live from the gRPC store. was taken and does not update. It holds only the board's read responses and the runtime from `build:snapshot`. +## Feature Preview + +Open a model's **More actions** menu and choose **Feature Preview**. Select +**Lane mesh** or **Stream towers** to see its preview and turn its switch on. +Both start off. Each preference applies immediately and is saved in your +browser for Studio at the same address. + +Each preview uses the same 3D view as its model tab, with fictional example +data. Drag to rotate the preview, right-drag to pan, or scroll to zoom. + +Lane mesh appears when the model has slices. Stream towers appears when a +swimlane holds events. Disabling the current view returns to the Board. +Saved HTML snapshots do not include these views. + ## Architecture The browser cannot speak tonic gRPC, so `server/index.mjs` is a thin Node diff --git a/tsworkspace/webapps/studio/e2e/export-html.spec.ts b/tsworkspace/webapps/studio/e2e/export-html.spec.ts index f9e1a685..9b59de50 100644 --- a/tsworkspace/webapps/studio/e2e/export-html.spec.ts +++ b/tsworkspace/webapps/studio/e2e/export-html.spec.ts @@ -32,7 +32,7 @@ test('Export HTML saves a board that renders from disk with no network', async ( }); await offline.goto(`file://${file}`); - await expect(offline.getByRole('heading', { name: 'Event Model Studio', level: 1 })).toBeVisible(); + await expect(offline.getByRole('heading', { name: 'Atlas', level: 1 })).toBeVisible(); await expect(offline.getByText(eventTitle)).toBeVisible(); await expect(offline.getByRole('status', { name: 'Snapshot' })).toContainText('baseline'); await expect(offline.getByRole('button', { name: 'More actions' })).toHaveCount(0); diff --git a/tsworkspace/webapps/studio/e2e/overview-to-board.spec.ts b/tsworkspace/webapps/studio/e2e/overview-to-board.spec.ts index a00d1bea..4c00aa61 100644 --- a/tsworkspace/webapps/studio/e2e/overview-to-board.spec.ts +++ b/tsworkspace/webapps/studio/e2e/overview-to-board.spec.ts @@ -70,7 +70,7 @@ test.describe('Overview to board flow', () => { await page.getByRole('link', { name: /order service/i }).click(); // The TopBar heading confirms we are on the EventModel page. - await expect(page.getByRole('heading', { name: 'Event Model Studio', level: 1 })).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Atlas', level: 1 })).toBeVisible(); // The scoped title in the TopBar shows the model name. await expect(page.getByText('Order Service', { exact: true }).first()).toBeVisible(); diff --git a/tsworkspace/webapps/studio/e2e/smoke.spec.ts b/tsworkspace/webapps/studio/e2e/smoke.spec.ts index fe3bba6f..b1a44784 100644 --- a/tsworkspace/webapps/studio/e2e/smoke.spec.ts +++ b/tsworkspace/webapps/studio/e2e/smoke.spec.ts @@ -51,7 +51,7 @@ test.describe('Real-stack smoke', () => { failOnConsoleErrors(page, errors); await page.goto(EM_PATH); - await expect(page.getByRole('heading', { name: 'Event Model Studio', level: 1 })).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Atlas', level: 1 })).toBeVisible(); // The one Event member of the fixture EventModel renders as a sticky // with its title visible on the board. await expect(page.getByText('Order Placed')).toBeVisible(); @@ -61,7 +61,7 @@ test.describe('Real-stack smoke', () => { test('(c) ?branch= names the branch on the switcher with the read-only hint', async ({ page }) => { await page.goto(`${EM_PATH}?branch=${encodeURIComponent(FIXTURE_BRANCH.name)}`); - await expect(page.getByRole('heading', { name: 'Event Model Studio', level: 1 })).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Atlas', level: 1 })).toBeVisible(); // The switcher itself carries the branch name (font-mono span inside BranchBar). await expect(page.getByText(FIXTURE_BRANCH.name, { exact: true })).toBeVisible(); diff --git a/tsworkspace/webapps/studio/e2e/validation-details.spec.ts b/tsworkspace/webapps/studio/e2e/validation-details.spec.ts index 8bf100c5..330808a3 100644 --- a/tsworkspace/webapps/studio/e2e/validation-details.spec.ts +++ b/tsworkspace/webapps/studio/e2e/validation-details.spec.ts @@ -84,6 +84,75 @@ test('validation details stay reachable with an inspector on a narrow screen', a await expect(panel).not.toBeVisible(); }); +test('the canvas toolbar leaves the validation drawer header unobstructed on a narrow screen', async ({ page }) => { + await page.setViewportSize({ width: 480, height: 844 }); + await page.goto(modelPath); + await page.getByRole('button', { name: 'Validation findings', exact: true }).click(); + const panel = page.getByRole('complementary', { name: 'Validation findings', exact: true }); + const heading = panel.getByRole('heading', { name: 'Validation findings', exact: true }); + const lastView = page.getByRole('tablist', { name: 'Model view' }).getByRole('tab').last(); + for (const width of [480, 640, 720]) { + await page.setViewportSize({ width, height: 844 }); + await expect(heading).toBeVisible(); + await expect + .poll(() => + heading.evaluate((element) => { + const bounds = element.getBoundingClientRect(); + const hit = document.elementFromPoint(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2); + return hit === element || element.contains(hit); + }), + ) + .toBe(true); + const viewBounds = await lastView.boundingBox(); + const triggerBounds = await page.getByRole('button', { name: 'Validation findings', exact: true }).boundingBox(); + if (!viewBounds || !triggerBounds) throw new Error('Toolbar controls have no visible bounds'); + const overlap = + viewBounds.x < triggerBounds.x + triggerBounds.width && + viewBounds.x + viewBounds.width > triggerBounds.x && + viewBounds.y < triggerBounds.y + triggerBounds.height && + viewBounds.y + viewBounds.height > triggerBounds.y; + expect(overlap).toBe(false); + await lastView.click(); + await expect(lastView).toHaveAttribute('aria-selected', 'true'); + } + await panel.getByRole('button', { name: 'Close validation findings' }).click(); + await expect(panel).not.toBeVisible(); +}); + +test('the canvas remains draggable between toolbar controls', async ({ page }) => { + await page.setViewportSize({ width: 1280, height: 844 }); + await page.addInitScript((path) => { + sessionStorage.setItem(`trogon-atlas-studio:viewport:${path}:board`, JSON.stringify({ x: 0, y: 200, zoom: 1 })); + }, modelPath); + await page.goto(modelPath); + const tabs = page.getByRole('tablist', { name: 'Model view' }); + const trigger = page.getByRole('button', { name: 'Validation findings', exact: true }); + await expect(trigger).toBeVisible(); + const tabsBounds = await tabs.boundingBox(); + const triggerBounds = await trigger.boundingBox(); + if (!tabsBounds || !triggerBounds) throw new Error('Toolbar controls have no visible bounds'); + const point = { + x: (tabsBounds.x + tabsBounds.width + triggerBounds.x) / 2, + y: triggerBounds.y + triggerBounds.height / 2, + }; + await expect + .poll(() => + page.locator('.react-flow').evaluate((element, position) => { + const hit = document.elementFromPoint(position.x, position.y); + return hit !== null && element.contains(hit); + }, point), + ) + .toBe(true); + const viewport = page.locator('.react-flow__viewport'); + const before = await viewport.getAttribute('style'); + if (before === null) throw new Error('Canvas viewport has no transform'); + await page.mouse.move(point.x, point.y); + await page.mouse.down(); + await page.mouse.move(point.x + 60, point.y + 80, { steps: 5 }); + await page.mouse.up(); + await expect(viewport).not.toHaveAttribute('style', before); +}); + test('finding jumps reduce excessive zoom and recenter again after panning away', async ({ page }) => { await page.addInitScript((path) => { sessionStorage.setItem(`trogon-atlas-studio:viewport:${path}:board`, JSON.stringify({ x: 30, y: 40, zoom: 1.8 })); diff --git a/tsworkspace/webapps/studio/package.json b/tsworkspace/webapps/studio/package.json index c7f37966..59af540a 100644 --- a/tsworkspace/webapps/studio/package.json +++ b/tsworkspace/webapps/studio/package.json @@ -36,7 +36,8 @@ "react-dom": "19.3.1", "tailwind-merge": "3.7.0", "three": "0.187.0", - "uuid": "14.0.3" + "uuid": "14.0.3", + "zod": "4.6.7" }, "devDependencies": { "@playwright/test": "^1.64.1", diff --git a/tsworkspace/webapps/studio/src/components/ConflictInspector.tsx b/tsworkspace/webapps/studio/src/components/ConflictInspector.tsx index 4e92b087..6c1b48dc 100644 --- a/tsworkspace/webapps/studio/src/components/ConflictInspector.tsx +++ b/tsworkspace/webapps/studio/src/components/ConflictInspector.tsx @@ -83,7 +83,7 @@ export function ConflictInspector({ const selectedStatus = selected ? branchEntryStatus(selected.status) : undefined; return ( -