|
|
|
@@ -0,0 +1,348 @@
|
|
|
|
|
# Monaco Script Editor (Roslyn-backed IntelliSense)
|
|
|
|
|
|
|
|
|
|
The script editor gives AdminUI users a first-class C# authoring experience for
|
|
|
|
|
virtual-tag scripts: completions, diagnostics, hover, signature help, document
|
|
|
|
|
formatting, and context-aware tag-path completions — all backed by the same
|
|
|
|
|
Roslyn compiler the runtime publish gate uses.
|
|
|
|
|
|
|
|
|
|
See the design doc for the original rationale and brainstorming record:
|
|
|
|
|
[`docs/plans/2026-06-09-monaco-script-editor-design.md`](plans/2026-06-09-monaco-script-editor-design.md).
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Overview
|
|
|
|
|
|
|
|
|
|
Virtual-tag scripts are C# expression bodies (see [VirtualTags.md](VirtualTags.md))
|
|
|
|
|
compiled at publish time by `ScriptEvaluator` against a restricted `ScriptSandbox`.
|
|
|
|
|
Before this feature the AdminUI's script page used a CDN Monaco instance wired
|
|
|
|
|
over a hidden textarea via an `eval` + `Task.Delay(50)` race — no IntelliSense,
|
|
|
|
|
fragile in air-gapped environments, and the editor accepted code that publish
|
|
|
|
|
would reject.
|
|
|
|
|
|
|
|
|
|
The replacement:
|
|
|
|
|
|
|
|
|
|
- **Vendored Monaco** served locally — no CDN, air-gap safe.
|
|
|
|
|
- **Reusable `MonacoEditor.razor`** component wired into both the ScriptEdit page
|
|
|
|
|
(`/scripts/{id}`) and the virtual-tag inline script panel in the UNS TagModal.
|
|
|
|
|
- **Full IntelliSense** via a Roslyn backend that analyses the user's source inside
|
|
|
|
|
the *exact* evaluator wrapper `ScriptEvaluator` compiles, so what the editor
|
|
|
|
|
accepts/rejects matches publish exactly.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Architecture
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
┌──────────────────────────── AdminUI (Blazor Server) ───────────────────────────┐
|
|
|
|
|
│ │
|
|
|
|
|
│ Components/Shared/MonacoEditor.razor ◄── Value/ValueChanged, Height, │
|
|
|
|
|
│ │ ReadOnly, ShowToolbar, theme/Format/wrap/minimap toolbar │
|
|
|
|
|
│ │ (DotNetObjectReference both ways) │
|
|
|
|
|
│ ▼ │
|
|
|
|
|
│ wwwroot/js/monaco-init.js ──registers──► completion / hover / signatureHelp / │
|
|
|
|
|
│ │ diagnostics / format / inlayHints │
|
|
|
|
|
│ │ fetch POST /api/script-analysis/* │
|
|
|
|
|
│ ▼ │
|
|
|
|
|
│ ScriptAnalysis/ScriptAnalysisEndpoints.cs (minimal-API group, FleetAdmin) │
|
|
|
|
|
│ ▼ │
|
|
|
|
|
│ ScriptAnalysis/ScriptAnalysisService.cs ── Roslyn over OtOpcUa's real wrapper │
|
|
|
|
|
│ ├─ ScriptSandbox.Build(typeof(VirtualTagContext)) (imports + refs) │
|
|
|
|
|
│ ├─ CompiledScript.Run(ScriptGlobals<VirtualTagContext>) wrapper │
|
|
|
|
|
│ ├─ ForbiddenTypeAnalyzer (sandbox-escape squiggles) │
|
|
|
|
|
│ ├─ DependencyExtractor (dynamic-path squiggles) │
|
|
|
|
|
│ └─ IScriptTagCatalog (tag-path string-literal completions) │
|
|
|
|
|
│ ▼ │
|
|
|
|
|
│ wwwroot/lib/monaco/vs/… (vendored Monaco, loaded by monaco-init.js) │
|
|
|
|
|
└─────────────────────────────────────────────────────────────────────────────────┘
|
|
|
|
|
│ references
|
|
|
|
|
▼
|
|
|
|
|
Core.Scripting + Scripting.Abstractions (ScriptSandbox, VirtualTagContext,
|
|
|
|
|
ScriptGlobals<>, ForbiddenTypeAnalyzer, DependencyExtractor)
|
|
|
|
|
Configuration (OtOpcUaConfigDbContext → tag / virtual-tag paths for catalog)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### MonacoEditor.razor
|
|
|
|
|
|
|
|
|
|
`src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/MonacoEditor.razor`
|
|
|
|
|
|
|
|
|
|
A reusable Blazor Server component. Key parameters:
|
|
|
|
|
|
|
|
|
|
| Parameter | Type | Notes |
|
|
|
|
|
|-----------|------|-------|
|
|
|
|
|
| `Value` | `string` | Two-way bound script source |
|
|
|
|
|
| `ValueChanged` | `EventCallback<string>` | Fires on every model change |
|
|
|
|
|
| `Height` | `string` | CSS height (default `400px`) |
|
|
|
|
|
| `ReadOnly` | `bool` | Disables editing; IntelliSense still works |
|
|
|
|
|
| `ShowToolbar` | `bool` | Shows the theme/Format/wrap/minimap toolbar |
|
|
|
|
|
|
|
|
|
|
The component creates the Monaco editor in `OnAfterRenderAsync`, registers a
|
|
|
|
|
`DotNetObjectReference` so JS can call back into Blazor on value change, and
|
|
|
|
|
uses a GUID-keyed container so multiple instances on the same page coexist safely.
|
|
|
|
|
External `Value` changes are pushed to the editor via `window.MonacoBlazor.setValue`.
|
|
|
|
|
|
|
|
|
|
### monaco-init.js
|
|
|
|
|
|
|
|
|
|
`src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/wwwroot/js/monaco-init.js`
|
|
|
|
|
|
|
|
|
|
Exposes `window.MonacoBlazor` with `createEditor / setValue / getValue /
|
|
|
|
|
setMarkers / setEditorOption / format / dispose`. Registers the six Monaco
|
|
|
|
|
language providers for the `csharp` language at load time; each provider POSTs
|
|
|
|
|
to the corresponding `/api/script-analysis/*` endpoint. Diagnostics are
|
|
|
|
|
debounced (~500 ms) to avoid firing on every keystroke.
|
|
|
|
|
|
|
|
|
|
### ScriptAnalysisService
|
|
|
|
|
|
|
|
|
|
`src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/ScriptAnalysisService.cs`
|
|
|
|
|
|
|
|
|
|
The analysis seam is the private `Analyze(userSource)` method, which:
|
|
|
|
|
|
|
|
|
|
1. Builds the wrapped document — the user's source is appended after the
|
|
|
|
|
OtOpcUa evaluator preamble (usings from `ScriptSandbox.Build`, the
|
|
|
|
|
`CompiledScript.Run(ScriptGlobals<VirtualTagContext>)` wrapper, `var ctx =
|
|
|
|
|
globals.ctx;`, then `#line 1` so Roslyn reports user-source coordinates for
|
|
|
|
|
diagnostics). Return type is `object` so one shared script is valid regardless
|
|
|
|
|
of any virtual tag's `DataType`.
|
|
|
|
|
2. Parses the wrapped document as a `CSharpSyntaxTree` and creates a
|
|
|
|
|
`CSharpCompilation` against the `ScriptSandbox` reference set.
|
|
|
|
|
3. Returns the `SemanticModel`, `SyntaxTree`, and the preamble byte offset (used
|
|
|
|
|
by every capability to map editor coordinates to wrapped-document offsets).
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## HTTP Endpoints
|
|
|
|
|
|
|
|
|
|
All six endpoints live under the `/api/script-analysis` group, registered by
|
|
|
|
|
`ScriptAnalysisEndpoints.MapScriptAnalysisEndpoints()` (called from
|
|
|
|
|
`EndpointRouteBuilderExtensions.AddAdminUI` / `Host/Program.cs`) and gated by
|
|
|
|
|
the `FleetAdmin` authorization policy (requires the `Administrator` role).
|
|
|
|
|
|
|
|
|
|
| Route | Request DTO | Response DTO | Purpose |
|
|
|
|
|
|-------|-------------|--------------|---------|
|
|
|
|
|
| `POST /api/script-analysis/diagnostics` | `DiagnoseRequest(Code)` | `DiagnoseResponse(Markers)` | Roslyn + ForbiddenType + dynamic-path error markers |
|
|
|
|
|
| `POST /api/script-analysis/completions` | `CompletionsRequest(CodeText, Line, Column)` | `CompletionsResponse(Items)` | Scope, dot-member, and tag-path completions |
|
|
|
|
|
| `POST /api/script-analysis/hover` | `HoverRequest(CodeText, Line, Column)` | `HoverResponse(Markdown?)` | Type + XML doc markdown |
|
|
|
|
|
| `POST /api/script-analysis/signature-help` | `SignatureHelpRequest(CodeText, Line, Column)` | `SignatureHelpResponse(Label?, Parameters?, ActiveParameter)` | Parameter help |
|
|
|
|
|
| `POST /api/script-analysis/format` | `FormatRequest(Code)` | `FormatResponse(Code)` | `NormalizeWhitespace()` formatting |
|
|
|
|
|
| `POST /api/script-analysis/inlay-hints` | `InlayHintsRequest(Code)` | `InlayHintsResponse(Hints)` | Inline type hints (stub — returns empty) |
|
|
|
|
|
|
|
|
|
|
DTO definitions live in `ScriptAnalysis/ScriptAnalysisContracts.cs`.
|
|
|
|
|
|
|
|
|
|
`DiagnosticMarker` fields: `Severity` (Monaco values: 8 = error, 4 = warning,
|
|
|
|
|
2 = info, 1 = hint), `StartLineNumber`, `StartColumn`, `EndLineNumber`,
|
|
|
|
|
`EndColumn`, `Message`, `Code`.
|
|
|
|
|
|
|
|
|
|
`CompletionItem` fields: `Label`, `InsertText`, `Detail`, `Kind` (Monaco kind
|
|
|
|
|
string: `"Method"`, `"Property"`, `"Field"`, `"Variable"`, `"Class"`, etc.),
|
|
|
|
|
`InsertTextRules` (0 for plain text, 4 for snippet).
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Diagnostics Composition
|
|
|
|
|
|
|
|
|
|
The diagnostics endpoint combines three sources and returns **errors only**:
|
|
|
|
|
|
|
|
|
|
### 1. Roslyn compile diagnostics
|
|
|
|
|
|
|
|
|
|
`compilation.GetDiagnostics()` filtered to `DiagnosticSeverity.Error`. Warnings
|
|
|
|
|
are intentionally excluded: the canonical passthrough pattern
|
|
|
|
|
`return ctx.GetTag("X").Value;` triggers nullable warning CS8605 under
|
|
|
|
|
`NullableContextOptions.Enable` but compiles and publishes fine — flagging it in
|
|
|
|
|
the editor would be misleading noise that does not reflect publish reality.
|
|
|
|
|
|
|
|
|
|
Line numbers are read from `GetMappedLineSpan()` (not `GetLineSpan()`).
|
|
|
|
|
`GetMappedLineSpan` honours the `#line 1` directive in the preamble, so it
|
|
|
|
|
returns user-source coordinates directly. Diagnostics whose
|
|
|
|
|
`SourceSpan.Start < preambleLength` are dropped — they are phantom wrapper
|
|
|
|
|
diagnostics (e.g. `CS0161` "not all code paths return a value" on the synthesized
|
|
|
|
|
`Run` method body when the user has not yet typed `return`) and are not the
|
|
|
|
|
user's fault.
|
|
|
|
|
|
|
|
|
|
### 2. ForbiddenTypeAnalyzer
|
|
|
|
|
|
|
|
|
|
`ForbiddenTypeAnalyzer.Analyze(compilation)` walks the wrapped syntax tree and
|
|
|
|
|
resolves every referenced symbol against the sandbox deny-list (namespace
|
|
|
|
|
prefixes `System.IO`, `System.Net`, `System.Diagnostics`, `System.Reflection`,
|
|
|
|
|
`System.Threading.Tasks`, etc., plus type-granular denials like
|
|
|
|
|
`System.Environment` and `System.Threading.Thread`). Each violation is mapped
|
|
|
|
|
via `tree.GetMappedLineSpan(span)` to user-source coordinates and emitted as an
|
|
|
|
|
error marker with code `OTSCRIPT_FORBIDDEN`.
|
|
|
|
|
|
|
|
|
|
### 3. DependencyExtractor
|
|
|
|
|
|
|
|
|
|
`DependencyExtractor.Extract(code).Rejections` enumerates every `ctx.GetTag` /
|
|
|
|
|
`ctx.SetVirtualTag` call whose first argument is NOT a string literal (variable,
|
|
|
|
|
concatenation, interpolation). Each rejection carries a `TextSpan` in
|
|
|
|
|
user-source coordinates, emitted as code `OTSCRIPT_DYNPATH`. This is the exact
|
|
|
|
|
same check publish enforces — the editor shows the squiggle where the literal
|
|
|
|
|
must appear.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Tag-Path Completion
|
|
|
|
|
|
|
|
|
|
When the caret is inside the first string-literal argument of a `GetTag("…")` or
|
|
|
|
|
`SetVirtualTag("…")` invocation, the completion provider delegates to
|
|
|
|
|
`IScriptTagCatalog` instead of the semantic model.
|
|
|
|
|
|
|
|
|
|
### IScriptTagCatalog contract
|
|
|
|
|
|
|
|
|
|
`ScriptAnalysis/IScriptTagCatalog.cs`
|
|
|
|
|
`GetPathsAsync(string? filter, CancellationToken)` returns the distinct
|
|
|
|
|
**runtime-resolvable keys** a script may pass to `ctx.GetTag` /
|
|
|
|
|
`ctx.SetVirtualTag`, optionally filtered by a case-insensitive `StartsWith`
|
|
|
|
|
prefix. The concrete implementation `ScriptTagCatalog` reads `Tag` +
|
|
|
|
|
`VirtualTag` rows from `OtOpcUaConfigDbContext` and projects them as follows:
|
|
|
|
|
|
|
|
|
|
| Tag category | Resolvable key emitted |
|
|
|
|
|
|---|---|
|
|
|
|
|
| Equipment driver tag (`EquipmentId != null`) | Driver `FullName` extracted from `Tag.TagConfig` JSON — the verified `DependencyMux` key |
|
|
|
|
|
| SystemPlatform / Galaxy tag (`EquipmentId == null`) | MXAccess dot-ref: `FolderPath.Name` when a folder is set, else `Name` |
|
|
|
|
|
| Virtual tag | Leaf `Name` (best-effort) |
|
|
|
|
|
|
|
|
|
|
**Why only resolvable keys?** The live runtime resolves a `ctx.GetTag("X")`
|
|
|
|
|
literal against `DriverInstanceActor.AttributeValuePublished.FullReference`,
|
|
|
|
|
which is the `FullName` field from `Tag.TagConfig`
|
|
|
|
|
(see `Phase7Composer.ExtractTagFullName` + `EquipmentNodeWalker.ExtractFullName`).
|
|
|
|
|
The UNS-path engine (`Core.VirtualTags.VirtualTagEngine`, keyed by the
|
|
|
|
|
slash-joined `Enterprise/Site/Area/Line/Equipment/TagName` browse path) is
|
|
|
|
|
dormant — it is not wired into the host — so UNS browse paths never resolve at
|
|
|
|
|
runtime and are intentionally NOT offered as completions.
|
|
|
|
|
|
|
|
|
|
The catalog is scoped per Blazor circuit; each call creates and disposes its own
|
|
|
|
|
`DbContext` via the pooled factory (same pattern as `UnsTreeService`). Results
|
|
|
|
|
are bounded to 200 entries to keep the completion list responsive on large fleets.
|
|
|
|
|
|
|
|
|
|
### Literal gate
|
|
|
|
|
|
|
|
|
|
`TryGetTagPathLiteral` identifies the tag-path context by climbing from the
|
|
|
|
|
syntax token under the caret to: `LiteralExpressionSyntax` → `ArgumentSyntax`
|
|
|
|
|
(first argument only) → `ArgumentListSyntax` → `InvocationExpressionSyntax` →
|
|
|
|
|
`MemberAccessExpressionSyntax`. The method name check is
|
|
|
|
|
`method is "GetTag" or "SetVirtualTag"` — it matches by method name only, not
|
|
|
|
|
by receiver type, so `anything.GetTag("…")` would also trigger tag-path
|
|
|
|
|
completion. This is harmless in the sandbox (no other `GetTag` methods exist in
|
|
|
|
|
the allowed reference set) and keeps the detection simple.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Vendoring / Air-gap Safety
|
|
|
|
|
|
|
|
|
|
Monaco is served from
|
|
|
|
|
`_content/ZB.MOM.WW.OtOpcUa.AdminUI/lib/monaco/vs/` (the Blazor static-asset
|
|
|
|
|
path for the AdminUI RCL). The loader script is at
|
|
|
|
|
`_content/ZB.MOM.WW.OtOpcUa.AdminUI/js/monaco-init.js`. No CDN is contacted at
|
|
|
|
|
runtime. The legacy CDN loader (`monaco-loader.js`) and the `eval`/`Task.Delay`
|
|
|
|
|
injection previously in `ScriptEdit.razor` have been removed.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Wire-in Points
|
|
|
|
|
|
|
|
|
|
### ScriptEdit page (`/scripts/{id}`)
|
|
|
|
|
|
|
|
|
|
`src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/ScriptEdit.razor`
|
|
|
|
|
|
|
|
|
|
Hosts `<MonacoEditor @bind-Value="_form.SourceCode">`. On save, SHA-256 is
|
|
|
|
|
recomputed from the editor value and stored in `Script.SourceHash` — unchanged
|
|
|
|
|
from the pre-Monaco save path.
|
|
|
|
|
|
|
|
|
|
### VirtualTagModal (`/uns` TagModal for virtual tags)
|
|
|
|
|
|
|
|
|
|
`src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/VirtualTagModal.razor`
|
|
|
|
|
|
|
|
|
|
When the selected script has a `ScriptId`, an inline "Script source" panel
|
|
|
|
|
renders `<MonacoEditor>` bound to the script source. The panel shows a
|
|
|
|
|
**"Used by N virtual tag(s)"** notice (populated from
|
|
|
|
|
`IUnsTreeService.CountVirtualTagsUsingScriptAsync`). A dedicated **Save script**
|
|
|
|
|
button calls `IUnsTreeService.UpdateScriptSourceAsync`, which performs a
|
|
|
|
|
`RowVersion`-guarded update of the `Script` row — a separate concurrency unit
|
|
|
|
|
from the virtual-tag Create/Save operation. Creating a brand-new script inline
|
|
|
|
|
from the modal is a follow-up (not v1).
|
|
|
|
|
|
|
|
|
|
Three `IUnsTreeService` methods back the inline panel:
|
|
|
|
|
`GetScriptSourceAsync` / `CountVirtualTagsUsingScriptAsync` /
|
|
|
|
|
`UpdateScriptSourceAsync`.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## How to Extend
|
|
|
|
|
|
|
|
|
|
### Adding a new completion source
|
|
|
|
|
|
|
|
|
|
1. Implement a new case in `ScriptAnalysisService.CompleteAsync`. The
|
|
|
|
|
`Analyze(code)` seam gives you the `SemanticModel`, `SyntaxTree`, and
|
|
|
|
|
preamble offset.
|
|
|
|
|
2. Add the corresponding request/response DTOs to
|
|
|
|
|
`ScriptAnalysisContracts.cs` if a new endpoint is needed, or fold into an
|
|
|
|
|
existing endpoint with an extra field.
|
|
|
|
|
3. Register the new Monaco provider in `monaco-init.js` using the same
|
|
|
|
|
`fetch('/api/script-analysis/…', { method: 'POST', … })` pattern.
|
|
|
|
|
|
|
|
|
|
### Adding a new analysis check (diagnostics)
|
|
|
|
|
|
|
|
|
|
Extend `Diagnose` in `ScriptAnalysisService`. The existing three-source pattern
|
|
|
|
|
(Roslyn → `ForbiddenTypeAnalyzer` → `DependencyExtractor`) is additive: add a
|
|
|
|
|
fourth source and append to `markers`. Keep severity at 8 (Monaco error) for
|
|
|
|
|
anything that blocks publish; use 4 (warning) for advisory-only hints.
|
|
|
|
|
|
|
|
|
|
### Changing the tag-path catalog source
|
|
|
|
|
|
|
|
|
|
Replace or extend the `ScriptTagCatalog` implementation of `IScriptTagCatalog`.
|
|
|
|
|
Register the replacement in `EndpointRouteBuilderExtensions.AddAdminUI`
|
|
|
|
|
(or inject via the DI container as the `IScriptTagCatalog` scoped service).
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Known Limitations / Follow-ups
|
|
|
|
|
|
|
|
|
|
- **Tag-path completion shows resolvable keys only.** Surfacing the UNS browse
|
|
|
|
|
path as a completion *detail* (a non-inserted hint shown alongside the
|
|
|
|
|
resolvable key) for discoverability is a tracked follow-up.
|
|
|
|
|
- **Literal gate matches by method name, not receiver.** `anything.GetTag("…")`
|
|
|
|
|
would also trigger tag-path completion (not just `ctx.GetTag("…")`). Harmless
|
|
|
|
|
in the sandbox today — no other `GetTag` methods exist in the allowed reference
|
|
|
|
|
set — but tighter binding to `ctx` / `VirtualTagContext` is a follow-up.
|
|
|
|
|
- **Analysis runs synchronous Roslyn (CPU-bound).** Each endpoint call builds a
|
|
|
|
|
`CSharpCompilation` synchronously. This is intentional: the endpoints are
|
|
|
|
|
admin-only, client-side debounced (~500 ms), and `Task.Run` offload adds
|
|
|
|
|
unnecessary complexity for infrequent admin use. If concurrency becomes a
|
|
|
|
|
concern under heavy admin usage, wrapping each `Analyze` call in `Task.Run` is
|
|
|
|
|
the correct fix.
|
|
|
|
|
- **InlayHints is a stub.** `/api/script-analysis/inlay-hints` returns an empty
|
|
|
|
|
list. Filling it in (parameter name hints from the semantic model) is a
|
|
|
|
|
follow-up task.
|
|
|
|
|
- **Language providers register globally at Monaco load.** The six providers are
|
|
|
|
|
registered for the `csharp` language on the global Monaco instance (loaded once
|
|
|
|
|
per page in `App.razor`). There is no impact today because only
|
|
|
|
|
`Administrator`-reachable pages host a `MonacoEditor`, but if a read-only
|
|
|
|
|
surface ever embeds the editor the providers will fire (harmlessly — they just
|
|
|
|
|
get 401 responses from the `FleetAdmin`-gated endpoints and return empty
|
|
|
|
|
results).
|
|
|
|
|
- **Creating a new script from the VirtualTagModal inline panel** is deferred.
|
|
|
|
|
The current inline panel loads and saves an existing `Script` row; creating a
|
|
|
|
|
brand-new script requires a separate create flow and is a follow-up.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Testing
|
|
|
|
|
|
|
|
|
|
Unit tests live in
|
|
|
|
|
`tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/ScriptAnalysis/`:
|
|
|
|
|
|
|
|
|
|
| File | Covers |
|
|
|
|
|
|------|--------|
|
|
|
|
|
| `DiagnoseTests.cs` | Clean script (no markers), Roslyn error, ForbiddenTypeAnalyzer violation, dynamic-path rejection, `#line` mapping |
|
|
|
|
|
| `CompletionTests.cs` | Scope completions, dot-member completions after `ctx.` |
|
|
|
|
|
| `TagPathCompletionTests.cs` | Tag-path literal completions inside `GetTag`/`SetVirtualTag` via a fake `IScriptTagCatalog` |
|
|
|
|
|
| `HoverSignatureTests.cs` | Hover markdown, signature-help parameter tracking |
|
|
|
|
|
| `FormatTests.cs` | `NormalizeWhitespace` round-trip |
|
|
|
|
|
| `ScriptTagCatalogTests.cs` | Equipment-tag `FullName`, Galaxy dot-ref, virtual-tag leaf `Name`, filter prefix, MaxResults bound |
|
|
|
|
|
|
|
|
|
|
`tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/ScriptSourceServiceTests.cs`
|
|
|
|
|
covers `GetScriptSourceAsync` / `CountVirtualTagsUsingScriptAsync` /
|
|
|
|
|
`UpdateScriptSourceAsync` via the in-memory EF pattern.
|
|
|
|
|
|
|
|
|
|
No bUnit tests exist for `MonacoEditor.razor` or `VirtualTagModal.razor` — the
|
|
|
|
|
AdminUI has no bUnit dependency. Razor/JS correctness was proven by live
|
|
|
|
|
docker-dev `/run` (ScriptEdit + UNS TagModal with Monaco, diagnostics, hover,
|
|
|
|
|
tag-path completions, Format, inline script save).
|