diff --git a/docs/advanced-filemaker-scripting-syntax.md b/docs/advanced-filemaker-scripting-syntax.md
new file mode 100644
index 0000000..d18905f
--- /dev/null
+++ b/docs/advanced-filemaker-scripting-syntax.md
@@ -0,0 +1,186 @@
+# Advanced FileMaker Scripting Syntax
+
+Status: authoritative reference for all script-step POCOs.
+
+## Purpose
+
+SharpFM renders FileMaker script XML as editable display text. Some XML
+state doesn't naturally appear in FileMaker Pro's own display (object
+IDs, flags, embedded newlines, etc.). To round-trip that state faithfully
+through display-text editing, SharpFM extends FM Pro's display grammar
+with a small set of named conventions — collectively, **Advanced
+FileMaker Scripting Syntax**.
+
+The extensions are safe because **FileMaker Pro never consumes SharpFM's
+display text**. FM Pro reads only the binary `Mac-XMSS` clipboard
+payload; `POCO.ToXml()` always emits pure FM Pro XML regardless of
+whatever extensions are present in the display. The display text is a
+SharpFM-internal surface for user editing, and our display parser only
+ever reads our own extended output.
+
+## Core invariant
+
+> `ToDisplayLine()` and `FromDisplayParams()` together are lossless for
+> every piece of state the POCO carries. XML state that is dropped is
+> dropped because it carries no information, not because the display
+> can't express it.
+
+Every POCO's docstring contains a **zero-loss audit** (see below) that
+enumerates XML state explicitly and marks each item as either rendered
+by FM Pro natively, covered by an advanced-syntax extension, or
+intentionally dropped with a written rationale.
+
+## The three extension forms
+
+Pick the form that matches the shape of the hidden state:
+
+| Shape of state | Syntax | Example | Used for |
+|---|---|---|---|
+| Id annotation on a named reference | `(#id)` suffix after the name | `"Find Customer" (#7)` | Script, Layout, TableOccurrence, Field id round-trip |
+| Boolean / enum flag already in FM Pro's grammar | word token inline, matching FM Pro's wording | `Exit after last: On` | Flags FM Pro itself renders; we mirror its wording |
+| Bulk invisible state (multi-slot / structured) | trailing `; Kind: [...]` block, word-token values | `; Buttons: ["OK" commit; "Cancel" nocommit]` | Button configurations, input-field metadata, any future bulk state |
+
+### Form 1 — `(#id)` suffix
+
+Appended to a quoted or `Table::Field` name:
+
+- `FieldRef` emits `People::FirstName (#7)` when an id is known; plain
+ `People::FirstName` otherwise.
+- `PerformScriptStep` emits `"Find Customer" (#42)`.
+- `GoToLayoutStep` emits `"Invoices Detail" (#11)`.
+
+Omitted when the id is zero or unknown — `(#0)` would be visual noise
+for unresolved references.
+
+### Form 2 — inline word tokens
+
+Parsed at a specific named prefix, matching FM Pro's own rendering of
+the same flag:
+
+- `Exit after last: On` on found-set iterators.
+- `With dialog: Off` on steps that can suppress their confirmation UI.
+- `Restore: On|Off` — **reserved**. Not currently emitted by any POCO;
+ see "What to drop vs. surface" below for the rationale.
+
+### Form 3 — trailing `; Kind: [...]` blocks
+
+Bulk or structured state that doesn't reduce to a single flag:
+
+- `; Buttons: ["OK" commit; "Cancel" nocommit; "" nocommit]` — used by
+ Show Custom Dialog for its button configuration.
+- Future bulk state (Input Field specs, etc.) takes this form.
+
+## Parsing precedence
+
+Named-prefix inline tokens (Form 2) are parsed before trailing
+`Kind: [...]` blocks (Form 3). A display line like
+
+```
+Go to Record/Request/Page [ Next ; Exit after last: On ; Buttons: [...] ]
+```
+
+is tokenized as:
+1. Positional `Next` (Form 2 equivalent — fixed-position enum).
+2. Named inline `Exit after last: On` (Form 2).
+3. Named block `Buttons: [...]` (Form 3).
+
+Form 1 (`(#id)`) is applied inside each name-bearing token — e.g. inside
+the bracketed name-and-id of a named ref — and does not collide with
+Form 2 or Form 3 separators.
+
+## Zero-loss audit requirement
+
+Every POCO author must complete this audit in the class XML doc
+comment. Template:
+
+```
+///
+/// Zero-loss audit for StepName:
+///
+/// - <Step> attributes (enable/id/name) — round-tripped.
+/// - <Calculation> CDATA — round-tripped via Calculation.
+/// - <SomeElement state="..."/> — Form 2 token "Some: On|Off".
+/// - <Dropped/> — intentionally dropped; rationale: ...
+///
+///
+```
+
+Items fall into exactly one of these buckets:
+
+1. **Rendered natively by FM Pro** — FM Pro's display grammar already
+ covers it; SharpFM mirrors the wording.
+2. **Covered by an extension form** — one of the three above.
+3. **Intentionally dropped** — rationale required. See next section for
+ how to judge.
+
+Omitting the audit is a review blocker.
+
+## What to drop vs. surface
+
+Hidden state is surfaced only when a user could meaningfully change it.
+Some state is structurally present in XML but semantically fixed — FM
+Pro never alters it, never emits it in clipboard output, and no user
+workflow produces a different value. Round-tripping such state adds
+visual noise for zero information.
+
+### Canonical drop: `` on `If`
+
+Upstream `agentic-fm` snippets include the element; FM Pro's own
+clipboard output never does; no FM Pro user interaction produces
+`state="True"`. `IfStep` drops it on both read and write. The audit
+entry documents the drop:
+
+> <Restore state="False"/> — intentionally dropped. FM Pro never
+> changes the value and never emits the element in clipboard output; it
+> carries no information worth round-tripping.
+
+### Canonical surface: field `id` via `(#id)` suffix
+
+`` is a real identity — the id
+selects which field is referenced, and two fields named `F` in different
+tables are not interchangeable. Dropping the id would change semantics.
+`FieldRef` always emits `(#12)` when an id is available.
+
+### The heuristic
+
+- If two valid FM Pro script states would be visually identical under
+ the display grammar without the extension, **surface** the state.
+- If the state has a fixed value that no user can change, **drop** it.
+- If you're not sure which, **surface**. Reversing a surface → drop
+ later is non-breaking; reversing a drop → surface may break tests
+ users wrote against the earlier display.
+
+## Adjacent convention: `//` disabled-step prefix
+
+Disabled steps are prefixed with `//` in display text:
+
+```
+// Set Error Capture [ On ]
+```
+
+Parsing strips the `//` and sets `ScriptStep.Enabled = false`. This is
+a document-level convention (applied to any step line) rather than a
+per-step extension. Covered here for completeness.
+
+## Implementation touch points
+
+- `FieldRef.ToDisplayString` / `FieldRef.FromDisplayToken`
+ (`src/SharpFM.Model/Scripting/Values/FieldRef.cs`) — Form 1 reference
+ implementation.
+- `CommentStep.ReturnGlyph`
+ (`src/SharpFM.Model/Scripting/Steps/CommentStep.cs`) — the `⏎`
+ (U+23CE) glyph for single-line rendering of multi-line comment text.
+ An idiom adjacent to the three forms but specific to Comment.
+- `ScriptLineParser.ParseLine`
+ (`src/SharpFM.Model/Scripting/ScriptLineParser.cs`) — disabled-step
+ prefix and bracket tokenization.
+- `PerformScriptStep.FromDisplayParams`,
+ `GoToLayoutStep.FromDisplayParams` — Form 1 regex parsers for named
+ refs with `(#id)` suffixes.
+
+## Change log
+
+- **2026-04** — Extracted from `docs/step-definitions.md:44-69` into
+ its own document as part of the POCO big-bang migration. Rationale
+ section ("what to drop vs. surface") added with `Restore` on `If` as
+ the canonical drop example.
diff --git a/docs/plans/script-step-poco-sweep.md b/docs/plans/script-step-poco-sweep.md
new file mode 100644
index 0000000..2c46745
--- /dev/null
+++ b/docs/plans/script-step-poco-sweep.md
@@ -0,0 +1,217 @@
+# Script Step POCO Migration — Sweep-Phase Plan
+
+Status: completed on `fuzzz/all-pocos`. All 205 script steps are typed
+`IStepFactory` POCOs discovered via `StepRegistry`. This doc is kept as
+a record of the sweep's approach; new step POCOs (e.g. after a FileMaker
+release that adds a step) should follow the same per-step TDD workflow
+below.
+
+## Context
+
+The [pilot](../advanced-filemaker-scripting-syntax.md) established the
+typed POCO pattern with three representative steps: `BeepStep`,
+`SetErrorCaptureStep`, `IfStep`. The sweep covers the remaining 189
+FileMaker script steps (206 total − 14 pre-existing POCOs − 3 pilot).
+When the sweep finishes, all 206 steps are typed POCOs,
+`StepCatalogLoader` and the catalog-driven helpers are deleted, and
+the `step-catalog-en.json` file is no longer embedded.
+
+The sweep is not a single PR. Each wave lands independently, leaves the
+tree green, and can be reviewed in isolation.
+
+## Tier-based ordering
+
+Run the catalog through the tier filter from the earlier analysis (see
+message history in the pilot branch's `agent/catalogs` walkthrough):
+
+| Tier | Shape | Count | Rationale |
+|---|---|---|---|
+| A | Zero-param | 43 | Trivially mechanical; establish the cadence. |
+| B | Only boolean/enum params | 29 | Locks in `ParamMetadata.ValidValues` patterns. |
+| C | Text-bearing | 7 | Introduces text-escaping and display bracket grammar. |
+| D | Calc / field / named-ref / complex | ~110 | The variety bucket. Contains real work — advanced-syntax decisions land here. |
+
+Migrate in A → B → C → D order. Within a tier, batch by FileMaker
+category (`control`, `windows`, `fields`, etc.) to keep related steps'
+PRs discoverable.
+
+## Per-step TDD workflow
+
+For each step, in this order:
+
+1. **Copy canonical XML** from
+ `C:\source\personal\agentic-fm\agent\snippet_examples\steps\\.xml`
+ (outside this repo — read-only reference, never committed) into an
+ inline `const string` fixture in the new test class.
+2. **Write the round-trip test first.** Parse the fixture → run through
+ the POCO's `Metadata.FromXml!(el).ToXml()` → assert
+ `XNode.DeepEquals` against the source. Fails because the POCO
+ doesn't exist yet.
+3. **Write the POCO** — implement `IStepFactory`, define `Metadata`,
+ implement `ToXml` / `ToDisplayLine` / `FromXml` /
+ `FromDisplayParams`. Pattern from `BeepStep.cs` (zero-param),
+ `SetErrorCaptureStep.cs` (single-boolean), or `IfStep` (block-pair,
+ calc) — pick the closest match.
+4. **Add the display tests.** `ToDisplayLine()` returns expected
+ string; `FromDisplay(Metadata.FromDisplay!(...))` survives a
+ round-trip.
+5. **Add the disabled-step test.** `enable="False"` → `step.Enabled`
+ is false → `ToXml()` emits `enable="False"`.
+6. **Complete the zero-loss audit** in the POCO's XML doc comment.
+ Every XML element/attribute either rendered natively by FM Pro,
+ covered by an advanced-syntax extension, or intentionally dropped
+ with rationale (see
+ [`advanced-filemaker-scripting-syntax.md`](../advanced-filemaker-scripting-syntax.md)).
+7. **For Tier D steps that need advanced syntax**, land the syntax
+ extension behind its own commit before the POCO that uses it, so
+ reviewers can validate the grammar decision in isolation.
+
+## Rollout cadence
+
+- **One PR per batch of ~10 POCOs within a tier.** Smaller is too
+ noisy; larger is unreviewable.
+- **Tier A fits in 4-5 PRs.** Tier B in 3. Tier C in 1. Tier D is
+ where PR planning gets real — expect 12-15 PRs depending on
+ per-step complexity.
+- **Each PR's diff is only steps + their tests.** No consumer rewires,
+ no deletions (with the specific exceptions listed below).
+- **Commit per POCO within a PR** so each step is individually
+ revertable if the pattern needs adjusting.
+
+## Consumer migration schedule
+
+Consumers of the legacy `StepCatalogLoader` / `StepCatalogGenerated`
+flip to `StepRegistry` as their dependencies complete:
+
+| Consumer | Flips when | What changes |
+|---|---|---|
+| `FmScriptCompletionProvider` | Already migrated in pilot | — |
+| `ScriptValidator` | All block-pair partners (If/End If, Loop/End Loop, Open/Commit Transaction, etc.) are POCOs | `definition.BlockPair` reads become `StepRegistry.ByName[name].BlockPair` |
+| `ScriptTextParser.FromDisplayLine` | All catalog-name resolutions have POCO equivalents | Drops the `StepCatalogLoader.ByName.TryGetValue` fallback |
+| `FmScript.ApplyUpdate` | Typed setters exist on every POCO | MCP apply-op dispatches to POCO-specific `With*` methods instead of generic `CatalogXmlBuilder.UpdateParam` |
+
+Until its flip point, each consumer keeps reading the legacy surface —
+the pilot's `StepRegistry` bridge populates `StepXmlFactory` /
+`StepDisplayFactory` so this works transparently.
+
+## Deletion schedule
+
+In order:
+
+1. **Per-wave**: as a tier completes, delete any catalog-helper code
+ paths that are now exclusively serving that tier's step shapes.
+ (Usually none — the helpers are shape-agnostic.)
+2. **After Tier D completes** — when all 206 steps are POCOs and all
+ consumers above have flipped:
+ - Delete `src/SharpFM.Model/Scripting/StepCatalogLoader.cs`.
+ - Delete `src/SharpFM.Model/Scripting/IStepCatalog.cs`.
+ - Delete `src/SharpFM.Model/Scripting/Serialization/StepXmlFactory.cs`.
+ - Delete `src/SharpFM.Model/Scripting/Serialization/StepDisplayFactory.cs`.
+ - Remove the `StepRegistry → legacy factory` bridge from
+ `StepRegistry.Scan()` — now dead code.
+ - Delete `src/SharpFM.Model/Scripting/Steps/RawStep.cs`.
+ - Delete `src/SharpFM.Model/Scripting/Steps/RawStepAllowList.cs`
+ and its tests.
+ - Delete `src/SharpFM.Model/Scripting/Serialization/CatalogXmlBuilder.cs`.
+ - Delete `src/SharpFM.Model/Scripting/Serialization/CatalogParamExtractor.cs`.
+ - Delete `src/SharpFM.Model/Scripting/Serialization/CatalogDisplayRenderer.cs`.
+ - Delete `src/SharpFM.Model/Scripting/Serialization/CatalogValidator.cs`.
+ - Remove the ``
+ line from `src/SharpFM.Model/SharpFM.Model.csproj`.
+ - Delete `src/SharpFM.Model/Scripting/Catalog/step-catalog-en.json`.
+ - Introduce `UnknownStep` in place of `RawStep` for forward-compat
+ against future FileMaker step additions (wraps raw XElement;
+ same semantics as the retired `RawStep` when `Definition == null`).
+3. **After ship verification** — `StepDefinition` / `StepParam` /
+ `StepBlockPair` / `BlockPairRole` / `StepEnumValue` no longer have
+ any catalog-side producers. They may be kept (used by
+ `StepMetadata.BlockPair`) or retired depending on whether the new
+ metadata types fully absorb them. Expect a small cleanup commit
+ here.
+
+## Coverage verification
+
+One test asserts every step catalogued by upstream is present in our
+registry:
+
+```csharp
+[Fact]
+public void StepRegistry_CoversFullCatalog()
+{
+ var expected = File.ReadAllText("path/to/canonical/step-list.json");
+ var names = JsonSerializer.Deserialize(expected);
+ foreach (var name in names)
+ Assert.True(StepRegistry.ByName.ContainsKey(name),
+ $"Missing POCO for step '{name}'");
+}
+```
+
+The canonical list is a snapshot of agentic-fm step names taken at
+sweep kickoff (checked in as a one-off fixture — not the same as the
+JSON catalog, just names + ids). As steps are added to upstream later,
+the fixture is updated deliberately, preventing silent drift.
+
+## Regression containment
+
+The pilot's `FmScriptCompletionProvider` regression (only 3 steps
+suggested in completions) resolves as the sweep progresses. Gate each
+sweep PR on **monotonically increasing completion coverage**:
+
+- `CompletionProviderTests` has an `EmptyLine_SuggestsPocoStepNames`
+ test that enumerates all expected pilot step names.
+- Each sweep PR adds the new step names to that test's expected list.
+- A PR that removes a name from the list is a review blocker unless
+ justified (e.g. a step was consolidated).
+
+After Tier A merges, the list is 43 + 14 + 3 = 60 steps. After Tier B,
+89. After Tier C, 96. After Tier D, 206. Upstream adds thereafter append.
+
+## Risks and gotchas (filled in during pilot execution)
+
+> This section gets populated with concrete discoveries from pilot
+> implementation. Left as a placeholder until pilot lands.
+
+Observed so far:
+
+- **`StepRegistry` lazy init doesn't run before pre-existing consumers
+ touch `StepXmlFactory`.** Mitigation: single `[ModuleInitializer]`
+ on `StepRegistry` itself. Acceptable — one initializer on the
+ registry, not per-POCO.
+- **Legacy `FmScript.ToDisplayLines` reads `step.Definition.BlockPair`
+ for indent decisions.** POCOs passing `Definition = null` break
+ indentation on block-pair steps. Mitigation for pilot: project a
+ minimal `StepDefinition` with just `BlockPair` populated into
+ `base(...)`. Cleanup: when `FmScript.ToDisplayLines` migrates to
+ `StepRegistry`, drop the synthesized `Definition`.
+- **Tests that used `Beep` as a "catalog-but-not-POCO" canary break
+ when Beep migrates.** Mitigation: swap to `Halt Script` (or
+ whatever zero-param step is still on the catalog-only path). Expect
+ similar swaps as each subsequent zero-param step migrates.
+
+## Open questions deferred from pilot
+
+- **Plugin-scenario multi-assembly registration.** Today
+ `StepRegistry.Scan()` reflects only the model assembly. If a plugin
+ ships its own POCOs (future scenario), should the registry scan
+ loaded plugin assemblies too? Probable answer: yes, with an
+ `RegisterAssembly(Assembly)` public method plugins call on load.
+ Address when the first plugin author asks for it.
+- **`Metadata.Notes` wiring into UI.** The pilot populates `StepNotes`
+ on `SetErrorCaptureStep` and `IfStep`. No UI reads it yet. Tooltip
+ and hover integration is a post-sweep concern — sequencing depends
+ on Monaco / Avalonia tooltip API decisions outside this migration's
+ scope.
+- **`Metadata.MonacoSnippet` or synthesis.** Pilot dropped the
+ catalog's `MonacoSnippet` field entirely; completion falls back to
+ the step name for self-closing steps and provides nothing for
+ multi-param ones. Options: (a) let each POCO author an optional
+ snippet string, (b) synthesize at runtime from `ParamMetadata`, (c)
+ defer snippets until someone complains. Recommend (c) — the three
+ pilot POCOs work fine without; revisit after Tier B lands.
+- **When to retire `StepDefinition`/`StepParam` records.** The new
+ metadata types (`StepMetadata` / `ParamMetadata`) are intended to
+ replace them, but `StepMetadata.BlockPair` currently reuses
+ `StepBlockPair` and `IfStep` still synthesizes a `StepDefinition`
+ for legacy `FmScript.ToDisplayLines`. Clean-up sequence TBD —
+ probably a small follow-up commit after the legacy factories are
+ deleted.
diff --git a/docs/step-definitions.md b/docs/step-definitions.md
new file mode 100644
index 0000000..e4532f6
--- /dev/null
+++ b/docs/step-definitions.md
@@ -0,0 +1,921 @@
+# FileMaker Script Step — Typed POCO Migration Tracker
+
+This document is the working checklist for migrating FileMaker script steps from the generic `RawStep` fallback to typed POCOs under `src/SharpFM.Model/Scripting/Steps/`. It exists to drive TDD: each step block holds a placeholder for verbatim FileMaker Pro clipboard XML, a list of the expected display-text form, and a checklist for the small refactor needed to land the typed POCO.
+
+**Rule of thumb:** we only add a typed POCO when a step has behavior the generic catalog-driven pipeline can't express (named references with ids, discriminated unions, bespoke display formats, nested calculations, custom parsing). Simple steps stay on `RawStep` indefinitely — that's fine, `RawStep` is fully lossless for the source XML.
+
+---
+
+## How to use this document
+
+For each step you want to migrate:
+
+1. **Copy verbatim XML from FileMaker Pro.** Create the step in the script workspace, right-click the step in the script and choose Copy, then paste directly into the sample block here. Do this once per meaningful permutation (e.g. each `LayoutDestination` for Go to Layout, each `RowPageLocation` for Go to Record).
+2. **Write failing tests first.** Create `tests/SharpFM.Tests/Scripting/Steps/{StepName}StepTests.cs` with the verbatim fixture as a constant and one test per behavior assertion: display rendering, round-trip XML preservation, and display-text → XML parsing.
+3. **Run the tests and watch them fail.** They'll fail against the current `RawStep` path with whatever generic output the catalog produces.
+4. **Add the typed POCO.** Create `src/SharpFM.Model/Scripting/Steps/{StepName}Step.cs` following the `GoToLayoutStep` pattern. Register with `StepXmlFactory` and `StepDisplayFactory` via a `[ModuleInitializer]`.
+5. **Run the tests and watch them pass.**
+6. **Check the regression list.** Any previously-failing test assertions for this step (see the tests under `tests/SharpFM.Tests/Scripting/` that failed after the `StepParamValue` deletion) should come back green.
+7. **Check the box** on this document.
+
+## Reference: value types already defined
+
+Reuse these wherever they fit — don't create new ones unless the step genuinely needs a new shape.
+
+| Type | Location | Purpose |
+|---|---|---|
+| `NamedRef(int Id, string Name)` | `Values/NamedRef.cs` | id+name pairs for Script, Layout (named), TableOccurrence refs |
+| `FieldRef(Table?, Id, Name, VariableName?)` | `Values/FieldRef.cs` | field or variable, with `ToDisplayString()` → `Table::Field` / `$var` |
+| `Calculation(string Text)` | `Values/Calculation.cs` | CDATA-wrapped calc expression |
+| `Animation(string WireValue)` | `Values/Animation.cs` | raw wire-string for FM animation names (unknown values pass through) |
+| `LayoutTarget` (sealed record hierarchy) | `Values/LayoutTarget.cs` | Original / Named / ByNameCalc / ByNumberCalc |
+
+## Reference: architecture pattern
+
+See `src/SharpFM.Model/Scripting/Steps/GoToLayoutStep.cs` as the canonical example. Every typed POCO has:
+
+- Typed properties (no `XElement`, no string bags)
+- `public static new ScriptStep FromXml(XElement step)` — parse from source XML
+- `public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams)` — parse from display-text tokens
+- `public override XElement ToXml()` — pure function of the typed state
+- `public override string ToDisplayLine()` — pure function of the typed state
+- A `[ModuleInitializer]` method that calls `StepXmlFactory.Register` and `StepDisplayFactory.Register`
+
+## Reference: display extension style guide
+
+See [`advanced-filemaker-scripting-syntax.md`](advanced-filemaker-scripting-syntax.md) for the full spec — the three extension forms (`(#id)` suffix, inline word tokens, trailing `; Kind: [...]` blocks), parsing precedence, the zero-loss audit every POCO must complete, and the judgment call on when to drop hidden state rather than surface it.
+
+---
+
+## Priority 1 — handler-backed steps (failing regressions)
+
+These 13 steps used to have bespoke display rendering or display-text parsing via the retired `Handlers/` tree. Their tests are currently failing because the generic catalog path can't reproduce their special formatting. Each one needs a typed POCO.
+
+P1 regression status:
+
+| Previously failing test | Status | Fixed by |
+|---|---|---|
+| `ScriptStepTests.SetField_FromXml_ToDisplayLine` | ✓ | `SetFieldStep` |
+| `ScriptStepTests.SetVariable_FromXml_ToDisplayLine` | ✓ | `SetVariableStep` |
+| `ScriptStepTests.SetVariable_WithRepetition_ToDisplayLine` | ✓ | `SetVariableStep` |
+| `ScriptStepTests.PerformScript_FromXml_ToDisplayLine` | ✓ | `PerformScriptStep` |
+| `FmScriptModelTests.FromDisplayText_ToXml_SetVariable` | ✓ | `SetVariableStep` |
+| `FmScriptModelTests.FromXml_ToDisplayText_IfEndIf_Indented` | ✓ | `ControlFlowSteps` |
+| `FmScriptModelTests.RoundTrip_RealisticScript` | ✓ | All above + Loop / End Loop typed POCOs |
+
+---
+
+### Set Field *(#76)*
+
+- [x] **Typed POCO landed** — `SetFieldStep.cs`, tests in `SetFieldStepTests.cs`
+
+**Why it's special:** FM Pro's display format is `Set Field [ Target ; Calculation ]` — the Field param appears *before* the Calculation param, even though the catalog order is [Calculation, Field]. The old `SetFieldHandler.ToDisplayLine` reordered these explicitly.
+
+**Fields to carry:** `FieldRef Target`, `Calculation Expression`. Both required (though tolerant of missing for partial input).
+
+**Verbatim XML samples** *(captured from FM Pro via Raw Clipboard Viewer)*:
+
+```xml
+
+
+
+
+
+```
+
+```xml
+
+
+
+
+
+```
+
+```xml
+
+
+
+
+
+```
+
+**Expected display forms:**
+
+- `Set Field [ ScriptDefinitionHelper::ModifiedBy ; "just-a-string" ]`
+- `Set Field [ ScriptDefinitionHelper::ModifiedBy ; $variable + " string" ]`
+- `Set Field [ ScriptDefinitionHelper::ModifiedBy ; ScriptDefinitionHelper::PrimaryKey & " " & ScriptDefinitionHelper::CreatedBy ]` (multi-line calc preserves internal newlines in CDATA but renders on one line for the display form)
+- `Set Field [ MyField ]` (no calc — degenerate case, still worth testing)
+
+**Notes on the captured XML:**
+
+- The `Field` element carries `table`, `id`, and `name` attributes — all three must round-trip (id is the lossless anchor, matching the `(#id)` convention used by `GoToLayoutStep`).
+- `Calculation` content is wrapped in CDATA and may contain literal newlines (sample 3). The typed POCO's `Calculation.Text` should preserve those newlines byte-for-byte.
+- Param order in the source XML is `[Calculation, Field]` but FM Pro's display is `[ Field ; Calculation ]` — this reorder is exactly what `SetFieldHandler.ToDisplayLine` used to do.
+
+---
+
+### Set Variable *(#141)*
+
+- [x] **Typed POCO landed** — `SetVariableStep.cs`, tests in `SetVariableStepTests.cs`
+
+**Why it's special:** The canonical display is `Set Variable [ $name[rep] ; Value: calc ]` — the `[rep]` suffix is shown only when repetition is set and not equal to 1. The catalog params are `Value` (namedCalc), `Repetition` (namedCalc), `Name` (text), which the old `SetVariableHandler` reordered and rewrapped into the display form above. Parsing back is also bespoke (`ParseVarRepetition` splits `$name[rep]`).
+
+**Fields to carry:** `string Name`, `Calculation Value`, `Calculation Repetition` (default `"1"`).
+
+**Verbatim XML samples** *(captured from FM Pro via Raw Clipboard Viewer)*:
+
+```xml
+
+
+
+
+
+
+
+
+ $count
+
+```
+
+```xml
+
+
+
+
+
+
+
+
+ $arr
+
+```
+
+```xml
+
+
+
+
+
+
+
+
+ $arr
+
+```
+
+**Expected display forms:**
+
+- `Set Variable [ $count ; Value: 0 ]`
+- `Set Variable [ $arr[3] ; Value: "third" ]`
+- `Set Variable [ $arr[$anotherVariable] ; Value: $count + 5 ]`
+- `Set Variable [ $x ]` (no value — degenerate)
+
+**Notes on the captured XML:**
+
+- XML param order is `[Value, Repetition, Name]`; display reorders to `[ Name[rep] ; Value: calc ]`.
+- `Repetition` is a full `Calculation`, not an integer — it can be a literal (`1`, `3`) or any calc expression (`$anotherVariable`, `$count + 5`). The typed POCO should carry `Calculation Repetition`, not `int`.
+- `Repetition` is *always present* in the source XML, even when it equals `1`. The display-line writer must suppress `[rep]` when `Repetition.Text == "1"` (literal-one check, not arithmetic), and the XML writer must always emit a `` element (round-trip requirement).
+- `Name` carries the `$` prefix as part of the text content — do not strip it on parse; treat the whole string as the variable name. Parsing `$arr[3]` or `$arr[$anotherVariable]` back from display text means splitting on the outermost `[` / `]` to separate name from repetition calc (watch for nested brackets in the repetition expression).
+- Both `Value` and `Repetition` wrap their `Calculation` in a named wrapper element (``) — this is the `namedCalc` catalog shape.
+
+---
+
+### Perform Script *(#1)*
+
+- [x] **Typed POCO landed** — `PerformScriptStep.cs`, tests in `PerformScriptStepTests.cs`
+
+**Why it's special:** Two discriminated modes (like Go to Layout's `LayoutTarget`):
+
+1. **Static reference** — a named `` element picks the script, optional parameter calc rides as a bare `` sibling.
+2. **By calculation** — no `
+
+```
+
+```xml
+
+
+
+
+
+
+
+```
+
+```xml
+
+
+
+
+```
+
+```xml
+
+
+
+
+
+
+```
+
+**Expected display forms** *(FM Pro's native wording confirmed; SharpFM appends `(#id)` on static refs per the style guide)*:
+
+- `Perform Script [ Specified: From list ; "Dummy-Script-For-Reference" (#4) ; Parameter: $$SomeGlobalVariable ]`
+- `Perform Script [ Specified: By name ; $$globalVar & " literal-string" ; Parameter: $$SomeGlobalVariable ]` *(no id — by-name form has no `
- ";
-
- var editor = new ScriptClipEditor(xml);
- // Trigger a non-sealed edit so the rebuild loop runs end-to-end.
- var line1 = editor.Document.GetLineByNumber(1);
- var t = editor.Document.GetText(line1.Offset, line1.Length);
- editor.Document.Replace(line1.Offset, line1.Length, t.Replace("$x > 0", "$x > 1"));
-
- var outXml = editor.ToXml();
- var outDoc = XDocument.Parse(outXml);
-
- // Script wrapper metadata preserved
- var scriptEl = outDoc.Root!.Element("Script");
- Assert.NotNull(scriptEl);
- Assert.Equal("42", scriptEl!.Attribute("id")!.Value);
- Assert.Equal("MyScript", scriptEl.Attribute("name")!.Value);
-
- // Sealed Beep's custom child preserved
- var beep = scriptEl.Elements("Step").FirstOrDefault(s => s.Attribute("id")?.Value == "93");
- Assert.NotNull(beep);
- Assert.NotNull(beep!.Element("WrapperPreserved"));
- }
-
- // --- Idempotency ---
-
- [Fact]
- public void ToXml_CalledTwice_ProducesIdenticalOutput()
- {
- var editor = new ScriptClipEditor(ScriptWithSealedBeepXml);
- var first = editor.ToXml();
- var second = editor.ToXml();
- Assert.Equal(first, second);
- }
-
- // --- Sealed step as the very last line (no trailing newline) ---
-
- [Fact]
- public void SealedStep_AtEndOfDocumentNoTrailingNewline_Preserved()
- {
- var xml = @"
- 0]]>
-
- ";
-
- var editor = new ScriptClipEditor(xml);
- var outXml = editor.ToXml();
- var outDoc = XDocument.Parse(outXml);
- var beep = outDoc.Root!.Elements("Step").Last();
-
- Assert.Equal("Beep", beep.Attribute("name")!.Value);
- Assert.NotNull(beep.Element("EofCanary"));
- }
-
- // --- FromXml reload ---
-
- [Fact]
- public void SealedAnchors_AfterDocumentShrink_AllOffsetsWithinBounds()
- {
- // Regression guard for the "stale renderer survives clip swap" bug
- // that crashed AvaloniaEdit with ArgumentOutOfRangeException on
- // Document.GetLineByOffset. The architectural fix (detach
- // renderers on clip swap) is at the UI layer and needs an Avalonia
- // context to test. This covers the defensive side: the editor's
- // own SealedAnchors iterator must never yield an anchor whose
- // offset lies past the current document length.
- var editor = new ScriptClipEditor(ScriptWithSealedBeepXml);
- Assert.Single(editor.SealedAnchors);
-
- // Drastically shrink the document — clears every line the
- // original anchor could live on.
- editor.Document.Replace(0, editor.Document.TextLength, string.Empty);
-
- foreach (var anchor in editor.SealedAnchors)
- {
- Assert.InRange(anchor.Offset, 0, editor.Document.TextLength);
- }
- }
-
- [Fact]
- public void SealedAnchors_AfterDocumentReplacedWithShorterContent_AllOffsetsWithinBounds()
- {
- // Same invariant but with content that has steps — proves the
- // iterator stays safe when the TextView swaps documents too.
- var editor = new ScriptClipEditor(ScriptWithSealedBeepXml);
- Assert.Single(editor.SealedAnchors);
-
- editor.Document.Replace(0, editor.Document.TextLength, "If [ $x > 0 ]");
-
- foreach (var anchor in editor.SealedAnchors)
- {
- Assert.InRange(anchor.Offset, 0, editor.Document.TextLength);
- }
- }
-
- [Fact]
- public void FromXml_Reload_RebuildsAnchorCacheFreshForNewContent()
- {
- var editor = new ScriptClipEditor(ScriptWithSealedBeepXml);
- Assert.Single(editor.SealedAnchors);
-
- // Reload with different content — previous anchor should be gone,
- // new anchors built for whatever sealed steps the new XML has.
- var differentXml = @"
- 0]]>
-
- ";
- editor.FromXml(differentXml);
-
- // No sealed anchors — both If and End If are POCOs.
- Assert.Empty(editor.SealedAnchors);
-
- // Reload with a sealed step back — a fresh anchor appears.
- editor.FromXml(ScriptWithSealedBeepXml);
- Assert.Single(editor.SealedAnchors);
- }
-}
diff --git a/tests/SharpFM.Tests/Scripting/Serialization/CatalogDisplayRendererTests.cs b/tests/SharpFM.Tests/Scripting/Serialization/CatalogDisplayRendererTests.cs
deleted file mode 100644
index 87c63aa..0000000
--- a/tests/SharpFM.Tests/Scripting/Serialization/CatalogDisplayRendererTests.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-using System.Xml.Linq;
-using SharpFM.Model.Scripting;
-using SharpFM.Model.Scripting.Serialization;
-using Xunit;
-
-namespace SharpFM.Tests.Scripting.Serialization;
-
-///
-/// Verifies that the stateless catalog-driven renderer reproduces the
-/// display strings the retired StepParamValue pipeline produced.
-/// These tests cross-reference existing HandlerTests / ScriptStepTests
-/// expectations so unmigrated catalog steps continue rendering identically.
-///
-public class CatalogDisplayRendererTests
-{
- private static XElement Parse(string xml) => XElement.Parse(xml);
-
- [Fact]
- public void Render_NoParamsStep_ReturnsBareName()
- {
- var el = Parse("");
- var def = StepCatalogLoader.ByName["Else"];
-
- Assert.Equal("Else", CatalogDisplayRenderer.Render(el, def));
- }
-
- [Fact]
- public void Render_IfStep_WithCalculation_BuildsBracketedForm()
- {
- var el = Parse(
- ""
- + " 0]]>");
- var def = StepCatalogLoader.ByName["If"];
-
- Assert.Equal("If [ $x > 0 ]", CatalogDisplayRenderer.Render(el, def));
- }
-
- [Fact]
- public void Render_EnumParam_ResolvesHrValue()
- {
- var el = Parse(
- ""
- + ""
- + "");
- var def = StepCatalogLoader.ByName["Go to Record/Request/Page"];
-
- var result = CatalogDisplayRenderer.Render(el, def);
- // Catalog order renders enum first; boolean Exit is False and
- // may render as "Exit after last: Off" depending on catalog metadata.
- Assert.Contains("Next", result);
- }
-
- [Fact]
- public void Render_CommentStep_FormatsWithHashPrefix()
- {
- var el = Parse("hello world");
- var def = StepCatalogLoader.ByName["# (comment)"];
-
- Assert.Equal("# hello world", CatalogDisplayRenderer.Render(el, def));
- }
-
- [Fact]
- public void Render_EmptyComment_StillReturnsHashPrefix()
- {
- var el = Parse("");
- var def = StepCatalogLoader.ByName["# (comment)"];
-
- Assert.Equal("# ", CatalogDisplayRenderer.Render(el, def));
- }
-
- [Fact]
- public void Render_NamedLayoutRef_RendersQuotedName()
- {
- // Without a typed POCO, generic rendering only carries the name
- // (no id suffix). This is the faithful pre-migration behavior.
- var el = Parse(
- ""
- + ""
- + "");
- var def = StepCatalogLoader.ByName["Go to Layout"];
-
- var result = CatalogDisplayRenderer.Render(el, def);
- Assert.Contains("\"Detail\"", result);
- }
-}
diff --git a/tests/SharpFM.Tests/Scripting/Serialization/CatalogValidatorTests.cs b/tests/SharpFM.Tests/Scripting/Serialization/CatalogValidatorTests.cs
deleted file mode 100644
index 393983d..0000000
--- a/tests/SharpFM.Tests/Scripting/Serialization/CatalogValidatorTests.cs
+++ /dev/null
@@ -1,81 +0,0 @@
-using System.Linq;
-using System.Xml.Linq;
-using SharpFM.Model.Scripting;
-using SharpFM.Model.Scripting.Serialization;
-using Xunit;
-
-namespace SharpFM.Tests.Scripting.Serialization;
-
-public class CatalogValidatorTests
-{
- private static XElement Parse(string xml) => XElement.Parse(xml);
-
- [Fact]
- public void Validate_ValidStep_ReturnsNoDiagnostics()
- {
- var el = Parse(
- ""
- + " 0]]>");
- var def = StepCatalogLoader.ByName["If"];
-
- var diagnostics = CatalogValidator.Validate(el, def, lineIndex: 0);
-
- Assert.Empty(diagnostics);
- }
-
- [Fact]
- public void Validate_NoParams_ReturnsNoDiagnostics()
- {
- var el = Parse("");
- var def = StepCatalogLoader.ByName["Else"];
-
- var diagnostics = CatalogValidator.Validate(el, def, lineIndex: 0);
-
- Assert.Empty(diagnostics);
- }
-
- [Fact]
- public void Validate_FieldParam_BadValue_EmitsFieldOrVariableWarning()
- {
- // Set Field's Field param is of type "field" — a bare identifier
- // without :: or $ prefix should trip the field-or-variable diagnostic.
- var el = Parse(
- ""
- + ""
- + "");
- var def = StepCatalogLoader.ByName["Set Field"];
-
- var diagnostics = CatalogValidator.Validate(el, def, lineIndex: 3);
-
- Assert.Contains(diagnostics, d =>
- d.Message.Contains("Expected field reference")
- && d.Line == 3);
- }
-
- [Fact]
- public void Validate_FieldParam_TableQualified_PassesValidation()
- {
- var el = Parse(
- ""
- + ""
- + "");
- var def = StepCatalogLoader.ByName["Set Field"];
-
- var diagnostics = CatalogValidator.Validate(el, def, lineIndex: 0);
-
- Assert.DoesNotContain(diagnostics, d => d.Message.Contains("Expected field reference"));
- }
-
- [Fact]
- public void Validate_MissingOptionalParam_IsSilent()
- {
- var el = Parse(""
- + "");
- var def = StepCatalogLoader.ByName["Go to Layout"];
-
- var diagnostics = CatalogValidator.Validate(el, def, lineIndex: 0);
-
- // OriginalLayout has no Layout or Animation element — both optional.
- Assert.Empty(diagnostics);
- }
-}
diff --git a/tests/SharpFM.Tests/Scripting/Serialization/CatalogXmlBuilderTests.cs b/tests/SharpFM.Tests/Scripting/Serialization/CatalogXmlBuilderTests.cs
deleted file mode 100644
index 7940d05..0000000
--- a/tests/SharpFM.Tests/Scripting/Serialization/CatalogXmlBuilderTests.cs
+++ /dev/null
@@ -1,147 +0,0 @@
-using System.Collections.Generic;
-using System.Xml.Linq;
-using SharpFM.Model.Scripting;
-using SharpFM.Model.Scripting.Serialization;
-using Xunit;
-
-namespace SharpFM.Tests.Scripting.Serialization;
-
-///
-/// Tests the stateless catalog-driven XML builder that replaces the
-/// retired StepParamValue construction path. Covers the three
-/// public entry points (BuildStep, BuildStepFromMap,
-/// UpdateParam) and the major param-type branches.
-///
-public class CatalogXmlBuilderTests
-{
- // --- BuildStep (from display tokens) ---
-
- ///
- /// Synthetic single-param calculation step — isolates the builder's
- /// calculation-emission path from catalog quirks like flag-style
- /// boolean prefixes that confuse positional matching.
- ///
- private static StepDefinition MakeSyntheticCalcStep() => new()
- {
- Name = "SyntheticCalc",
- Id = 9999,
- Params = new[]
- {
- new StepParam { XmlElement = "Calculation", Type = "calculation", Required = true }
- }
- };
-
- [Fact]
- public void BuildStep_CalculationParam_WrapsExpressionInCdata()
- {
- var def = MakeSyntheticCalcStep();
- var xml = CatalogXmlBuilder.BuildStep(def, enabled: true, ["$x > 0"]);
-
- Assert.Equal("True", xml.Attribute("enable")!.Value);
- Assert.Equal("9999", xml.Attribute("id")!.Value);
- Assert.Equal("SyntheticCalc", xml.Attribute("name")!.Value);
- Assert.Contains("$x > 0", xml.Element("Calculation")!.Value);
- }
-
- [Fact]
- public void BuildStep_DisabledFlag_EmitsEnableFalse()
- {
- var def = StepCatalogLoader.ByName["Else"];
- var xml = CatalogXmlBuilder.BuildStep(def, enabled: false, []);
-
- Assert.Equal("False", xml.Attribute("enable")!.Value);
- }
-
- [Fact]
- public void BuildStep_SetField_FromDisplayTokens_ReordersByCatalog()
- {
- // Catalog order is [Calculation, Field]. Display provides tokens
- // positionally — the first positional match binds to Calculation,
- // the second to Field.
- var def = StepCatalogLoader.ByName["Set Field"];
- var xml = CatalogXmlBuilder.BuildStep(def, enabled: true, ["People::FirstName", "$count + 1"]);
-
- Assert.NotNull(xml.Element("Calculation"));
- Assert.NotNull(xml.Element("Field"));
- }
-
- [Fact]
- public void BuildStep_NamedRefParam_EmitsNameWithoutHardcodedId()
- {
- // Pre-refactor, the old BuildNamedRefXml hardcoded id="0".
- // The new generic path emits ONLY the name attribute — ids come
- // from typed POCOs that carry them explicitly.
- var def = StepCatalogLoader.ByName["Go to Layout"];
- var xml = CatalogXmlBuilder.BuildStep(def, enabled: true, ["SelectedLayout", "\"Detail\""]);
-
- var layout = xml.Element("Layout");
- Assert.NotNull(layout);
- Assert.Equal("Detail", layout!.Attribute("name")!.Value);
- Assert.Null(layout.Attribute("id")); // id is not invented anymore
- }
-
- // --- BuildStepFromMap (from param-name dict) ---
-
- [Fact]
- public void BuildStepFromMap_IfStep_ReadsCalculationByParamName()
- {
- var def = StepCatalogLoader.ByName["If"];
- var map = new Dictionary { ["Calculation"] = "Get(FoundCount) > 0" };
-
- var xml = CatalogXmlBuilder.BuildStepFromMap(def, enabled: true, map);
-
- Assert.Contains("Get(FoundCount) > 0", xml.Element("Calculation")!.Value);
- }
-
- [Fact]
- public void BuildStepFromMap_NullMap_StillEmitsStructuralElement()
- {
- var def = StepCatalogLoader.ByName["Else"];
- var xml = CatalogXmlBuilder.BuildStepFromMap(def, enabled: true, null);
-
- Assert.Equal("Else", xml.Attribute("name")!.Value);
- }
-
- // --- UpdateParam ---
-
- [Fact]
- public void UpdateParam_IfCalculation_ReplacesExpression()
- {
- var original = XElement.Parse(
- ""
- + " 0]]>");
- var def = StepCatalogLoader.ByName["If"];
-
- var updated = CatalogXmlBuilder.UpdateParam(original, def, "Calculation", "$x >= 100");
-
- Assert.NotNull(updated);
- Assert.Contains("$x >= 100", updated!.Element("Calculation")!.Value);
- Assert.DoesNotContain("$x > 0", updated.Element("Calculation")!.Value);
- }
-
- [Fact]
- public void UpdateParam_UnknownParamName_ReturnsNull()
- {
- var original = XElement.Parse(
- ""
- + " 0]]>");
- var def = StepCatalogLoader.ByName["If"];
-
- var updated = CatalogXmlBuilder.UpdateParam(original, def, "NonExistent", "value");
-
- Assert.Null(updated);
- }
-
- [Fact]
- public void UpdateParam_LeavesOriginalUntouched()
- {
- var original = XElement.Parse(
- ""
- + " 0]]>");
- var def = StepCatalogLoader.ByName["If"];
-
- CatalogXmlBuilder.UpdateParam(original, def, "Calculation", "different");
-
- Assert.Contains("$x > 0", original.Element("Calculation")!.Value);
- }
-}
diff --git a/tests/SharpFM.Tests/Scripting/Serialization/FieldParamCatalogRoundTripTests.cs b/tests/SharpFM.Tests/Scripting/Serialization/FieldParamCatalogRoundTripTests.cs
deleted file mode 100644
index edb692f..0000000
--- a/tests/SharpFM.Tests/Scripting/Serialization/FieldParamCatalogRoundTripTests.cs
+++ /dev/null
@@ -1,117 +0,0 @@
-using System.Linq;
-using System.Xml.Linq;
-using SharpFM.Model.Scripting;
-using SharpFM.Model.Scripting.Serialization;
-using Xunit;
-
-namespace SharpFM.Tests.Scripting.Serialization;
-
-///
-/// Coverage for the field-param refactor on the catalog (RawStep) side:
-/// CatalogParamExtractor.ExtractField, CatalogXmlBuilder.BuildFieldXml,
-/// and CatalogValidator's tolerance for the lossless (#id)
-/// suffix. The 47-ish catalog-path field params rely on these helpers
-/// for id preservation across display-text editing.
-///
-public class FieldParamCatalogRoundTripTests
-{
- private static XElement MakeStep(string xml) => XElement.Parse(xml);
-
- // "Check Selection" (id=18) is a catalog-known, non-POCO step with
- // SelectAll (boolean) + Field params. Both elements are present so the
- // extract-then-rebuild round-trip has tokens for each catalog slot.
- private const string CheckSelectionXml =
- ""
- + ""
- + "";
-
- [Fact]
- public void Extract_FieldParam_IncludesIdSuffix()
- {
- var el = MakeStep(CheckSelectionXml);
- var def = StepCatalogLoader.ByName["Check Selection"];
-
- // The field param is the first (and only) param.
- var fieldParam = def.Params.First(p => p.Type == "field" || p.Type == "fieldOrVariable");
- var extracted = CatalogParamExtractor.Extract(el, fieldParam);
-
- Assert.Equal("People::Email (#7)", extracted);
- }
-
- [Fact]
- public void Build_FieldParam_ParsesIdSuffixFromDisplayToken()
- {
- var def = StepCatalogLoader.ByName["Check Selection"];
-
- // Check Selection has two params — SelectAll (boolean, label "Select")
- // and Field (no label). Pass both tokens so positional matching
- // assigns the field token to the Field slot.
- var step = CatalogXmlBuilder.BuildStep(def, enabled: true,
- hrParams: new[] { "On", "People::Email (#7)" });
-
- var field = step.Element("Field");
- Assert.NotNull(field);
- Assert.Equal("People", field!.Attribute("table")!.Value);
- Assert.Equal("7", field.Attribute("id")!.Value);
- Assert.Equal("Email", field.Attribute("name")!.Value);
- }
-
- [Fact]
- public void Build_FieldParam_NoIdSuffix_YieldsIdZero()
- {
- // Back-compat: tokens without (#id) still work (id=0 sentinel).
- var def = StepCatalogLoader.ByName["Check Selection"];
- var step = CatalogXmlBuilder.BuildStep(def, enabled: true,
- hrParams: new[] { "On", "People::Email" });
-
- var field = step.Element("Field");
- Assert.Equal("People", field!.Attribute("table")!.Value);
- Assert.Equal("0", field.Attribute("id")!.Value);
- }
-
- [Fact]
- public void FullRoundTrip_FieldParam_PreservesIdAttribute()
- {
- // Extract BOTH params from source display-side, rebuild from
- // those tokens, verify Field attributes survive byte-for-byte.
- var source = MakeStep(CheckSelectionXml);
- var def = StepCatalogLoader.ByName["Check Selection"];
-
- var tokens = def.Params
- .Select(p =>
- {
- var extracted = CatalogParamExtractor.Extract(source, p);
- // Label-prefixed tokens mimic the display format
- // MatchDisplayParams expects for labelled params.
- return p.HrLabel != null && extracted != null
- ? $"{p.HrLabel}: {extracted}"
- : extracted;
- })
- .Where(t => t != null)
- .Select(t => t!)
- .ToArray();
-
- var rebuilt = CatalogXmlBuilder.BuildStep(def, enabled: true, hrParams: tokens);
- var originalField = source.Element("Field")!;
- var rebuiltField = rebuilt.Element("Field")!;
-
- Assert.Equal(originalField.Attribute("table")!.Value, rebuiltField.Attribute("table")!.Value);
- Assert.Equal(originalField.Attribute("id")!.Value, rebuiltField.Attribute("id")!.Value);
- Assert.Equal(originalField.Attribute("name")!.Value, rebuiltField.Attribute("name")!.Value);
- }
-
- [Fact]
- public void Validate_FieldParam_WithIdSuffix_ProducesNoWarning()
- {
- // The validator's field-shape check must tolerate the (#id) suffix.
- // Without the refactor, "People::Email (#7)" failed the shape check
- // because it didn't contain literal "::" on its own (after the suffix).
- var el = MakeStep(CheckSelectionXml);
- var def = StepCatalogLoader.ByName["Check Selection"];
-
- var diagnostics = CatalogValidator.Validate(el, def, lineIndex: 0);
-
- Assert.DoesNotContain(diagnostics, d =>
- d.Message.Contains("Expected field reference"));
- }
-}
diff --git a/tests/SharpFM.Tests/Scripting/StepCatalogLoaderTests.cs b/tests/SharpFM.Tests/Scripting/StepCatalogLoaderTests.cs
deleted file mode 100644
index 196459a..0000000
--- a/tests/SharpFM.Tests/Scripting/StepCatalogLoaderTests.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-using System.Linq;
-using SharpFM.Scripting;
-using Xunit;
-
-namespace SharpFM.Tests.ScriptConverter;
-
-public class StepCatalogLoaderTests
-{
- [Fact]
- public void LoadsCatalog_HasExpectedStepCount()
- {
- Assert.True(StepCatalogLoader.All.Count > 200);
- }
-
- [Fact]
- public void LookupById_Comment_ReturnsCorrectDefinition()
- {
- var step = StepCatalogLoader.ById[89];
- Assert.Equal("# (comment)", step.Name);
- }
-
- [Fact]
- public void LookupById_If_ReturnsCorrectDefinition()
- {
- var step = StepCatalogLoader.ById[68];
- Assert.Equal("If", step.Name);
- Assert.NotNull(step.BlockPair);
- Assert.Equal(BlockPairRole.Open, step.BlockPair!.Role);
- }
-
- [Fact]
- public void LookupById_SetVariable_HasCorrectParams()
- {
- var step = StepCatalogLoader.ById[141];
- Assert.Equal("Set Variable", step.Name);
- var paramNames = step.Params.Select(p => p.XmlElement).ToArray();
- Assert.Contains("Name", paramNames);
- // Value and Repetition are wrapper elements, xmlElement is "Calculation"
- var wrappers = step.Params.Select(p => p.WrapperElement).ToArray();
- Assert.Contains("Value", wrappers);
- Assert.Contains("Repetition", wrappers);
- }
-
- [Fact]
- public void LookupByName_CaseInsensitive()
- {
- Assert.True(StepCatalogLoader.ByName.ContainsKey("set variable"));
- Assert.True(StepCatalogLoader.ByName.ContainsKey("SET VARIABLE"));
- Assert.Equal(141, StepCatalogLoader.ByName["set variable"].Id);
- }
-
- [Fact]
- public void AllEntries_WithIds_HaveValidIds()
- {
- foreach (var step in StepCatalogLoader.All.Where(s => s.Id.HasValue))
- {
- Assert.True(step.Id > 0, $"Step '{step.Name}' has invalid id {step.Id}");
- }
- }
-
- [Fact]
- public void AllEntries_HaveNoDuplicateIds()
- {
- var ids = StepCatalogLoader.All.Where(s => s.Id.HasValue).Select(s => s.Id!.Value).ToList();
- var distinct = ids.Distinct().ToList();
- Assert.Equal(distinct.Count, ids.Count);
- }
-
- [Fact]
- public void BlockPairSteps_HaveMatchingPartners()
- {
- var openSteps = StepCatalogLoader.All
- .Where(s => s.BlockPair?.Role == BlockPairRole.Open)
- .ToList();
-
- foreach (var step in openSteps)
- {
- foreach (var partner in step.BlockPair!.Partners)
- {
- Assert.True(
- StepCatalogLoader.ByName.ContainsKey(partner),
- $"Step '{step.Name}' has partner '{partner}' that doesn't exist in catalog");
- }
- }
- }
-}
diff --git a/tests/SharpFM.Tests/Scripting/StepCatalogTests.cs b/tests/SharpFM.Tests/Scripting/StepCatalogTests.cs
deleted file mode 100644
index 8a8e6ee..0000000
--- a/tests/SharpFM.Tests/Scripting/StepCatalogTests.cs
+++ /dev/null
@@ -1,150 +0,0 @@
-using System.Text.Json;
-using Xunit;
-
-namespace SharpFM.Tests.Scripting;
-
-public class StepCatalogTests
-{
- // --- BlockPairRole serialization (converter is on StepBlockPair.Role) ---
-
- [Theory]
- [InlineData("{\"role\":\"open\",\"partners\":[]}", BlockPairRole.Open)]
- [InlineData("{\"role\":\"middle\",\"partners\":[]}", BlockPairRole.Middle)]
- [InlineData("{\"role\":\"close\",\"partners\":[]}", BlockPairRole.Close)]
- public void BlockPairRole_Deserializes(string json, BlockPairRole expected)
- {
- var pair = JsonSerializer.Deserialize(json);
- Assert.Equal(expected, pair!.Role);
- }
-
- [Theory]
- [InlineData(BlockPairRole.Open, "open")]
- [InlineData(BlockPairRole.Middle, "middle")]
- [InlineData(BlockPairRole.Close, "close")]
- public void BlockPairRole_Serializes(BlockPairRole value, string expected)
- {
- var pair = new StepBlockPair { Role = value, Partners = [] };
- var json = JsonSerializer.Serialize(pair);
- Assert.Contains($"\"role\":\"{expected}\"", json);
- }
-
- [Fact]
- public void BlockPairRole_Deserialize_Unknown_Throws()
- {
- Assert.Throws(() =>
- JsonSerializer.Deserialize("{\"role\":\"unknown\",\"partners\":[]}"));
- }
-
- // --- StepDefinition record ---
-
- [Fact]
- public void StepDefinition_Deserializes_FromJson()
- {
- var json = """
- {
- "name": "Set Variable",
- "id": 141,
- "category": "Control",
- "selfClosing": true,
- "params": [
- { "xmlElement": "Name", "type": "text" }
- ]
- }
- """;
-
- var def = JsonSerializer.Deserialize(json);
- Assert.NotNull(def);
- Assert.Equal("Set Variable", def!.Name);
- Assert.Equal(141, def.Id);
- Assert.Equal("Control", def.Category);
- Assert.True(def.SelfClosing);
- Assert.Single(def.Params);
- Assert.Equal("Name", def.Params[0].XmlElement);
- }
-
- [Fact]
- public void StepDefinition_Defaults_AreCorrect()
- {
- var def = new StepDefinition();
- Assert.Equal("", def.Name);
- Assert.Null(def.Id);
- Assert.Equal("", def.Category);
- Assert.False(def.SelfClosing);
- Assert.Empty(def.Params);
- Assert.Null(def.BlockPair);
- }
-
- // --- StepParam record ---
-
- [Fact]
- public void StepParam_Deserializes_WithOptionalFields()
- {
- var json = """
- {
- "xmlElement": "Calculation",
- "type": "namedCalc",
- "hrLabel": "Value",
- "wrapperElement": "Value",
- "required": true,
- "invertedHr": true
- }
- """;
-
- var param = JsonSerializer.Deserialize(json);
- Assert.NotNull(param);
- Assert.Equal("Calculation", param!.XmlElement);
- Assert.Equal("namedCalc", param.Type);
- Assert.Equal("Value", param.HrLabel);
- Assert.Equal("Value", param.WrapperElement);
- Assert.True(param.Required);
- Assert.True(param.InvertedHr);
- }
-
- [Fact]
- public void StepParam_Defaults_AreCorrect()
- {
- var param = new StepParam();
- Assert.Equal("", param.XmlElement);
- Assert.Equal("", param.Type);
- Assert.Null(param.HrLabel);
- Assert.Null(param.XmlAttr);
- Assert.Null(param.WrapperElement);
- Assert.Null(param.ParentElement);
- Assert.False(param.Required);
- Assert.False(param.InvertedHr);
- }
-
- // --- StepBlockPair record ---
-
- [Fact]
- public void StepBlockPair_Deserializes()
- {
- var json = """{"role": "open", "partners": ["Else", "Else If", "End If"]}""";
-
- var pair = JsonSerializer.Deserialize(json);
- Assert.NotNull(pair);
- Assert.Equal(BlockPairRole.Open, pair!.Role);
- Assert.Equal(3, pair.Partners.Length);
- Assert.Contains("Else", pair.Partners);
- Assert.Contains("End If", pair.Partners);
- }
-
- // --- StepParam with hrEnumValues ---
-
- [Fact]
- public void StepParam_HrEnumValues_Deserializes()
- {
- var json = """
- {
- "xmlElement": "RowPageLocation",
- "type": "enum",
- "hrEnumValues": { "1": "First", "2": "Last", "3": "Previous", "4": "Next" }
- }
- """;
-
- var param = JsonSerializer.Deserialize(json);
- Assert.NotNull(param!.HrEnumValues);
- Assert.Equal("First", param.HrEnumValues!["1"]);
- Assert.Equal(4, param.HrEnumValues.Count);
- }
-}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/AVPlayerPlayStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/AVPlayerPlayStepTests.cs
new file mode 100644
index 0000000..8f8436b
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/AVPlayerPlayStepTests.cs
@@ -0,0 +1,27 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class AVPlayerPlayStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = AVPlayerPlayStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("AVPlayer Play", out var metadata));
+ Assert.Equal(177, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/AVPlayerSetOptionsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/AVPlayerSetOptionsStepTests.cs
new file mode 100644
index 0000000..86f87c8
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/AVPlayerSetOptionsStepTests.cs
@@ -0,0 +1,27 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class AVPlayerSetOptionsStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = AVPlayerSetOptionsStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("AVPlayer Set Options", out var metadata));
+ Assert.Equal(179, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/AVPlayerSetPlaybackStateStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/AVPlayerSetPlaybackStateStepTests.cs
new file mode 100644
index 0000000..654e86d
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/AVPlayerSetPlaybackStateStepTests.cs
@@ -0,0 +1,43 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class AVPlayerSetPlaybackStateStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = AVPlayerSetPlaybackStateStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsHrMappedValue()
+ {
+ var step = AVPlayerSetPlaybackStateStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal("AVPlayer Set Playback State [ Stopped ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void FromDisplay_ParsesHrValueBack()
+ {
+ var step = AVPlayerSetPlaybackStateStep.Metadata.FromDisplay!(true, new[] { "Stopped" });
+ Assert.True(XNode.DeepEquals(XElement.Parse(CanonicalXml), step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("AVPlayer Set Playback State", out var metadata));
+ Assert.Equal(178, metadata!.Id);
+ Assert.Single(metadata.Params);
+ Assert.Equal("enum", metadata.Params[0].Type);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/AddAccountStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/AddAccountStepTests.cs
new file mode 100644
index 0000000..6caf515
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/AddAccountStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class AddAccountStepTests
+{
+ private const string CanonicalXml = """$example""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = AddAccountStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = AddAccountStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = AddAccountStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Add Account", out var metadata));
+ Assert.Equal(134, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/AdjustWindowStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/AdjustWindowStepTests.cs
new file mode 100644
index 0000000..d50b272
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/AdjustWindowStepTests.cs
@@ -0,0 +1,43 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class AdjustWindowStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = AdjustWindowStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsHrMappedValue()
+ {
+ var step = AdjustWindowStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal("Adjust Window [ Resize to Fit ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void FromDisplay_ParsesHrValueBack()
+ {
+ var step = AdjustWindowStep.Metadata.FromDisplay!(true, new[] { "Resize to Fit" });
+ Assert.True(XNode.DeepEquals(XElement.Parse(CanonicalXml), step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Adjust Window", out var metadata));
+ Assert.Equal(31, metadata!.Id);
+ Assert.Single(metadata.Params);
+ Assert.Equal("enum", metadata.Params[0].Type);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/AllowFormattingBarStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/AllowFormattingBarStepTests.cs
new file mode 100644
index 0000000..6869d95
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/AllowFormattingBarStepTests.cs
@@ -0,0 +1,49 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class AllowFormattingBarStepTests
+{
+ private const string TrueStateXml = """""";
+ private const string FalseStateXml = """""";
+
+ [Fact]
+ public void RoundTrip_True_IsPreserved()
+ {
+ var source = XElement.Parse(TrueStateXml);
+ var step = AllowFormattingBarStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void RoundTrip_False_IsPreserved()
+ {
+ var source = XElement.Parse(FalseStateXml);
+ var step = AllowFormattingBarStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsExpectedFormat()
+ {
+ // Setting underlying prop=true renders as "On"
+ // (boolean: XML True displays as On).
+ var stepTrue = ((AllowFormattingBarStep)AllowFormattingBarStep.Metadata.FromXml!(XElement.Parse(TrueStateXml)));
+ Assert.Equal("Allow Formatting Bar [ On ]", stepTrue.ToDisplayLine());
+
+ var stepFalse = ((AllowFormattingBarStep)AllowFormattingBarStep.Metadata.FromXml!(XElement.Parse(FalseStateXml)));
+ Assert.Equal("Allow Formatting Bar [ Off ]", stepFalse.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Allow Formatting Bar", out var metadata));
+ Assert.Equal(115, metadata!.Id);
+ Assert.Single(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/AllowUserAbortStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/AllowUserAbortStepTests.cs
new file mode 100644
index 0000000..b297f8e
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/AllowUserAbortStepTests.cs
@@ -0,0 +1,49 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class AllowUserAbortStepTests
+{
+ private const string TrueStateXml = """""";
+ private const string FalseStateXml = """""";
+
+ [Fact]
+ public void RoundTrip_True_IsPreserved()
+ {
+ var source = XElement.Parse(TrueStateXml);
+ var step = AllowUserAbortStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void RoundTrip_False_IsPreserved()
+ {
+ var source = XElement.Parse(FalseStateXml);
+ var step = AllowUserAbortStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsExpectedFormat()
+ {
+ // Setting underlying prop=true renders as "On"
+ // (boolean: XML True displays as On).
+ var stepTrue = ((AllowUserAbortStep)AllowUserAbortStep.Metadata.FromXml!(XElement.Parse(TrueStateXml)));
+ Assert.Equal("Allow User Abort [ On ]", stepTrue.ToDisplayLine());
+
+ var stepFalse = ((AllowUserAbortStep)AllowUserAbortStep.Metadata.FromXml!(XElement.Parse(FalseStateXml)));
+ Assert.Equal("Allow User Abort [ Off ]", stepFalse.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Allow User Abort", out var metadata));
+ Assert.Equal(85, metadata!.Id);
+ Assert.Single(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ArrangeAllWindowsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ArrangeAllWindowsStepTests.cs
new file mode 100644
index 0000000..d7fe1f3
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ArrangeAllWindowsStepTests.cs
@@ -0,0 +1,43 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ArrangeAllWindowsStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ArrangeAllWindowsStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsHrMappedValue()
+ {
+ var step = ArrangeAllWindowsStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal("Arrange All Windows [ Tile Horizontally ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void FromDisplay_ParsesHrValueBack()
+ {
+ var step = ArrangeAllWindowsStep.Metadata.FromDisplay!(true, new[] { "Tile Horizontally" });
+ Assert.True(XNode.DeepEquals(XElement.Parse(CanonicalXml), step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Arrange All Windows", out var metadata));
+ Assert.Equal(120, metadata!.Id);
+ Assert.Single(metadata.Params);
+ Assert.Equal("enum", metadata.Params[0].Type);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/BeepStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/BeepStepTests.cs
new file mode 100644
index 0000000..3b06fa2
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/BeepStepTests.cs
@@ -0,0 +1,64 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-parameter POCO canonical pattern. Fixtures are inline raw string
+/// literals copied from agentic-fm's snippet_examples so the test reads
+/// without having to open a separate file.
+///
+public class BeepStepTests
+{
+ // Canonical shape from
+ // agent/snippet_examples/steps/miscellaneous/Beep.xml
+ private const string CanonicalSnippet = """
+
+
+
+
+ """;
+
+ private static XElement CanonicalStepElement() =>
+ XDocument.Parse(CanonicalSnippet).Root!.Element("Step")!;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = CanonicalStepElement();
+ var step = BeepStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new BeepStep();
+ Assert.Equal("Beep", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = BeepStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasBeep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Beep", out var metadata));
+ Assert.Equal(93, metadata!.Id);
+ Assert.Equal("miscellaneous", metadata.Category);
+ Assert.Null(metadata.BlockPair);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ChangePasswordStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ChangePasswordStepTests.cs
new file mode 100644
index 0000000..da1a63f
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ChangePasswordStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ChangePasswordStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ChangePasswordStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = ChangePasswordStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = ChangePasswordStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Change Password", out var metadata));
+ Assert.Equal(83, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/CheckFoundSetStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CheckFoundSetStepTests.cs
new file mode 100644
index 0000000..1876832
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/CheckFoundSetStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for CheckFoundSetStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class CheckFoundSetStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = CheckFoundSetStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new CheckFoundSetStep();
+ Assert.Equal("Check Found Set", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = CheckFoundSetStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Check Found Set", out var metadata));
+ Assert.Equal(20, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/CheckRecordStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CheckRecordStepTests.cs
new file mode 100644
index 0000000..93995b9
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/CheckRecordStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for CheckRecordStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class CheckRecordStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = CheckRecordStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new CheckRecordStep();
+ Assert.Equal("Check Record", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = CheckRecordStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Check Record", out var metadata));
+ Assert.Equal(19, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/CheckSelectionStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CheckSelectionStepTests.cs
new file mode 100644
index 0000000..01b23da
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/CheckSelectionStepTests.cs
@@ -0,0 +1,27 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class CheckSelectionStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = CheckSelectionStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Check Selection", out var metadata));
+ Assert.Equal(18, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ClearStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ClearStepTests.cs
new file mode 100644
index 0000000..c2f2c10
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ClearStepTests.cs
@@ -0,0 +1,27 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ClearStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ClearStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Clear", out var metadata));
+ Assert.Equal(49, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/CloseDataFileStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CloseDataFileStepTests.cs
new file mode 100644
index 0000000..a93f70b
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/CloseDataFileStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class CloseDataFileStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = CloseDataFileStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = CloseDataFileStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = CloseDataFileStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Close Data File", out var metadata));
+ Assert.Equal(196, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/CloseFileStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CloseFileStepTests.cs
new file mode 100644
index 0000000..7508958
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/CloseFileStepTests.cs
@@ -0,0 +1,44 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class CloseFileStepTests
+{
+ private const string CanonicalXml = """
+ file:NameOfFile
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = CloseFileStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_BareStep_UsesCurrentFile()
+ {
+ var bare = XElement.Parse("");
+ var step = CloseFileStep.Metadata.FromXml!(bare);
+ Assert.Equal("Close File [ Current File ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Display_WithFileReference_UsesName()
+ {
+ var step = (CloseFileStep)CloseFileStep.Metadata.FromXml!(XElement.Parse(
+ "file:Employees"));
+ Assert.Equal("Close File [ \"Employees\" ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Close File", out var metadata));
+ Assert.Equal(34, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ClosePopoverStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ClosePopoverStepTests.cs
new file mode 100644
index 0000000..0ad026c
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ClosePopoverStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for ClosePopoverStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class ClosePopoverStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ClosePopoverStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new ClosePopoverStep();
+ Assert.Equal("Close Popover", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = ClosePopoverStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Close Popover", out var metadata));
+ Assert.Equal(169, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/CloseWindowStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CloseWindowStepTests.cs
new file mode 100644
index 0000000..e3a45d8
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/CloseWindowStepTests.cs
@@ -0,0 +1,27 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class CloseWindowStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = CloseWindowStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Close Window", out var metadata));
+ Assert.Equal(121, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/CommentStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CommentStepTests.cs
index f362a37..1a051c4 100644
--- a/tests/SharpFM.Tests/Scripting/Steps/CommentStepTests.cs
+++ b/tests/SharpFM.Tests/Scripting/Steps/CommentStepTests.cs
@@ -226,8 +226,8 @@ public void FullRoundTrip_Script_WithMixedComments_Preserves()
// The multi-line comment must still be exactly ONE step after round-trip.
Assert.Equal(4, script2.Steps.Count);
- Assert.Equal("# (comment)", script2.Steps[0].Definition?.Name);
- Assert.Equal("# (comment)", script2.Steps[2].Definition?.Name);
+ Assert.IsType(script2.Steps[0]);
+ Assert.IsType(script2.Steps[2]);
var roundTrippedMultiLine = Assert.IsType(script2.Steps[2]);
Assert.Equal("line1\nline2\nline3", roundTrippedMultiLine.Text);
diff --git a/tests/SharpFM.Tests/Scripting/Steps/CommitRecordsRequestsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CommitRecordsRequestsStepTests.cs
new file mode 100644
index 0000000..aea7fad
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/CommitRecordsRequestsStepTests.cs
@@ -0,0 +1,67 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class CommitRecordsRequestsStepTests
+{
+ // Canonical FM Pro clipboard form — NoInteract=True means no dialog,
+ // which surfaces in display as "With dialog: Off".
+ private const string NoDialogXml = """
+
+ """;
+
+ private const string WithDialogXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_NoDialog_IsPreserved()
+ {
+ var source = XElement.Parse(NoDialogXml);
+ var step = CommitRecordsRequestsStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void RoundTrip_WithDialog_IsPreserved()
+ {
+ var source = XElement.Parse(WithDialogXml);
+ var step = CommitRecordsRequestsStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void InvertedHr_Display_Correct()
+ {
+ // NoInteract=True (XML) -> "With dialog: Off" (display).
+ var step = (CommitRecordsRequestsStep)CommitRecordsRequestsStep.Metadata.FromXml!(XElement.Parse(NoDialogXml));
+ Assert.False(step.WithDialog);
+ Assert.StartsWith("Commit Records/Requests [ With dialog: Off", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = CommitRecordsRequestsStep.Metadata.FromXml!(XElement.Parse(NoDialogXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = CommitRecordsRequestsStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Commit Records/Requests", out var metadata));
+ Assert.Equal(75, metadata!.Id);
+ Assert.Equal(3, metadata.Params.Count);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/CommitTransactionStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CommitTransactionStepTests.cs
new file mode 100644
index 0000000..ac18b23
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/CommitTransactionStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for CommitTransactionStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class CommitTransactionStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = CommitTransactionStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new CommitTransactionStep();
+ Assert.Equal("Commit Transaction", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = CommitTransactionStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Commit Transaction", out var metadata));
+ Assert.Equal(206, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ConfigureAIAccountStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ConfigureAIAccountStepTests.cs
new file mode 100644
index 0000000..bafd405
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ConfigureAIAccountStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ConfigureAIAccountStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ConfigureAIAccountStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = ConfigureAIAccountStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = ConfigureAIAccountStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Configure AI Account", out var metadata));
+ Assert.Equal(212, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ConfigureLocalNotificationStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ConfigureLocalNotificationStepTests.cs
new file mode 100644
index 0000000..1a3f169
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ConfigureLocalNotificationStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ConfigureLocalNotificationStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ConfigureLocalNotificationStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Configure Local Notification", out var metadata));
+ Assert.Equal(187, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ConfigureMachineLearningModelStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ConfigureMachineLearningModelStepTests.cs
new file mode 100644
index 0000000..45e7785
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ConfigureMachineLearningModelStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ConfigureMachineLearningModelStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ConfigureMachineLearningModelStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Configure Machine Learning Model", out var metadata));
+ Assert.Equal(202, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ConfigureNfcReadingStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ConfigureNfcReadingStepTests.cs
new file mode 100644
index 0000000..f1e1446
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ConfigureNfcReadingStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ConfigureNfcReadingStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ConfigureNfcReadingStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Configure NFC Reading", out var metadata));
+ Assert.Equal(201, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ConfigurePromptTemplateStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ConfigurePromptTemplateStepTests.cs
new file mode 100644
index 0000000..27a9c7d
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ConfigurePromptTemplateStepTests.cs
@@ -0,0 +1,27 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ConfigurePromptTemplateStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ConfigurePromptTemplateStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Configure Prompt Template", out var metadata));
+ Assert.Equal(226, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ConfigureRAGAccountStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ConfigureRAGAccountStepTests.cs
new file mode 100644
index 0000000..4ad8b21
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ConfigureRAGAccountStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ConfigureRAGAccountStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ConfigureRAGAccountStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = ConfigureRAGAccountStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = ConfigureRAGAccountStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Configure RAG Account ", out var metadata));
+ Assert.Equal(227, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ConfigureRegionMonitorScriptStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ConfigureRegionMonitorScriptStepTests.cs
new file mode 100644
index 0000000..f5bbd7f
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ConfigureRegionMonitorScriptStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ConfigureRegionMonitorScriptStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ConfigureRegionMonitorScriptStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Configure Region Monitor Script", out var metadata));
+ Assert.Equal(185, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ConfigureRegressionModelStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ConfigureRegressionModelStepTests.cs
new file mode 100644
index 0000000..99fd1c8
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ConfigureRegressionModelStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ConfigureRegressionModelStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ConfigureRegressionModelStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Configure Regression Model", out var metadata));
+ Assert.Equal(222, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ConstrainFoundSetStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ConstrainFoundSetStepTests.cs
new file mode 100644
index 0000000..c7fc5c1
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ConstrainFoundSetStepTests.cs
@@ -0,0 +1,39 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ConstrainFoundSetStepTests
+{
+ private const string CanonicalXml = """
+ $query
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ConstrainFoundSetStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Query_IsTyped()
+ {
+ var step = (ConstrainFoundSetStep)ConstrainFoundSetStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.NotNull(step.Query);
+ Assert.Single(step.Query!.Requests);
+ Assert.Equal("Include", step.Query.Requests[0].Operation);
+ Assert.Single(step.Query.Requests[0].Criteria);
+ Assert.Equal("$query", step.Query.Requests[0].Criteria[0].Query);
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Constrain Found Set", out var metadata));
+ Assert.Equal(126, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ConvertFileStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ConvertFileStepTests.cs
new file mode 100644
index 0000000..eb8fdb4
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ConvertFileStepTests.cs
@@ -0,0 +1,55 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ConvertFileStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ConvertFileStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = ConvertFileStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = ConvertFileStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void DataSourceType_Unlabeled_RoundTripsByPosition()
+ {
+ var xml = XElement.Parse("""
+
+ """);
+ var step = (ConvertFileStep)ConvertFileStep.Metadata.FromXml!(xml);
+ Assert.Equal("XMLSource", step.DataSourceType);
+ Assert.True(XNode.DeepEquals(xml, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Convert File", out var metadata));
+ Assert.Equal(139, metadata!.Id);
+ Assert.Equal(5, metadata.Params.Count);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/CopyAllRecordsRequestsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CopyAllRecordsRequestsStepTests.cs
new file mode 100644
index 0000000..5eb6692
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/CopyAllRecordsRequestsStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for CopyAllRecordsRequestsStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class CopyAllRecordsRequestsStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = CopyAllRecordsRequestsStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new CopyAllRecordsRequestsStep();
+ Assert.Equal("Copy All Records/Requests", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = CopyAllRecordsRequestsStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Copy All Records/Requests", out var metadata));
+ Assert.Equal(98, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/CopyRecordRequestStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CopyRecordRequestStepTests.cs
new file mode 100644
index 0000000..3709fd7
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/CopyRecordRequestStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for CopyRecordRequestStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class CopyRecordRequestStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = CopyRecordRequestStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new CopyRecordRequestStep();
+ Assert.Equal("Copy Record/Request", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = CopyRecordRequestStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Copy Record/Request", out var metadata));
+ Assert.Equal(101, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/CopyStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CopyStepTests.cs
new file mode 100644
index 0000000..343a35a
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/CopyStepTests.cs
@@ -0,0 +1,27 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class CopyStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = CopyStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Copy", out var metadata));
+ Assert.Equal(47, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/CorrectWordStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CorrectWordStepTests.cs
new file mode 100644
index 0000000..6384382
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/CorrectWordStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for CorrectWordStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class CorrectWordStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = CorrectWordStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new CorrectWordStep();
+ Assert.Equal("Correct Word", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = CorrectWordStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Correct Word", out var metadata));
+ Assert.Equal(106, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/CreateDataFileStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CreateDataFileStepTests.cs
new file mode 100644
index 0000000..d899c61
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/CreateDataFileStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class CreateDataFileStepTests
+{
+ private const string CanonicalXml = """$example""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = CreateDataFileStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = CreateDataFileStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = CreateDataFileStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Create Data File", out var metadata));
+ Assert.Equal(190, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/CutStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CutStepTests.cs
new file mode 100644
index 0000000..ef65dcf
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/CutStepTests.cs
@@ -0,0 +1,27 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class CutStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = CutStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Cut", out var metadata));
+ Assert.Equal(46, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/DeleteAccountStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/DeleteAccountStepTests.cs
new file mode 100644
index 0000000..46c37c0
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/DeleteAccountStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class DeleteAccountStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = DeleteAccountStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = DeleteAccountStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = DeleteAccountStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Delete Account", out var metadata));
+ Assert.Equal(135, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/DeleteAllRecordsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/DeleteAllRecordsStepTests.cs
new file mode 100644
index 0000000..7ba1edb
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/DeleteAllRecordsStepTests.cs
@@ -0,0 +1,49 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class DeleteAllRecordsStepTests
+{
+ private const string TrueStateXml = """""";
+ private const string FalseStateXml = """""";
+
+ [Fact]
+ public void RoundTrip_True_IsPreserved()
+ {
+ var source = XElement.Parse(TrueStateXml);
+ var step = DeleteAllRecordsStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void RoundTrip_False_IsPreserved()
+ {
+ var source = XElement.Parse(FalseStateXml);
+ var step = DeleteAllRecordsStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsExpectedFormat()
+ {
+ // Setting underlying prop=true renders as "Off"
+ // (invertedHr: XML True displays as Off).
+ var stepTrue = ((DeleteAllRecordsStep)DeleteAllRecordsStep.Metadata.FromXml!(XElement.Parse(TrueStateXml)));
+ Assert.Equal("Delete All Records [ With dialog: Off ]", stepTrue.ToDisplayLine());
+
+ var stepFalse = ((DeleteAllRecordsStep)DeleteAllRecordsStep.Metadata.FromXml!(XElement.Parse(FalseStateXml)));
+ Assert.Equal("Delete All Records [ With dialog: On ]", stepFalse.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Delete All Records", out var metadata));
+ Assert.Equal(10, metadata!.Id);
+ Assert.Single(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/DeleteFileStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/DeleteFileStepTests.cs
new file mode 100644
index 0000000..25b430a
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/DeleteFileStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class DeleteFileStepTests
+{
+ private const string CanonicalXml = """$path""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = DeleteFileStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsLabeledPath()
+ {
+ var step = (DeleteFileStep)DeleteFileStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal("Delete File [ Target file: $path ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void FromDisplay_ParsesLabeledPath()
+ {
+ var step = (DeleteFileStep)DeleteFileStep.Metadata.FromDisplay!(true, new[] { "Target file: $path" });
+ Assert.Equal("$path", step.TargetFile);
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Delete File", out var metadata));
+ Assert.Equal(197, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/DeletePortalRowStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/DeletePortalRowStepTests.cs
new file mode 100644
index 0000000..5688dac
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/DeletePortalRowStepTests.cs
@@ -0,0 +1,49 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class DeletePortalRowStepTests
+{
+ private const string TrueStateXml = """""";
+ private const string FalseStateXml = """""";
+
+ [Fact]
+ public void RoundTrip_True_IsPreserved()
+ {
+ var source = XElement.Parse(TrueStateXml);
+ var step = DeletePortalRowStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void RoundTrip_False_IsPreserved()
+ {
+ var source = XElement.Parse(FalseStateXml);
+ var step = DeletePortalRowStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsExpectedFormat()
+ {
+ // Setting underlying prop=true renders as "Off"
+ // (invertedHr: XML True displays as Off).
+ var stepTrue = ((DeletePortalRowStep)DeletePortalRowStep.Metadata.FromXml!(XElement.Parse(TrueStateXml)));
+ Assert.Equal("Delete Portal Row [ With dialog: Off ]", stepTrue.ToDisplayLine());
+
+ var stepFalse = ((DeletePortalRowStep)DeletePortalRowStep.Metadata.FromXml!(XElement.Parse(FalseStateXml)));
+ Assert.Equal("Delete Portal Row [ With dialog: On ]", stepFalse.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Delete Portal Row", out var metadata));
+ Assert.Equal(104, metadata!.Id);
+ Assert.Single(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/DeleteRecordRequestStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/DeleteRecordRequestStepTests.cs
new file mode 100644
index 0000000..57248de
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/DeleteRecordRequestStepTests.cs
@@ -0,0 +1,49 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class DeleteRecordRequestStepTests
+{
+ private const string TrueStateXml = """""";
+ private const string FalseStateXml = """""";
+
+ [Fact]
+ public void RoundTrip_True_IsPreserved()
+ {
+ var source = XElement.Parse(TrueStateXml);
+ var step = DeleteRecordRequestStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void RoundTrip_False_IsPreserved()
+ {
+ var source = XElement.Parse(FalseStateXml);
+ var step = DeleteRecordRequestStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsExpectedFormat()
+ {
+ // Setting underlying prop=true renders as "Off"
+ // (invertedHr: XML True displays as Off).
+ var stepTrue = ((DeleteRecordRequestStep)DeleteRecordRequestStep.Metadata.FromXml!(XElement.Parse(TrueStateXml)));
+ Assert.Equal("Delete Record/Request [ With dialog: Off ]", stepTrue.ToDisplayLine());
+
+ var stepFalse = ((DeleteRecordRequestStep)DeleteRecordRequestStep.Metadata.FromXml!(XElement.Parse(FalseStateXml)));
+ Assert.Equal("Delete Record/Request [ With dialog: On ]", stepFalse.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Delete Record/Request", out var metadata));
+ Assert.Equal(9, metadata!.Id);
+ Assert.Single(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/DialPhoneStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/DialPhoneStepTests.cs
new file mode 100644
index 0000000..b2125b5
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/DialPhoneStepTests.cs
@@ -0,0 +1,27 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class DialPhoneStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = DialPhoneStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Dial Phone", out var metadata));
+ Assert.Equal(65, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/DuplicateRecordRequestStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/DuplicateRecordRequestStepTests.cs
new file mode 100644
index 0000000..4921f6e
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/DuplicateRecordRequestStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for DuplicateRecordRequestStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class DuplicateRecordRequestStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = DuplicateRecordRequestStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new DuplicateRecordRequestStep();
+ Assert.Equal("Duplicate Record/Request", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = DuplicateRecordRequestStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Duplicate Record/Request", out var metadata));
+ Assert.Equal(8, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/EditUserDictionaryStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/EditUserDictionaryStepTests.cs
new file mode 100644
index 0000000..8167ba3
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/EditUserDictionaryStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for EditUserDictionaryStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class EditUserDictionaryStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = EditUserDictionaryStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new EditUserDictionaryStep();
+ Assert.Equal("Edit User Dictionary", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = EditUserDictionaryStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Edit User Dictionary", out var metadata));
+ Assert.Equal(109, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/EnableAccountStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/EnableAccountStepTests.cs
new file mode 100644
index 0000000..b689096
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/EnableAccountStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class EnableAccountStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = EnableAccountStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = EnableAccountStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = EnableAccountStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Enable Account", out var metadata));
+ Assert.Equal(137, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/EnableTouchKeyboardStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/EnableTouchKeyboardStepTests.cs
new file mode 100644
index 0000000..c5e5690
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/EnableTouchKeyboardStepTests.cs
@@ -0,0 +1,43 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class EnableTouchKeyboardStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = EnableTouchKeyboardStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsHrMappedValue()
+ {
+ var step = EnableTouchKeyboardStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal("Enable Touch Keyboard [ On ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void FromDisplay_ParsesHrValueBack()
+ {
+ var step = EnableTouchKeyboardStep.Metadata.FromDisplay!(true, new[] { "On" });
+ Assert.True(XNode.DeepEquals(XElement.Parse(CanonicalXml), step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Enable Touch Keyboard", out var metadata));
+ Assert.Equal(174, metadata!.Id);
+ Assert.Single(metadata.Params);
+ Assert.Equal("enum", metadata.Params[0].Type);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/EnterBrowseModeStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/EnterBrowseModeStepTests.cs
new file mode 100644
index 0000000..4477717
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/EnterBrowseModeStepTests.cs
@@ -0,0 +1,49 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class EnterBrowseModeStepTests
+{
+ private const string TrueStateXml = """""";
+ private const string FalseStateXml = """""";
+
+ [Fact]
+ public void RoundTrip_True_IsPreserved()
+ {
+ var source = XElement.Parse(TrueStateXml);
+ var step = EnterBrowseModeStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void RoundTrip_False_IsPreserved()
+ {
+ var source = XElement.Parse(FalseStateXml);
+ var step = EnterBrowseModeStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsExpectedFormat()
+ {
+ // Setting underlying prop=true renders as "On"
+ // (boolean: XML True displays as On).
+ var stepTrue = ((EnterBrowseModeStep)EnterBrowseModeStep.Metadata.FromXml!(XElement.Parse(TrueStateXml)));
+ Assert.Equal("Enter Browse Mode [ Pause: On ]", stepTrue.ToDisplayLine());
+
+ var stepFalse = ((EnterBrowseModeStep)EnterBrowseModeStep.Metadata.FromXml!(XElement.Parse(FalseStateXml)));
+ Assert.Equal("Enter Browse Mode [ Pause: Off ]", stepFalse.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Enter Browse Mode", out var metadata));
+ Assert.Equal(55, metadata!.Id);
+ Assert.Single(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/EnterFindModeStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/EnterFindModeStepTests.cs
new file mode 100644
index 0000000..9222aaa
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/EnterFindModeStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class EnterFindModeStepTests
+{
+ private const string CanonicalXml = """
+ $query
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = EnterFindModeStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Enter Find Mode", out var metadata));
+ Assert.Equal(22, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/EnterPreviewModeStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/EnterPreviewModeStepTests.cs
new file mode 100644
index 0000000..6982432
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/EnterPreviewModeStepTests.cs
@@ -0,0 +1,49 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class EnterPreviewModeStepTests
+{
+ private const string TrueStateXml = """""";
+ private const string FalseStateXml = """""";
+
+ [Fact]
+ public void RoundTrip_True_IsPreserved()
+ {
+ var source = XElement.Parse(TrueStateXml);
+ var step = EnterPreviewModeStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void RoundTrip_False_IsPreserved()
+ {
+ var source = XElement.Parse(FalseStateXml);
+ var step = EnterPreviewModeStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsExpectedFormat()
+ {
+ // Setting underlying prop=true renders as "On"
+ // (boolean: XML True displays as On).
+ var stepTrue = ((EnterPreviewModeStep)EnterPreviewModeStep.Metadata.FromXml!(XElement.Parse(TrueStateXml)));
+ Assert.Equal("Enter Preview Mode [ Pause: On ]", stepTrue.ToDisplayLine());
+
+ var stepFalse = ((EnterPreviewModeStep)EnterPreviewModeStep.Metadata.FromXml!(XElement.Parse(FalseStateXml)));
+ Assert.Equal("Enter Preview Mode [ Pause: Off ]", stepFalse.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Enter Preview Mode", out var metadata));
+ Assert.Equal(41, metadata!.Id);
+ Assert.Single(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ExecuteFileMakerDataApiStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ExecuteFileMakerDataApiStepTests.cs
new file mode 100644
index 0000000..26d5ce7
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ExecuteFileMakerDataApiStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ExecuteFileMakerDataApiStepTests
+{
+ private const string CanonicalXml = """
+ $response
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ExecuteFileMakerDataApiStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Execute FileMaker Data API", out var metadata));
+ Assert.Equal(203, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ExecuteSqlStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ExecuteSqlStepTests.cs
new file mode 100644
index 0000000..5bd818b
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ExecuteSqlStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ExecuteSqlStepTests
+{
+ private const string CanonicalXml = """
+ $sql_statement
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ExecuteSqlStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Execute SQL", out var metadata));
+ Assert.Equal(117, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ExitApplicationStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ExitApplicationStepTests.cs
new file mode 100644
index 0000000..a326733
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ExitApplicationStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for ExitApplicationStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class ExitApplicationStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ExitApplicationStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new ExitApplicationStep();
+ Assert.Equal("Exit Application", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = ExitApplicationStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Exit Application", out var metadata));
+ Assert.Equal(44, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ExitScriptStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ExitScriptStepTests.cs
new file mode 100644
index 0000000..6b080c9
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ExitScriptStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ExitScriptStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ExitScriptStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = ExitScriptStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = ExitScriptStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Exit Script", out var metadata));
+ Assert.Equal(103, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ExportFieldContentsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ExportFieldContentsStepTests.cs
new file mode 100644
index 0000000..9522a62
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ExportFieldContentsStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ExportFieldContentsStepTests
+{
+ private const string CanonicalXml = """
+ $path
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ExportFieldContentsStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Export Field Contents", out var metadata));
+ Assert.Equal(132, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ExportRecordsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ExportRecordsStepTests.cs
new file mode 100644
index 0000000..18e3e9b
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ExportRecordsStepTests.cs
@@ -0,0 +1,36 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ExportRecordsStepTests
+{
+ private const string CanonicalXml = """
+ $path
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ExportRecordsStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void ExportEntries_AreTyped()
+ {
+ var step = (ExportRecordsStep)ExportRecordsStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal(2, step.ExportEntries.Count);
+ Assert.Equal("name", step.ExportEntries[0].Field.Name);
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Export Records", out var metadata));
+ Assert.Equal(36, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ExtendFoundSetStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ExtendFoundSetStepTests.cs
new file mode 100644
index 0000000..7b3808c
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ExtendFoundSetStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ExtendFoundSetStepTests
+{
+ private const string CanonicalXml = """
+ $query
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ExtendFoundSetStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Extend Found Set", out var metadata));
+ Assert.Equal(127, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/FindMatchingRecordsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/FindMatchingRecordsStepTests.cs
new file mode 100644
index 0000000..0ce95ee
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/FindMatchingRecordsStepTests.cs
@@ -0,0 +1,35 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class FindMatchingRecordsStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = FindMatchingRecordsStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsModeAndField()
+ {
+ var step = (FindMatchingRecordsStep)FindMatchingRecordsStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal("Find Matching Records [ Constrain ; Customer::state (#1) ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Find Matching Records", out var metadata));
+ Assert.Equal(155, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/FineTuneModelStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/FineTuneModelStepTests.cs
new file mode 100644
index 0000000..04a8edb
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/FineTuneModelStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class FineTuneModelStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = FineTuneModelStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Fine-Tune Model", out var metadata));
+ Assert.Equal(213, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/FlushCacheToDiskStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/FlushCacheToDiskStepTests.cs
new file mode 100644
index 0000000..bbb6901
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/FlushCacheToDiskStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for FlushCacheToDiskStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class FlushCacheToDiskStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = FlushCacheToDiskStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new FlushCacheToDiskStep();
+ Assert.Equal("Flush Cache to Disk", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = FlushCacheToDiskStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Flush Cache to Disk", out var metadata));
+ Assert.Equal(102, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/FreezeWindowStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/FreezeWindowStepTests.cs
new file mode 100644
index 0000000..1dc535b
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/FreezeWindowStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for FreezeWindowStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class FreezeWindowStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = FreezeWindowStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new FreezeWindowStep();
+ Assert.Equal("Freeze Window", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = FreezeWindowStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Freeze Window", out var metadata));
+ Assert.Equal(79, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/GenerateResponseFromModelStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/GenerateResponseFromModelStepTests.cs
new file mode 100644
index 0000000..fa41f4c
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/GenerateResponseFromModelStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class GenerateResponseFromModelStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = GenerateResponseFromModelStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Generate Response from Model", out var metadata));
+ Assert.Equal(220, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/GetDataFilePositionStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/GetDataFilePositionStepTests.cs
new file mode 100644
index 0000000..68d8829
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/GetDataFilePositionStepTests.cs
@@ -0,0 +1,35 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class GetDataFilePositionStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = GetDataFilePositionStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsFileIdAndTarget()
+ {
+ var step = (GetDataFilePositionStep)GetDataFilePositionStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal("Get Data File Position [ File ID: $handle ; Target: Data::pos (#1) ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Get Data File Position", out var metadata));
+ Assert.Equal(194, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/GetFileExistsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/GetFileExistsStepTests.cs
new file mode 100644
index 0000000..7e5b234
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/GetFileExistsStepTests.cs
@@ -0,0 +1,35 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class GetFileExistsStepTests
+{
+ private const string CanonicalXml = """
+ $path
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = GetFileExistsStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsPathAndTarget()
+ {
+ var step = (GetFileExistsStep)GetFileExistsStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal("Get File Exists [ $path ; Target: Results::exists (#5) ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Get File Exists", out var metadata));
+ Assert.Equal(188, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/GetFileSizeStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/GetFileSizeStepTests.cs
new file mode 100644
index 0000000..d0ee637
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/GetFileSizeStepTests.cs
@@ -0,0 +1,35 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class GetFileSizeStepTests
+{
+ private const string CanonicalXml = """
+ $path
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = GetFileSizeStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsPathAndTarget()
+ {
+ var step = (GetFileSizeStep)GetFileSizeStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal("Get File Size [ $path ; Target: Results::size (#6) ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Get File Size", out var metadata));
+ Assert.Equal(189, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/GetFolderPathStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/GetFolderPathStepTests.cs
new file mode 100644
index 0000000..fe7e226
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/GetFolderPathStepTests.cs
@@ -0,0 +1,27 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class GetFolderPathStepTests
+{
+ private const string CanonicalXml = """$example""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = GetFolderPathStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Get Folder Path", out var metadata));
+ Assert.Equal(181, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/GoToFieldStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/GoToFieldStepTests.cs
new file mode 100644
index 0000000..cefc625
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/GoToFieldStepTests.cs
@@ -0,0 +1,27 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class GoToFieldStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = GoToFieldStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Go to Field", out var metadata));
+ Assert.Equal(17, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/GoToListOfRecordsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/GoToListOfRecordsStepTests.cs
new file mode 100644
index 0000000..65fc8b1
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/GoToListOfRecordsStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class GoToListOfRecordsStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = GoToListOfRecordsStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Go to List of Records", out var metadata));
+ Assert.Equal(228, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/GoToNextFieldStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/GoToNextFieldStepTests.cs
new file mode 100644
index 0000000..66b7481
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/GoToNextFieldStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for GoToNextFieldStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class GoToNextFieldStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = GoToNextFieldStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new GoToNextFieldStep();
+ Assert.Equal("Go to Next Field", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = GoToNextFieldStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Go to Next Field", out var metadata));
+ Assert.Equal(5, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/GoToObjectStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/GoToObjectStepTests.cs
new file mode 100644
index 0000000..b3870da
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/GoToObjectStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class GoToObjectStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = GoToObjectStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = GoToObjectStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = GoToObjectStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Go to Object", out var metadata));
+ Assert.Equal(145, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/GoToPortalRowStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/GoToPortalRowStepTests.cs
new file mode 100644
index 0000000..913dc0b
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/GoToPortalRowStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class GoToPortalRowStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = GoToPortalRowStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Go to Portal Row", out var metadata));
+ Assert.Equal(99, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/GoToPreviousFieldStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/GoToPreviousFieldStepTests.cs
new file mode 100644
index 0000000..18d6244
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/GoToPreviousFieldStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for GoToPreviousFieldStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class GoToPreviousFieldStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = GoToPreviousFieldStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new GoToPreviousFieldStep();
+ Assert.Equal("Go to Previous Field", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = GoToPreviousFieldStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Go to Previous Field", out var metadata));
+ Assert.Equal(4, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/GoToRelatedRecordStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/GoToRelatedRecordStepTests.cs
new file mode 100644
index 0000000..4945c10
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/GoToRelatedRecordStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class GoToRelatedRecordStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = GoToRelatedRecordStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Go to Related Record", out var metadata));
+ Assert.Equal(74, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/HaltScriptStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/HaltScriptStepTests.cs
new file mode 100644
index 0000000..ff2bd73
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/HaltScriptStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for HaltScriptStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class HaltScriptStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = HaltScriptStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new HaltScriptStep();
+ Assert.Equal("Halt Script", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = HaltScriptStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Halt Script", out var metadata));
+ Assert.Equal(90, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/IfStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/IfStepTests.cs
new file mode 100644
index 0000000..3b2a00b
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/IfStepTests.cs
@@ -0,0 +1,87 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using SharpFM.Model.Scripting.Values;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// IfStep pilot: calc + block-pair POCO with an intentional
+/// drop of the Restore param per the zero-loss audit
+/// in docs/advanced-filemaker-scripting-syntax.md.
+///
+public class IfStepTests
+{
+ // Canonical shape from agentic-fm's snippet — note the
+ // element that FM Pro itself never emits.
+ private const string AgenticFmSnippet = """
+
+
+
+
+ 0]]>
+
+
+ """;
+
+ // FM Pro clipboard form — no element at all.
+ private const string FmProStyleStep = """
+ 0]]>
+ """;
+
+ private static XElement StepFromSnippet(string snippet) =>
+ XDocument.Parse(snippet).Root!.Element("Step")!;
+
+ [Fact]
+ public void RoundTrip_WithRestoreInSource_DropsRestore()
+ {
+ // Input has ; output must not. This codifies the
+ // intentional drop described in the zero-loss audit.
+ var source = StepFromSnippet(AgenticFmSnippet);
+ var step = IfStep.Metadata.FromXml!(source);
+ var output = step.ToXml();
+
+ Assert.Null(output.Element("Restore"));
+ Assert.Equal("$error <> 0", output.Element("Calculation")!.Value);
+ }
+
+ [Fact]
+ public void RoundTrip_FmProStyle_IsByteIdentical()
+ {
+ var source = XElement.Parse(FmProStyleStep);
+ var step = IfStep.Metadata.FromXml!(source);
+
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsConditionInBrackets()
+ {
+ var step = new IfStep(true, new Calculation("$error <> 0"));
+ Assert.Equal("If [ $error <> 0 ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasIfWithBlockPair()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("If", out var metadata));
+ Assert.Equal(68, metadata!.Id);
+ Assert.Equal("control", metadata.Category);
+ Assert.NotNull(metadata.BlockPair);
+ Assert.Equal(BlockPairRole.Open, metadata.BlockPair!.Role);
+ Assert.Contains("End If", metadata.BlockPair.Partners);
+ Assert.Contains("Else", metadata.BlockPair.Partners);
+ Assert.Contains("Else If", metadata.BlockPair.Partners);
+ }
+
+ [Fact]
+ public void Metadata_Params_DescribeCalculation()
+ {
+ var metadata = StepRegistry.ByName["If"];
+ var param = Assert.Single(metadata.Params);
+ Assert.Equal("Calculation", param.XmlElement);
+ Assert.Equal("calculation", param.Type);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ImportRecordsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ImportRecordsStepTests.cs
new file mode 100644
index 0000000..74b1b8c
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ImportRecordsStepTests.cs
@@ -0,0 +1,37 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ImportRecordsStepTests
+{
+ private const string CanonicalXml = """
+ $path
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ImportRecordsStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void TargetFields_AreTyped()
+ {
+ var step = (ImportRecordsStep)ImportRecordsStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal(2, step.TargetFields.Count);
+ Assert.Equal("Import", step.TargetFields[0].Map);
+ Assert.Equal("DoNotImport", step.TargetFields[1].Map);
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Import Records", out var metadata));
+ Assert.Equal(35, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/InsertAudioVideoStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/InsertAudioVideoStepTests.cs
new file mode 100644
index 0000000..4307c2c
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/InsertAudioVideoStepTests.cs
@@ -0,0 +1,50 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class InsertAudioVideoStepTests
+{
+ private const string CanonicalXml = """
+ $path
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = InsertAudioVideoStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsPathAndReference()
+ {
+ var step = (InsertAudioVideoStep)InsertAudioVideoStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal("Insert Audio/Video [ $path ; Reference: Embedded ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = InsertAudioVideoStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = InsertAudioVideoStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Insert Audio/Video", out var metadata));
+ Assert.Equal(159, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/InsertCalculatedResultStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/InsertCalculatedResultStepTests.cs
new file mode 100644
index 0000000..8a6ccb6
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/InsertCalculatedResultStepTests.cs
@@ -0,0 +1,35 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class InsertCalculatedResultStepTests
+{
+ private const string CanonicalXml = """
+ $variable
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = InsertCalculatedResultStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsSelectTargetAndCalc()
+ {
+ var step = (InsertCalculatedResultStep)InsertCalculatedResultStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal("Insert Calculated Result [ Select ; Target: $variable ; \"calculation\" ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Insert Calculated Result", out var metadata));
+ Assert.Equal(77, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/InsertCurrentDateStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/InsertCurrentDateStepTests.cs
new file mode 100644
index 0000000..26667b1
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/InsertCurrentDateStepTests.cs
@@ -0,0 +1,27 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class InsertCurrentDateStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = InsertCurrentDateStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Insert Current Date", out var metadata));
+ Assert.Equal(13, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/InsertCurrentTimeStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/InsertCurrentTimeStepTests.cs
new file mode 100644
index 0000000..f08e6c3
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/InsertCurrentTimeStepTests.cs
@@ -0,0 +1,27 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class InsertCurrentTimeStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = InsertCurrentTimeStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Insert Current Time", out var metadata));
+ Assert.Equal(14, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/InsertCurrentUserNameStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/InsertCurrentUserNameStepTests.cs
new file mode 100644
index 0000000..5eb5721
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/InsertCurrentUserNameStepTests.cs
@@ -0,0 +1,27 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class InsertCurrentUserNameStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = InsertCurrentUserNameStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Insert Current User Name", out var metadata));
+ Assert.Equal(60, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/InsertEmbeddingInFoundSetStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/InsertEmbeddingInFoundSetStepTests.cs
new file mode 100644
index 0000000..6b0866e
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/InsertEmbeddingInFoundSetStepTests.cs
@@ -0,0 +1,37 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class InsertEmbeddingInFoundSetStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = InsertEmbeddingInFoundSetStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void FlagElements_PresenceMeansOn()
+ {
+ var step = (InsertEmbeddingInFoundSetStep)InsertEmbeddingInFoundSetStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.True(step.Overwrite);
+ Assert.True(step.ContinueOnError);
+ Assert.True(step.ShowSummary);
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Insert Embedding in Found Set", out var metadata));
+ Assert.Equal(216, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/InsertEmbeddingStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/InsertEmbeddingStepTests.cs
new file mode 100644
index 0000000..49c29b0
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/InsertEmbeddingStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class InsertEmbeddingStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = InsertEmbeddingStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Insert Embedding", out var metadata));
+ Assert.Equal(215, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/InsertFileStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/InsertFileStepTests.cs
new file mode 100644
index 0000000..38580f1
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/InsertFileStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class InsertFileStepTests
+{
+ private const string CanonicalXml = """
+ $path$file
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = InsertFileStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Insert File", out var metadata));
+ Assert.Equal(131, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/InsertFromDeviceStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/InsertFromDeviceStepTests.cs
new file mode 100644
index 0000000..e381c0e
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/InsertFromDeviceStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class InsertFromDeviceStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = InsertFromDeviceStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Insert from Device", out var metadata));
+ Assert.Equal(161, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/InsertFromIndexStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/InsertFromIndexStepTests.cs
new file mode 100644
index 0000000..144b1a4
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/InsertFromIndexStepTests.cs
@@ -0,0 +1,27 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class InsertFromIndexStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = InsertFromIndexStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Insert from Index", out var metadata));
+ Assert.Equal(11, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/InsertFromLastVisitedStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/InsertFromLastVisitedStepTests.cs
new file mode 100644
index 0000000..d77a003
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/InsertFromLastVisitedStepTests.cs
@@ -0,0 +1,27 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class InsertFromLastVisitedStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = InsertFromLastVisitedStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Insert from Last Visited", out var metadata));
+ Assert.Equal(12, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/InsertFromUrlStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/InsertFromUrlStepTests.cs
new file mode 100644
index 0000000..a84c5a6
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/InsertFromUrlStepTests.cs
@@ -0,0 +1,109 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class InsertFromUrlStepTests
+{
+ private const string CanonicalXml = """
+ $file
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = InsertFromUrlStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Insert from URL", out var metadata));
+ Assert.Equal(160, metadata!.Id);
+ }
+
+ [Theory]
+ [InlineData(false, false, true, false, false, false, false)]
+ [InlineData(true, false, false, false, false, false, false)]
+ [InlineData(false, false, true, false, true, true, true)]
+ [InlineData(true, false, true, false, true, true, true)]
+ [InlineData(true, false, true, true, true, true, true)]
+ [InlineData(true, true, true, true, true, true, true)]
+ public void RealWorldShapes_RoundTrip(
+ bool noInteract, bool dontEncode, bool selectAll, bool verifySsl,
+ bool withCurlOptions, bool withUrl, bool withField)
+ {
+ // Covers the series of variants in the user's FM Pro sample: base,
+ // base without Select, with calcs, set to variable, verify SSL on,
+ // and DontEncodeURL on. Each must XML-round-trip byte-intact.
+ var xml = new System.Text.StringBuilder();
+ xml.Append("");
+ xml.Append($"");
+ xml.Append($"");
+ xml.Append($"");
+ xml.Append($"");
+ if (withCurlOptions)
+ xml.Append("");
+ if (withUrl)
+ xml.Append("");
+ if (withField)
+ {
+ xml.Append("");
+ xml.Append("$insertFromUrlDataResponseVar");
+ }
+ xml.Append("");
+
+ var source = XElement.Parse(xml.ToString());
+ var step = InsertFromUrlStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()),
+ $"Round-trip mismatch.\nSource:\n{source}\n\nOutput:\n{step.ToXml()}");
+ }
+
+ [Fact]
+ public void NoInteract_True_RendersAsWithDialogOff()
+ {
+ // FM Pro semantics: NoInteract state="True" suppresses the dialog.
+ // Display must say "With dialog: Off".
+ var source = XElement.Parse(
+ ""
+ + ""
+ + ""
+ + "");
+ var step = (InsertFromUrlStep)InsertFromUrlStep.Metadata.FromXml!(source);
+ Assert.False(step.WithDialog);
+ Assert.Contains("With dialog: Off", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void NoInteract_False_RendersAsWithDialogOn()
+ {
+ var source = XElement.Parse(
+ ""
+ + ""
+ + ""
+ + "");
+ var step = (InsertFromUrlStep)InsertFromUrlStep.Metadata.FromXml!(source);
+ Assert.True(step.WithDialog);
+ Assert.Contains("With dialog: On", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Display_BaseShape_IsValidatorClean()
+ {
+ // Sanity: render the display line for a typical shape and feed
+ // it back through the script validator — zero diagnostics expected.
+ var source = XElement.Parse(
+ ""
+ + ""
+ + ""
+ + "");
+ var step = InsertFromUrlStep.Metadata.FromXml!(source);
+ var display = step.ToDisplayLine();
+ var diagnostics = SharpFM.Model.Scripting.ScriptValidator.Validate(display);
+ Assert.Empty(diagnostics);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/InsertPdfStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/InsertPdfStepTests.cs
new file mode 100644
index 0000000..ff4c92f
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/InsertPdfStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class InsertPdfStepTests
+{
+ private const string CanonicalXml = """
+ $path
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = InsertPdfStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Insert PDF", out var metadata));
+ Assert.Equal(158, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/InsertPictureStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/InsertPictureStepTests.cs
new file mode 100644
index 0000000..a39e647
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/InsertPictureStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class InsertPictureStepTests
+{
+ private const string CanonicalXml = """
+ $path
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = InsertPictureStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Insert Picture", out var metadata));
+ Assert.Equal(56, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/InsertTextStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/InsertTextStepTests.cs
new file mode 100644
index 0000000..cc193e5
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/InsertTextStepTests.cs
@@ -0,0 +1,35 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class InsertTextStepTests
+{
+ private const string CanonicalXml = """
+ text content$variable
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = InsertTextStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsSelectTargetAndText()
+ {
+ var step = (InsertTextStep)InsertTextStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal("Insert Text [ Select ; Target: $variable ; \"text content\" ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Insert Text", out var metadata));
+ Assert.Equal(61, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/InstallMenuSetStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/InstallMenuSetStepTests.cs
new file mode 100644
index 0000000..e82083e
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/InstallMenuSetStepTests.cs
@@ -0,0 +1,35 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class InstallMenuSetStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = InstallMenuSetStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsMenuSetAndFlag()
+ {
+ var step = (InstallMenuSetStep)InstallMenuSetStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal("Install Menu Set [ \"[Standard FileMaker Menus]\" ; Use as file default: Off ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Install Menu Set", out var metadata));
+ Assert.Equal(142, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/InstallOnTimerScriptStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/InstallOnTimerScriptStepTests.cs
new file mode 100644
index 0000000..bcedaa6
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/InstallOnTimerScriptStepTests.cs
@@ -0,0 +1,35 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class InstallOnTimerScriptStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = InstallOnTimerScriptStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsScriptAndInterval()
+ {
+ var step = (InstallOnTimerScriptStep)InstallOnTimerScriptStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal("Install OnTimer Script [ \"Refresh\" ; Interval: 30 ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Install OnTimer Script", out var metadata));
+ Assert.Equal(148, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/InstallPlugInFileStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/InstallPlugInFileStepTests.cs
new file mode 100644
index 0000000..fac11dd
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/InstallPlugInFileStepTests.cs
@@ -0,0 +1,27 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class InstallPlugInFileStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = InstallPlugInFileStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Install Plug-In File", out var metadata));
+ Assert.Equal(157, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ModifyLastFindStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ModifyLastFindStepTests.cs
new file mode 100644
index 0000000..06a2d39
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ModifyLastFindStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for ModifyLastFindStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class ModifyLastFindStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ModifyLastFindStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new ModifyLastFindStep();
+ Assert.Equal("Modify Last Find", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = ModifyLastFindStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Modify Last Find", out var metadata));
+ Assert.Equal(24, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/MoveResizeWindowStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/MoveResizeWindowStepTests.cs
new file mode 100644
index 0000000..0510e60
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/MoveResizeWindowStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class MoveResizeWindowStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = MoveResizeWindowStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = MoveResizeWindowStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = MoveResizeWindowStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Move/Resize Window", out var metadata));
+ Assert.Equal(119, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/NewFileStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/NewFileStepTests.cs
new file mode 100644
index 0000000..295ac03
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/NewFileStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for NewFileStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class NewFileStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = NewFileStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new NewFileStep();
+ Assert.Equal("New File", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = NewFileStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("New File", out var metadata));
+ Assert.Equal(82, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/NewRecordRequestStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/NewRecordRequestStepTests.cs
new file mode 100644
index 0000000..717ece2
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/NewRecordRequestStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for NewRecordRequestStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class NewRecordRequestStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = NewRecordRequestStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new NewRecordRequestStep();
+ Assert.Equal("New Record/Request", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = NewRecordRequestStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("New Record/Request", out var metadata));
+ Assert.Equal(7, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/NewWindowStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/NewWindowStepTests.cs
new file mode 100644
index 0000000..4979cdc
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/NewWindowStepTests.cs
@@ -0,0 +1,27 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class NewWindowStepTests
+{
+ private const string CanonicalXml = """$example""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = NewWindowStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("New Window", out var metadata));
+ Assert.Equal(122, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/NoInteractSemanticTests.cs b/tests/SharpFM.Tests/Scripting/Steps/NoInteractSemanticTests.cs
new file mode 100644
index 0000000..21a431f
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/NoInteractSemanticTests.cs
@@ -0,0 +1,166 @@
+using System.Linq;
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Every step with a <NoInteract state="..."> child uses the
+/// FileMaker convention where state="True" means "suppress the
+/// dialog" = display text "With dialog: Off". This test locks that
+/// invariant across all NoInteract-bearing POCOs so the mapping can't
+/// silently invert in any of them again.
+///
+///
+/// The test drives FromXml → ToDisplayLine with both states and asserts
+/// the expected "With dialog: On/Off" substring. Steps whose display form
+/// doesn't render the flag at all (it's hidden because the step carries
+/// no other user-visible state worth showing it against) are filtered.
+///
+///
+public class NoInteractSemanticTests
+{
+ [Theory]
+ [InlineData("Change Password", 39)]
+ [InlineData("Commit Records/Requests", 75)]
+ [InlineData("Convert File", 181)]
+ [InlineData("Delete All Records", 24)]
+ [InlineData("Delete Portal Row", 50)]
+ [InlineData("Delete Record/Request", 26)]
+ [InlineData("Dial Phone", 65)]
+ [InlineData("Execute SQL", 117)]
+ [InlineData("Export Records", 36)]
+ [InlineData("Go to Portal Row", 99)]
+ [InlineData("Go to Record/Request/Page", 16)]
+ [InlineData("Import Records", 35)]
+ [InlineData("Insert from URL", 160)]
+ [InlineData("Omit Multiple Records", 19)]
+ [InlineData("Open URL", 150)]
+ [InlineData("Perform Find/Replace", 128)]
+ [InlineData("Print", 43)]
+ [InlineData("Print Setup", 42)]
+ [InlineData("Re-Login", 136)]
+ [InlineData("Recover File", 32)]
+ [InlineData("Relookup Field Contents", 63)]
+ [InlineData("Replace Field Contents", 91)]
+ [InlineData("Revert Record/Request", 19)]
+ [InlineData("Save Records as Excel", 143)]
+ [InlineData("Save Records as PDF", 144)]
+ [InlineData("Send Mail", 63)]
+ [InlineData("Sort Records", 39)]
+ [InlineData("Trigger Claris Connect Flow", 0)]
+ [InlineData("Truncate Table", 182)]
+ public void NoInteract_True_DisplaysAsDialogOff(string stepName, int id)
+ {
+ var step = ParseWithNoInteract(stepName, id, state: "True");
+ var display = step.ToDisplayLine();
+ Assert.DoesNotContain("With dialog: On", display);
+ // We allow steps whose display hides the flag entirely; when it
+ // IS rendered, it must say "Off" for NoInteract=True.
+ if (display.Contains("With dialog:"))
+ Assert.Contains("With dialog: Off", display);
+ }
+
+ [Theory]
+ [InlineData("Change Password", 39)]
+ [InlineData("Commit Records/Requests", 75)]
+ [InlineData("Convert File", 181)]
+ [InlineData("Delete All Records", 24)]
+ [InlineData("Delete Portal Row", 50)]
+ [InlineData("Delete Record/Request", 26)]
+ [InlineData("Dial Phone", 65)]
+ [InlineData("Execute SQL", 117)]
+ [InlineData("Export Records", 36)]
+ [InlineData("Go to Portal Row", 99)]
+ [InlineData("Go to Record/Request/Page", 16)]
+ [InlineData("Import Records", 35)]
+ [InlineData("Insert from URL", 160)]
+ [InlineData("Omit Multiple Records", 19)]
+ [InlineData("Open URL", 150)]
+ [InlineData("Perform Find/Replace", 128)]
+ [InlineData("Print", 43)]
+ [InlineData("Print Setup", 42)]
+ [InlineData("Re-Login", 136)]
+ [InlineData("Recover File", 32)]
+ [InlineData("Relookup Field Contents", 63)]
+ [InlineData("Replace Field Contents", 91)]
+ [InlineData("Revert Record/Request", 19)]
+ [InlineData("Save Records as Excel", 143)]
+ [InlineData("Save Records as PDF", 144)]
+ [InlineData("Send Mail", 63)]
+ [InlineData("Sort Records", 39)]
+ [InlineData("Trigger Claris Connect Flow", 0)]
+ [InlineData("Truncate Table", 182)]
+ public void NoInteract_False_DisplaysAsDialogOn(string stepName, int id)
+ {
+ var step = ParseWithNoInteract(stepName, id, state: "False");
+ var display = step.ToDisplayLine();
+ Assert.DoesNotContain("With dialog: Off", display);
+ if (display.Contains("With dialog:"))
+ Assert.Contains("With dialog: On", display);
+ }
+
+ [Fact]
+ public void EveryNoInteractStep_IsInTheory()
+ {
+ // Discover every POCO whose ToXml emits a child and
+ // confirm it's covered by the Theory data above. New POCOs with
+ // NoInteract must opt in here so the inversion rule stays enforced.
+ var noInteractSteps = StepRegistry.All
+ .Select(m => m.Name)
+ .Where(name =>
+ {
+ if (m(name) is not { } step) return false;
+ return step.ToXml().Element("NoInteract") is not null;
+ })
+ .ToHashSet();
+
+ var covered = new System.Collections.Generic.HashSet
+ {
+ "Change Password","Commit Records/Requests","Convert File",
+ "Delete All Records","Delete Portal Row","Delete Record/Request",
+ "Dial Phone","Execute SQL","Export Records","Go to Portal Row",
+ "Go to Record/Request/Page","Import Records","Insert from URL",
+ "Omit Multiple Records","Open URL","Perform Find/Replace",
+ "Print","Print Setup","Re-Login","Recover File",
+ "Relookup Field Contents","Replace Field Contents",
+ "Revert Record/Request","Save Records as Excel",
+ "Save Records as PDF","Send Mail","Sort Records",
+ "Trigger Claris Connect Flow","Truncate Table",
+ };
+
+ var missing = noInteractSteps.Except(covered).ToList();
+ Assert.True(missing.Count == 0,
+ "POCOs emit but aren't in the Theory data above: "
+ + string.Join(", ", missing));
+ }
+
+ private static SharpFM.Model.Scripting.ScriptStep ParseWithNoInteract(string name, int id, string state)
+ {
+ var xml = new XElement("Step",
+ new XAttribute("enable", "True"),
+ new XAttribute("id", id),
+ new XAttribute("name", name),
+ new XElement("NoInteract", new XAttribute("state", state)));
+ return StepRegistry.ByName[name].FromXml!(xml);
+ }
+
+ // Helper used inside the discovery LINQ: construct the POCO from a
+ // BARE (no NoInteract child). Typed POCOs always emit their
+ // flag children regardless of input shape, so NoInteract appears in
+ // the output. The StepChildBag-backed AI/ML POCOs echo whatever
+ // children their input had — with no NoInteract in the probe input,
+ // they won't emit one either, and are correctly excluded.
+ private static SharpFM.Model.Scripting.ScriptStep? m(string stepName)
+ {
+ if (!StepRegistry.ByName.TryGetValue(stepName, out var meta)) return null;
+ if (meta.FromXml is null) return null;
+ var xml = new XElement("Step",
+ new XAttribute("enable", "True"),
+ new XAttribute("id", meta.Id),
+ new XAttribute("name", stepName));
+ try { return meta.FromXml(xml); }
+ catch { return null; }
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/OmitMultipleRecordsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OmitMultipleRecordsStepTests.cs
new file mode 100644
index 0000000..f651b40
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/OmitMultipleRecordsStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class OmitMultipleRecordsStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = OmitMultipleRecordsStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = OmitMultipleRecordsStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = OmitMultipleRecordsStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Omit Multiple Records", out var metadata));
+ Assert.Equal(26, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/OmitRecordStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OmitRecordStepTests.cs
new file mode 100644
index 0000000..f56680f
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/OmitRecordStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for OmitRecordStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class OmitRecordStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = OmitRecordStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new OmitRecordStep();
+ Assert.Equal("Omit Record", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = OmitRecordStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Omit Record", out var metadata));
+ Assert.Equal(25, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenDataFileStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenDataFileStepTests.cs
new file mode 100644
index 0000000..ee12f25
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/OpenDataFileStepTests.cs
@@ -0,0 +1,35 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class OpenDataFileStepTests
+{
+ private const string CanonicalXml = """
+ $path
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = OpenDataFileStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsPathAndTarget()
+ {
+ var step = (OpenDataFileStep)OpenDataFileStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal("Open Data File [ $path ; Target: Data::handle (#1) ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Open Data File", out var metadata));
+ Assert.Equal(191, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenEditSavedFindsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenEditSavedFindsStepTests.cs
new file mode 100644
index 0000000..7e307cf
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/OpenEditSavedFindsStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for OpenEditSavedFindsStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class OpenEditSavedFindsStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = OpenEditSavedFindsStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new OpenEditSavedFindsStep();
+ Assert.Equal("Open Edit Saved Finds", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = OpenEditSavedFindsStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Open Edit Saved Finds", out var metadata));
+ Assert.Equal(149, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenFavoritesStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenFavoritesStepTests.cs
new file mode 100644
index 0000000..996adf7
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/OpenFavoritesStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for OpenFavoritesStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class OpenFavoritesStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = OpenFavoritesStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new OpenFavoritesStep();
+ Assert.Equal("Open Favorites", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = OpenFavoritesStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Open Favorites", out var metadata));
+ Assert.Equal(183, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenFileOptionsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenFileOptionsStepTests.cs
new file mode 100644
index 0000000..1ad14ca
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/OpenFileOptionsStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for OpenFileOptionsStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class OpenFileOptionsStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = OpenFileOptionsStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new OpenFileOptionsStep();
+ Assert.Equal("Open File Options", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = OpenFileOptionsStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Open File Options", out var metadata));
+ Assert.Equal(114, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenFileStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenFileStepTests.cs
new file mode 100644
index 0000000..a28163e
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/OpenFileStepTests.cs
@@ -0,0 +1,44 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class OpenFileStepTests
+{
+ private const string CanonicalXml = """
+ file:NameOfFile
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = OpenFileStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsHiddenFlagAndFileName()
+ {
+ var step = (OpenFileStep)OpenFileStep.Metadata.FromXml!(XElement.Parse(
+ ""));
+ Assert.Equal("Open File [ Open hidden: On ; \"Books\" ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Display_WithoutFile_OmitsFileToken()
+ {
+ var step = (OpenFileStep)OpenFileStep.Metadata.FromXml!(XElement.Parse(
+ ""));
+ Assert.Equal("Open File [ Open hidden: Off ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Open File", out var metadata));
+ Assert.Equal(33, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenFindReplaceStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenFindReplaceStepTests.cs
new file mode 100644
index 0000000..ce1a878
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/OpenFindReplaceStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for OpenFindReplaceStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class OpenFindReplaceStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = OpenFindReplaceStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new OpenFindReplaceStep();
+ Assert.Equal("Open Find/Replace", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = OpenFindReplaceStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Open Find/Replace", out var metadata));
+ Assert.Equal(129, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenHelpStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenHelpStepTests.cs
new file mode 100644
index 0000000..7cbef4a
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/OpenHelpStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for OpenHelpStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class OpenHelpStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = OpenHelpStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new OpenHelpStep();
+ Assert.Equal("Open Help", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = OpenHelpStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Open Help", out var metadata));
+ Assert.Equal(32, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenHostsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenHostsStepTests.cs
new file mode 100644
index 0000000..6333564
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/OpenHostsStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for OpenHostsStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class OpenHostsStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = OpenHostsStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new OpenHostsStep();
+ Assert.Equal("Open Hosts", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = OpenHostsStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Open Hosts", out var metadata));
+ Assert.Equal(118, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenManageContainersStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenManageContainersStepTests.cs
new file mode 100644
index 0000000..f37c840
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/OpenManageContainersStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for OpenManageContainersStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class OpenManageContainersStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = OpenManageContainersStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new OpenManageContainersStep();
+ Assert.Equal("Open Manage Containers", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = OpenManageContainersStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Open Manage Containers", out var metadata));
+ Assert.Equal(156, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenManageDataSourcesStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenManageDataSourcesStepTests.cs
new file mode 100644
index 0000000..01472c8
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/OpenManageDataSourcesStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for OpenManageDataSourcesStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class OpenManageDataSourcesStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = OpenManageDataSourcesStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new OpenManageDataSourcesStep();
+ Assert.Equal("Open Manage Data Sources", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = OpenManageDataSourcesStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Open Manage Data Sources", out var metadata));
+ Assert.Equal(140, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenManageDatabaseStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenManageDatabaseStepTests.cs
new file mode 100644
index 0000000..8a407a1
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/OpenManageDatabaseStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for OpenManageDatabaseStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class OpenManageDatabaseStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = OpenManageDatabaseStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new OpenManageDatabaseStep();
+ Assert.Equal("Open Manage Database", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = OpenManageDatabaseStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Open Manage Database", out var metadata));
+ Assert.Equal(38, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenManageLayoutsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenManageLayoutsStepTests.cs
new file mode 100644
index 0000000..ec8a830
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/OpenManageLayoutsStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for OpenManageLayoutsStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class OpenManageLayoutsStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = OpenManageLayoutsStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new OpenManageLayoutsStep();
+ Assert.Equal("Open Manage Layouts", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = OpenManageLayoutsStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Open Manage Layouts", out var metadata));
+ Assert.Equal(151, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenManageThemesStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenManageThemesStepTests.cs
new file mode 100644
index 0000000..2e050db
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/OpenManageThemesStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for OpenManageThemesStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class OpenManageThemesStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = OpenManageThemesStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new OpenManageThemesStep();
+ Assert.Equal("Open Manage Themes", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = OpenManageThemesStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Open Manage Themes", out var metadata));
+ Assert.Equal(165, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenManageValueListsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenManageValueListsStepTests.cs
new file mode 100644
index 0000000..196c1f8
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/OpenManageValueListsStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for OpenManageValueListsStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class OpenManageValueListsStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = OpenManageValueListsStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new OpenManageValueListsStep();
+ Assert.Equal("Open Manage Value Lists", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = OpenManageValueListsStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Open Manage Value Lists", out var metadata));
+ Assert.Equal(112, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenRecordRequestStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenRecordRequestStepTests.cs
new file mode 100644
index 0000000..9b3e331
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/OpenRecordRequestStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for OpenRecordRequestStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class OpenRecordRequestStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = OpenRecordRequestStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new OpenRecordRequestStep();
+ Assert.Equal("Open Record/Request", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = OpenRecordRequestStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Open Record/Request", out var metadata));
+ Assert.Equal(133, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenScriptWorkspaceStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenScriptWorkspaceStepTests.cs
new file mode 100644
index 0000000..5962617
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/OpenScriptWorkspaceStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for OpenScriptWorkspaceStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class OpenScriptWorkspaceStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = OpenScriptWorkspaceStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new OpenScriptWorkspaceStep();
+ Assert.Equal("Open Script Workspace", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = OpenScriptWorkspaceStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Open Script Workspace", out var metadata));
+ Assert.Equal(88, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenSettingsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenSettingsStepTests.cs
new file mode 100644
index 0000000..3191afe
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/OpenSettingsStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for OpenSettingsStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class OpenSettingsStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = OpenSettingsStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new OpenSettingsStep();
+ Assert.Equal("Open Settings", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = OpenSettingsStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Open Settings", out var metadata));
+ Assert.Equal(105, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenSharingStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenSharingStepTests.cs
new file mode 100644
index 0000000..86c55eb
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/OpenSharingStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for OpenSharingStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class OpenSharingStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = OpenSharingStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new OpenSharingStep();
+ Assert.Equal("Open Sharing", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = OpenSharingStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Open Sharing", out var metadata));
+ Assert.Equal(113, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenTransactionStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenTransactionStepTests.cs
new file mode 100644
index 0000000..ca9acf8
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/OpenTransactionStepTests.cs
@@ -0,0 +1,60 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class OpenTransactionStepTests
+{
+ // Restore is intentionally dropped per the zero-loss audit — it's
+ // semantically fixed and carries no information. Test fixture omits
+ // it so round-trip is byte-identical against what our POCO emits.
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = OpenTransactionStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = OpenTransactionStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = OpenTransactionStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void RestoreElement_InSource_IsIntentionallyDropped()
+ {
+ // Upstream agentic-fm snippets include ,
+ // which we drop on both read and write.
+ var withRestore = XElement.Parse("""
+
+ """);
+ var step = OpenTransactionStep.Metadata.FromXml!(withRestore);
+ Assert.Null(step.ToXml().Element("Restore"));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Open Transaction", out var metadata));
+ Assert.Equal(205, metadata!.Id);
+ Assert.Equal(3, metadata.Params.Count);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenURLStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenURLStepTests.cs
new file mode 100644
index 0000000..d3b842d
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/OpenURLStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class OpenURLStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = OpenURLStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = OpenURLStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = OpenURLStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Open URL", out var metadata));
+ Assert.Equal(111, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenUploadToHostStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenUploadToHostStepTests.cs
new file mode 100644
index 0000000..fd58d08
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/OpenUploadToHostStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for OpenUploadToHostStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class OpenUploadToHostStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = OpenUploadToHostStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new OpenUploadToHostStep();
+ Assert.Equal("Open Upload to Host", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = OpenUploadToHostStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Open Upload to Host", out var metadata));
+ Assert.Equal(172, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/PasteStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/PasteStepTests.cs
new file mode 100644
index 0000000..3344387
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/PasteStepTests.cs
@@ -0,0 +1,27 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class PasteStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = PasteStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Paste", out var metadata));
+ Assert.Equal(48, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/PauseResumeScriptStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/PauseResumeScriptStepTests.cs
new file mode 100644
index 0000000..b5d15c7
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/PauseResumeScriptStepTests.cs
@@ -0,0 +1,27 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class PauseResumeScriptStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = PauseResumeScriptStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Pause/Resume Script", out var metadata));
+ Assert.Equal(62, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/PerformAppleScriptStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/PerformAppleScriptStepTests.cs
new file mode 100644
index 0000000..13c0e15
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/PerformAppleScriptStepTests.cs
@@ -0,0 +1,27 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class PerformAppleScriptStepTests
+{
+ private const string CanonicalXml = """$example""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = PerformAppleScriptStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Perform AppleScript", out var metadata));
+ Assert.Equal(67, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/PerformFindByNaturalLanguageStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/PerformFindByNaturalLanguageStepTests.cs
new file mode 100644
index 0000000..4febef2
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/PerformFindByNaturalLanguageStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class PerformFindByNaturalLanguageStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = PerformFindByNaturalLanguageStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Perform Find by Natural Language", out var metadata));
+ Assert.Equal(221, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/PerformFindReplaceStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/PerformFindReplaceStepTests.cs
new file mode 100644
index 0000000..fb968a4
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/PerformFindReplaceStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class PerformFindReplaceStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = PerformFindReplaceStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Perform Find/Replace", out var metadata));
+ Assert.Equal(128, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/PerformFindStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/PerformFindStepTests.cs
new file mode 100644
index 0000000..d46ff14
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/PerformFindStepTests.cs
@@ -0,0 +1,37 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class PerformFindStepTests
+{
+ private const string CanonicalXml = """
+ $query1
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = PerformFindStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void MultipleRequests_ExcludeOperationIsPreserved()
+ {
+ var step = (PerformFindStep)PerformFindStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal(2, step.Query!.Requests.Count);
+ Assert.Equal("Include", step.Query.Requests[0].Operation);
+ Assert.Equal("Exclude", step.Query.Requests[1].Operation);
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Perform Find", out var metadata));
+ Assert.Equal(28, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/PerformJavaScriptInWebViewerStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/PerformJavaScriptInWebViewerStepTests.cs
new file mode 100644
index 0000000..bfeb455
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/PerformJavaScriptInWebViewerStepTests.cs
@@ -0,0 +1,36 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class PerformJavaScriptInWebViewerStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = PerformJavaScriptInWebViewerStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Parameters_AreTyped()
+ {
+ var step = (PerformJavaScriptInWebViewerStep)PerformJavaScriptInWebViewerStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Single(step.Parameters);
+ Assert.Equal("$parameter1", step.Parameters[0].Text);
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Perform JavaScript in Web Viewer", out var metadata));
+ Assert.Equal(175, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/PerformQuickFindStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/PerformQuickFindStepTests.cs
new file mode 100644
index 0000000..d9e50df
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/PerformQuickFindStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class PerformQuickFindStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = PerformQuickFindStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = PerformQuickFindStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = PerformQuickFindStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Perform Quick Find", out var metadata));
+ Assert.Equal(150, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/PerformRagActionStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/PerformRagActionStepTests.cs
new file mode 100644
index 0000000..d933210
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/PerformRagActionStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class PerformRagActionStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = PerformRagActionStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Perform RAG Action", out var metadata));
+ Assert.Equal(219, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/PerformScriptOnServerStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/PerformScriptOnServerStepTests.cs
new file mode 100644
index 0000000..67a2f59
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/PerformScriptOnServerStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class PerformScriptOnServerStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = PerformScriptOnServerStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Perform Script on Server", out var metadata));
+ Assert.Equal(164, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/PerformScriptOnServerWithCallbackStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/PerformScriptOnServerWithCallbackStepTests.cs
new file mode 100644
index 0000000..75dc49b
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/PerformScriptOnServerWithCallbackStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class PerformScriptOnServerWithCallbackStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = PerformScriptOnServerWithCallbackStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Perform Script on Server with Callback", out var metadata));
+ Assert.Equal(210, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/PerformSemanticFindStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/PerformSemanticFindStepTests.cs
new file mode 100644
index 0000000..c998988
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/PerformSemanticFindStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class PerformSemanticFindStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = PerformSemanticFindStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Perform Semantic Find", out var metadata));
+ Assert.Equal(218, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/PerformSqlQueryByNaturalLanguageStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/PerformSqlQueryByNaturalLanguageStepTests.cs
new file mode 100644
index 0000000..40a9386
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/PerformSqlQueryByNaturalLanguageStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class PerformSqlQueryByNaturalLanguageStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = PerformSqlQueryByNaturalLanguageStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Perform SQL Query by Natural Language", out var metadata));
+ Assert.Equal(214, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/PrintSetupStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/PrintSetupStepTests.cs
new file mode 100644
index 0000000..dfdf1a4
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/PrintSetupStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class PrintSetupStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = PrintSetupStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Print Setup", out var metadata));
+ Assert.Equal(42, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/PrintStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/PrintStepTests.cs
new file mode 100644
index 0000000..8f1cd5f
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/PrintStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class PrintStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = PrintStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Print", out var metadata));
+ Assert.Equal(43, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/RawStepAllowListTests.cs b/tests/SharpFM.Tests/Scripting/Steps/RawStepAllowListTests.cs
deleted file mode 100644
index 8a1a249..0000000
--- a/tests/SharpFM.Tests/Scripting/Steps/RawStepAllowListTests.cs
+++ /dev/null
@@ -1,72 +0,0 @@
-using System.Xml.Linq;
-using SharpFM.Model.Scripting;
-using SharpFM.Model.Scripting.Serialization;
-using SharpFM.Model.Scripting.Steps;
-using SharpFM.Model.Scripting.Values;
-using Xunit;
-
-namespace SharpFM.Tests.Scripting.Steps;
-
-///
-/// Contract tests for the "sealed step" mechanism: typed POCOs are
-/// always fully editable, RawSteps default to sealed (not editable)
-/// unless explicitly granted via . The
-/// two mechanisms must be mutually exclusive — a POCO-backed step is
-/// never on the allow-list, otherwise there would be two sources of
-/// truth for editability.
-///
-public class RawStepAllowListTests
-{
- [Fact]
- public void AllowList_IsDisjointFromRegisteredPocoNames()
- {
- // Every name on the allow-list must NOT already be backed by a
- // typed POCO — POCOs get editability from being typed, the
- // allow-list is for catalog-path steps that we've verified
- // round-trip losslessly. Double-listing would make intent unclear.
- foreach (var pocoName in StepXmlFactory.RegisteredNames)
- {
- Assert.DoesNotContain(pocoName, RawStepAllowList.Names);
- }
- }
-
- [Fact]
- public void TypedPocoStep_IsFullyEditable_ReturnsTrue()
- {
- var step = new IfStep(enabled: true, condition: new Calculation("$x > 0"));
- Assert.True(step.IsFullyEditable);
- }
-
- [Fact]
- public void TypedPocoStep_SetField_IsFullyEditable_ReturnsTrue()
- {
- var step = new SetFieldStep(
- enabled: true,
- target: FieldRef.ForField("T", 1, "F"),
- expression: new Calculation("\"x\""));
- Assert.True(step.IsFullyEditable);
- }
-
- [Fact]
- public void RawStep_NotInAllowList_IsFullyEditable_ReturnsFalse()
- {
- // "Beep" is a catalog-known step with no typed POCO. Not in the
- // allow-list (which ships empty), so it must be sealed.
- var xml = XElement.Parse("");
- var step = ScriptStep.FromXml(xml);
-
- Assert.IsType(step);
- Assert.False(step.IsFullyEditable);
- }
-
- [Fact]
- public void RawStep_UnknownStepName_IsFullyEditable_ReturnsFalse()
- {
- // Completely unknown step — not in catalog, not typed, not allowed.
- var xml = XElement.Parse("");
- var step = ScriptStep.FromXml(xml);
-
- Assert.IsType(step);
- Assert.False(step.IsFullyEditable);
- }
-}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ReLoginStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ReLoginStepTests.cs
new file mode 100644
index 0000000..628b429
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ReLoginStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ReLoginStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ReLoginStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = ReLoginStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = ReLoginStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Re-Login", out var metadata));
+ Assert.Equal(138, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ReadFromDataFileStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ReadFromDataFileStepTests.cs
new file mode 100644
index 0000000..605e1b2
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ReadFromDataFileStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ReadFromDataFileStepTests
+{
+ private const string CanonicalXml = """
+ $variable
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ReadFromDataFileStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Read from Data File", out var metadata));
+ Assert.Equal(193, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/RecoverFileStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/RecoverFileStepTests.cs
new file mode 100644
index 0000000..50c9351
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/RecoverFileStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class RecoverFileStepTests
+{
+ private const string CanonicalXml = """$example""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = RecoverFileStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = RecoverFileStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = RecoverFileStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Recover File", out var metadata));
+ Assert.Equal(95, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/RefreshObjectStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/RefreshObjectStepTests.cs
new file mode 100644
index 0000000..c9f3adc
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/RefreshObjectStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class RefreshObjectStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = RefreshObjectStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = RefreshObjectStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = RefreshObjectStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Refresh Object", out var metadata));
+ Assert.Equal(167, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/RefreshPortalStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/RefreshPortalStepTests.cs
new file mode 100644
index 0000000..0daca2b
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/RefreshPortalStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class RefreshPortalStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = RefreshPortalStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = RefreshPortalStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = RefreshPortalStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Refresh Portal", out var metadata));
+ Assert.Equal(180, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/RefreshWindowStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/RefreshWindowStepTests.cs
new file mode 100644
index 0000000..aa80b7a
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/RefreshWindowStepTests.cs
@@ -0,0 +1,44 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class RefreshWindowStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = RefreshWindowStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = RefreshWindowStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+
+ // Extract the tokens inside [ ... ] and feed through FromDisplay.
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = RefreshWindowStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Refresh Window", out var metadata));
+ Assert.Equal(80, metadata!.Id);
+ Assert.Equal(2, metadata.Params.Count);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/RelookupFieldContentsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/RelookupFieldContentsStepTests.cs
new file mode 100644
index 0000000..4e0dc2b
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/RelookupFieldContentsStepTests.cs
@@ -0,0 +1,27 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class RelookupFieldContentsStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = RelookupFieldContentsStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Relookup Field Contents", out var metadata));
+ Assert.Equal(40, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/RenameFileStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/RenameFileStepTests.cs
new file mode 100644
index 0000000..fb477c1
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/RenameFileStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class RenameFileStepTests
+{
+ private const string CanonicalXml = """$example""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = RenameFileStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = RenameFileStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = RenameFileStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Rename File", out var metadata));
+ Assert.Equal(199, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ReplaceFieldContentsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ReplaceFieldContentsStepTests.cs
new file mode 100644
index 0000000..3d9f04d
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ReplaceFieldContentsStepTests.cs
@@ -0,0 +1,50 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ReplaceFieldContentsStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ // Source with Restore element (as from agentic-fm snippets) — our POCO drops it.
+ private const string WithRestoreXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ReplaceFieldContentsStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void RestoreElement_IsDropped()
+ {
+ var source = XElement.Parse(WithRestoreXml);
+ var step = ReplaceFieldContentsStep.Metadata.FromXml!(source);
+ var output = step.ToXml();
+ Assert.Null(output.Element("Restore"));
+ }
+
+ [Fact]
+ public void Display_EmitsDialogFieldAndCalculation()
+ {
+ var step = (ReplaceFieldContentsStep)ReplaceFieldContentsStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ // NoInteract state="True" in the canonical XML ⇒ dialog suppressed ⇒ "With dialog: Off".
+ Assert.Equal("Replace Field Contents [ With dialog: Off ; Customer::id (#3) ; \"value\" ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Replace Field Contents", out var metadata));
+ Assert.Equal(91, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ResetAccountPasswordStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ResetAccountPasswordStepTests.cs
new file mode 100644
index 0000000..d6bd5c9
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ResetAccountPasswordStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ResetAccountPasswordStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ResetAccountPasswordStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = ResetAccountPasswordStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = ResetAccountPasswordStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Reset Account Password", out var metadata));
+ Assert.Equal(136, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/RevertRecordRequestStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/RevertRecordRequestStepTests.cs
new file mode 100644
index 0000000..865233d
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/RevertRecordRequestStepTests.cs
@@ -0,0 +1,49 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class RevertRecordRequestStepTests
+{
+ private const string TrueStateXml = """""";
+ private const string FalseStateXml = """""";
+
+ [Fact]
+ public void RoundTrip_True_IsPreserved()
+ {
+ var source = XElement.Parse(TrueStateXml);
+ var step = RevertRecordRequestStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void RoundTrip_False_IsPreserved()
+ {
+ var source = XElement.Parse(FalseStateXml);
+ var step = RevertRecordRequestStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsExpectedFormat()
+ {
+ // Setting underlying prop=true renders as "Off"
+ // (invertedHr: XML True displays as Off).
+ var stepTrue = ((RevertRecordRequestStep)RevertRecordRequestStep.Metadata.FromXml!(XElement.Parse(TrueStateXml)));
+ Assert.Equal("Revert Record/Request [ With dialog: Off ]", stepTrue.ToDisplayLine());
+
+ var stepFalse = ((RevertRecordRequestStep)RevertRecordRequestStep.Metadata.FromXml!(XElement.Parse(FalseStateXml)));
+ Assert.Equal("Revert Record/Request [ With dialog: On ]", stepFalse.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Revert Record/Request", out var metadata));
+ Assert.Equal(51, metadata!.Id);
+ Assert.Single(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/RevertTransactionStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/RevertTransactionStepTests.cs
new file mode 100644
index 0000000..180b31f
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/RevertTransactionStepTests.cs
@@ -0,0 +1,27 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class RevertTransactionStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = RevertTransactionStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Revert Transaction", out var metadata));
+ Assert.Equal(207, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SaveACopyAsAddOnPackageStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SaveACopyAsAddOnPackageStepTests.cs
new file mode 100644
index 0000000..e176356
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SaveACopyAsAddOnPackageStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SaveACopyAsAddOnPackageStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SaveACopyAsAddOnPackageStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = SaveACopyAsAddOnPackageStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = SaveACopyAsAddOnPackageStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Save a Copy as Add-on Package", out var metadata));
+ Assert.Equal(96, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SaveACopyAsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SaveACopyAsStepTests.cs
new file mode 100644
index 0000000..27fc0d3
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SaveACopyAsStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SaveACopyAsStepTests
+{
+ private const string CanonicalXml = """$example""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SaveACopyAsStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = SaveACopyAsStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = SaveACopyAsStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Save a Copy as", out var metadata));
+ Assert.Equal(37, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SaveACopyAsXMLStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SaveACopyAsXMLStepTests.cs
new file mode 100644
index 0000000..cb12f48
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SaveACopyAsXMLStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SaveACopyAsXMLStepTests
+{
+ private const string CanonicalXml = """$example""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SaveACopyAsXMLStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = SaveACopyAsXMLStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = SaveACopyAsXMLStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Save a Copy as XML", out var metadata));
+ Assert.Equal(3, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SaveRecordsAsExcelStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SaveRecordsAsExcelStepTests.cs
new file mode 100644
index 0000000..f27a079
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SaveRecordsAsExcelStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SaveRecordsAsExcelStepTests
+{
+ private const string CanonicalXml = """
+ $path
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SaveRecordsAsExcelStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Save Records as Excel", out var metadata));
+ Assert.Equal(143, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SaveRecordsAsJsonlStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SaveRecordsAsJsonlStepTests.cs
new file mode 100644
index 0000000..cc33cf7
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SaveRecordsAsJsonlStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SaveRecordsAsJsonlStepTests
+{
+ private const string CanonicalXml = """
+ $path
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SaveRecordsAsJsonlStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Save Records as JSONL", out var metadata));
+ Assert.Equal(225, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SaveRecordsAsPdfStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SaveRecordsAsPdfStepTests.cs
new file mode 100644
index 0000000..c741012
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SaveRecordsAsPdfStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SaveRecordsAsPdfStepTests
+{
+ private const string CanonicalXml = """
+ $output
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SaveRecordsAsPdfStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Save Records as PDF", out var metadata));
+ Assert.Equal(144, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SaveRecordsAsSnapshotLinkStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SaveRecordsAsSnapshotLinkStepTests.cs
new file mode 100644
index 0000000..3e8c27f
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SaveRecordsAsSnapshotLinkStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SaveRecordsAsSnapshotLinkStepTests
+{
+ private const string CanonicalXml = """$example""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SaveRecordsAsSnapshotLinkStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = SaveRecordsAsSnapshotLinkStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = SaveRecordsAsSnapshotLinkStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Save Records as Snapshot Link", out var metadata));
+ Assert.Equal(152, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ScrollWindowStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ScrollWindowStepTests.cs
new file mode 100644
index 0000000..4648b56
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ScrollWindowStepTests.cs
@@ -0,0 +1,43 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ScrollWindowStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ScrollWindowStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsHrMappedValue()
+ {
+ var step = ScrollWindowStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal("Scroll Window [ Direction: Home ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void FromDisplay_ParsesHrValueBack()
+ {
+ var step = ScrollWindowStep.Metadata.FromDisplay!(true, new[] { "Direction: Home" });
+ Assert.True(XNode.DeepEquals(XElement.Parse(CanonicalXml), step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Scroll Window", out var metadata));
+ Assert.Equal(81, metadata!.Id);
+ Assert.Single(metadata.Params);
+ Assert.Equal("enum", metadata.Params[0].Type);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SelectAllStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SelectAllStepTests.cs
new file mode 100644
index 0000000..3bb0699
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SelectAllStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for SelectAllStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class SelectAllStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SelectAllStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new SelectAllStep();
+ Assert.Equal("Select All", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = SelectAllStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Select All", out var metadata));
+ Assert.Equal(50, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SelectDictionariesStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SelectDictionariesStepTests.cs
new file mode 100644
index 0000000..7893027
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SelectDictionariesStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for SelectDictionariesStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class SelectDictionariesStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SelectDictionariesStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new SelectDictionariesStep();
+ Assert.Equal("Select Dictionaries", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = SelectDictionariesStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Select Dictionaries", out var metadata));
+ Assert.Equal(108, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SelectWindowStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SelectWindowStepTests.cs
new file mode 100644
index 0000000..59b3b0e
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SelectWindowStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SelectWindowStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SelectWindowStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = SelectWindowStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = SelectWindowStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Select Window", out var metadata));
+ Assert.Equal(123, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SendDDEExecuteStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SendDDEExecuteStepTests.cs
new file mode 100644
index 0000000..15b104e
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SendDDEExecuteStepTests.cs
@@ -0,0 +1,43 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SendDDEExecuteStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SendDDEExecuteStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsHrMappedValue()
+ {
+ var step = SendDDEExecuteStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal("Send DDE Execute [ File ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void FromDisplay_ParsesHrValueBack()
+ {
+ var step = SendDDEExecuteStep.Metadata.FromDisplay!(true, new[] { "File" });
+ Assert.True(XNode.DeepEquals(XElement.Parse(CanonicalXml), step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Send DDE Execute", out var metadata));
+ Assert.Equal(64, metadata!.Id);
+ Assert.Single(metadata.Params);
+ Assert.Equal("enum", metadata.Params[0].Type);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SendEventStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SendEventStepTests.cs
new file mode 100644
index 0000000..138818b
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SendEventStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SendEventStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SendEventStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Send Event", out var metadata));
+ Assert.Equal(57, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SendMailStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SendMailStepTests.cs
new file mode 100644
index 0000000..d7312fb
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SendMailStepTests.cs
@@ -0,0 +1,46 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SendMailStepTests
+{
+ private const string CanonicalXml = """
+ $attachmentFilePathList
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SendMailStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void HotFields_ReadThroughBag()
+ {
+ var step = (SendMailStep)SendMailStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal("$to", step.To!.Text);
+ Assert.Equal("$subject", step.Subject!.Text);
+ Assert.Equal("$message", step.Message!.Text);
+ }
+
+ [Fact]
+ public void RoundTrip_PreservesUseFoundSetAttribute()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SendMailStep.Metadata.FromXml!(source);
+ var output = step.ToXml();
+ Assert.Equal("False", output.Element("To")!.Attribute("UseFoundSet")!.Value);
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Send Mail", out var metadata));
+ Assert.Equal(63, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetAICallLoggingStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetAICallLoggingStepTests.cs
new file mode 100644
index 0000000..824854b
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SetAICallLoggingStepTests.cs
@@ -0,0 +1,57 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SetAICallLoggingStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ private const string OffXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SetAICallLoggingStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void FlagElements_PresenceMeansOn()
+ {
+ var step = (SetAICallLoggingStep)SetAICallLoggingStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.True(step.Logging);
+ Assert.True(step.Verbose);
+ Assert.True(step.TruncateMessages);
+ }
+
+ [Fact]
+ public void FlagElements_AbsenceMeansOff()
+ {
+ var step = (SetAICallLoggingStep)SetAICallLoggingStep.Metadata.FromXml!(XElement.Parse(OffXml));
+ Assert.False(step.Logging);
+ Assert.False(step.Verbose);
+ Assert.False(step.TruncateMessages);
+ }
+
+ [Fact]
+ public void Display_EmitsAllFlags()
+ {
+ var step = (SetAICallLoggingStep)SetAICallLoggingStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal("Set AI Call Logging [ On ; Filename: \"ai_log.txt\" ; Verbose: On ; Truncate Messages: On ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Set AI Call Logging", out var metadata));
+ Assert.Equal(217, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetDataFilePositionStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetDataFilePositionStepTests.cs
new file mode 100644
index 0000000..d611d1c
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SetDataFilePositionStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SetDataFilePositionStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SetDataFilePositionStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = SetDataFilePositionStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = SetDataFilePositionStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Set Data File Position", out var metadata));
+ Assert.Equal(195, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetDictionaryStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetDictionaryStepTests.cs
new file mode 100644
index 0000000..66adbca
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SetDictionaryStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SetDictionaryStepTests
+{
+ private const string CanonicalXml = """$example""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SetDictionaryStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = SetDictionaryStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = SetDictionaryStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Set Dictionary", out var metadata));
+ Assert.Equal(209, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetErrorCaptureStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetErrorCaptureStepTests.cs
new file mode 100644
index 0000000..de72e87
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SetErrorCaptureStepTests.cs
@@ -0,0 +1,106 @@
+using System.Linq;
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Single-boolean POCO canonical pattern. The underlying XML carries a
+/// <Set state="True|False"/> child; display text uses
+/// "On"/"Off" matching FileMaker Pro's wording.
+///
+public class SetErrorCaptureStepTests
+{
+ // Canonical shape from
+ // agent/snippet_examples/steps/control/Set Error Capture.xml
+ private const string StateTrueSnippet = """
+
+
+
+
+
+
+ """;
+
+ private const string StateFalseSnippet = """
+
+
+
+
+
+
+ """;
+
+ private static XElement StepFrom(string snippet) =>
+ XDocument.Parse(snippet).Root!.Element("Step")!;
+
+ [Fact]
+ public void RoundTrip_StateTrue()
+ {
+ var source = StepFrom(StateTrueSnippet);
+ var step = (SetErrorCaptureStep)SetErrorCaptureStep.Metadata.FromXml!(source);
+
+ Assert.True(step.CaptureErrors);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void RoundTrip_StateFalse()
+ {
+ var source = StepFrom(StateFalseSnippet);
+ var step = (SetErrorCaptureStep)SetErrorCaptureStep.Metadata.FromXml!(source);
+
+ Assert.False(step.CaptureErrors);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsOnOff()
+ {
+ Assert.Equal("Set Error Capture [ On ]",
+ new SetErrorCaptureStep(captureErrors: true).ToDisplayLine());
+ Assert.Equal("Set Error Capture [ Off ]",
+ new SetErrorCaptureStep(captureErrors: false).ToDisplayLine());
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step = (SetErrorCaptureStep)SetErrorCaptureStep.Metadata.FromDisplay!(true, new[] { "On" });
+ Assert.True(step.CaptureErrors);
+
+ var off = (SetErrorCaptureStep)SetErrorCaptureStep.Metadata.FromDisplay!(true, new[] { "Off" });
+ Assert.False(off.CaptureErrors);
+ }
+
+ [Fact]
+ public void Registry_HasSetErrorCaptureWithParam()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Set Error Capture", out var metadata));
+ Assert.Equal(86, metadata!.Id);
+ Assert.Equal("control", metadata.Category);
+ Assert.Null(metadata.BlockPair);
+
+ var param = Assert.Single(metadata.Params);
+ Assert.Equal("Set", param.XmlElement);
+ Assert.Equal("boolean", param.Type);
+ Assert.Equal("state", param.XmlAttr);
+
+ // Description is sourced from agentic-fm's snippet comment
+ // and powers tooltip / hover UIs as they're wired up later.
+ Assert.NotNull(param.Description);
+ Assert.Contains("suppresses", param.Description, System.StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void GetValidValues_ReturnsOnAndOff()
+ {
+ var metadata = StepRegistry.ByName["Set Error Capture"];
+ var values = StepRegistry.GetValidValues(metadata.Params[0]);
+ Assert.Contains("On", values);
+ Assert.Contains("Off", values);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetErrorLoggingStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetErrorLoggingStepTests.cs
new file mode 100644
index 0000000..8318f60
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SetErrorLoggingStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SetErrorLoggingStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SetErrorLoggingStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = SetErrorLoggingStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = SetErrorLoggingStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Set Error Logging", out var metadata));
+ Assert.Equal(200, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetFieldByNameStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetFieldByNameStepTests.cs
new file mode 100644
index 0000000..1486fab
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SetFieldByNameStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SetFieldByNameStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SetFieldByNameStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = SetFieldByNameStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = SetFieldByNameStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Set Field By Name", out var metadata));
+ Assert.Equal(147, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetLayoutObjectAnimationStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetLayoutObjectAnimationStepTests.cs
new file mode 100644
index 0000000..7275873
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SetLayoutObjectAnimationStepTests.cs
@@ -0,0 +1,49 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SetLayoutObjectAnimationStepTests
+{
+ private const string TrueStateXml = """""";
+ private const string FalseStateXml = """""";
+
+ [Fact]
+ public void RoundTrip_True_IsPreserved()
+ {
+ var source = XElement.Parse(TrueStateXml);
+ var step = SetLayoutObjectAnimationStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void RoundTrip_False_IsPreserved()
+ {
+ var source = XElement.Parse(FalseStateXml);
+ var step = SetLayoutObjectAnimationStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsExpectedFormat()
+ {
+ // Setting underlying prop=true renders as "On"
+ // (boolean: XML True displays as On).
+ var stepTrue = ((SetLayoutObjectAnimationStep)SetLayoutObjectAnimationStep.Metadata.FromXml!(XElement.Parse(TrueStateXml)));
+ Assert.Equal("Set Layout Object Animation [ Animation: On ]", stepTrue.ToDisplayLine());
+
+ var stepFalse = ((SetLayoutObjectAnimationStep)SetLayoutObjectAnimationStep.Metadata.FromXml!(XElement.Parse(FalseStateXml)));
+ Assert.Equal("Set Layout Object Animation [ Animation: Off ]", stepFalse.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Set Layout Object Animation", out var metadata));
+ Assert.Equal(168, metadata!.Id);
+ Assert.Single(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetMultiUserStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetMultiUserStepTests.cs
new file mode 100644
index 0000000..b475687
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SetMultiUserStepTests.cs
@@ -0,0 +1,43 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SetMultiUserStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SetMultiUserStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsHrMappedValue()
+ {
+ var step = SetMultiUserStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal("Set Multi-User [ Network access: On ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void FromDisplay_ParsesHrValueBack()
+ {
+ var step = SetMultiUserStep.Metadata.FromDisplay!(true, new[] { "Network access: On" });
+ Assert.True(XNode.DeepEquals(XElement.Parse(CanonicalXml), step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Set Multi-User", out var metadata));
+ Assert.Equal(84, metadata!.Id);
+ Assert.Single(metadata.Params);
+ Assert.Equal("enum", metadata.Params[0].Type);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetNextSerialValueStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetNextSerialValueStepTests.cs
new file mode 100644
index 0000000..8c7e185
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SetNextSerialValueStepTests.cs
@@ -0,0 +1,35 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SetNextSerialValueStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SetNextSerialValueStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsFieldAndNextValue()
+ {
+ var step = (SetNextSerialValueStep)SetNextSerialValueStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal("Set Next Serial Value [ Customer::id (#3) ; Max ( id ) + 1 ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Set Next Serial Value", out var metadata));
+ Assert.Equal(116, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetRevertTransactionOnErrorStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetRevertTransactionOnErrorStepTests.cs
new file mode 100644
index 0000000..598456e
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SetRevertTransactionOnErrorStepTests.cs
@@ -0,0 +1,49 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SetRevertTransactionOnErrorStepTests
+{
+ private const string TrueStateXml = """""";
+ private const string FalseStateXml = """""";
+
+ [Fact]
+ public void RoundTrip_True_IsPreserved()
+ {
+ var source = XElement.Parse(TrueStateXml);
+ var step = SetRevertTransactionOnErrorStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void RoundTrip_False_IsPreserved()
+ {
+ var source = XElement.Parse(FalseStateXml);
+ var step = SetRevertTransactionOnErrorStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsExpectedFormat()
+ {
+ // Setting underlying prop=true renders as "On"
+ // (boolean: XML True displays as On).
+ var stepTrue = ((SetRevertTransactionOnErrorStep)SetRevertTransactionOnErrorStep.Metadata.FromXml!(XElement.Parse(TrueStateXml)));
+ Assert.Equal("Set Revert Transaction on Error [ Revert on error: On ]", stepTrue.ToDisplayLine());
+
+ var stepFalse = ((SetRevertTransactionOnErrorStep)SetRevertTransactionOnErrorStep.Metadata.FromXml!(XElement.Parse(FalseStateXml)));
+ Assert.Equal("Set Revert Transaction on Error [ Revert on error: Off ]", stepFalse.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Set Revert Transaction on Error", out var metadata));
+ Assert.Equal(223, metadata!.Id);
+ Assert.Single(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetSelectionStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetSelectionStepTests.cs
new file mode 100644
index 0000000..99f263d
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SetSelectionStepTests.cs
@@ -0,0 +1,35 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SetSelectionStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SetSelectionStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsFieldAndPositions()
+ {
+ var step = (SetSelectionStep)SetSelectionStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal("Set Selection [ Notes::body (#1) ; Start Position: 1 ; End Position: 10 ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Set Selection", out var metadata));
+ Assert.Equal(130, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetSessionIdentifierStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetSessionIdentifierStepTests.cs
new file mode 100644
index 0000000..4660574
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SetSessionIdentifierStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SetSessionIdentifierStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SetSessionIdentifierStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = SetSessionIdentifierStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = SetSessionIdentifierStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Set Session Identifier", out var metadata));
+ Assert.Equal(208, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetUseSystemFormatsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetUseSystemFormatsStepTests.cs
new file mode 100644
index 0000000..c5fa0ea
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SetUseSystemFormatsStepTests.cs
@@ -0,0 +1,49 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SetUseSystemFormatsStepTests
+{
+ private const string TrueStateXml = """""";
+ private const string FalseStateXml = """""";
+
+ [Fact]
+ public void RoundTrip_True_IsPreserved()
+ {
+ var source = XElement.Parse(TrueStateXml);
+ var step = SetUseSystemFormatsStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void RoundTrip_False_IsPreserved()
+ {
+ var source = XElement.Parse(FalseStateXml);
+ var step = SetUseSystemFormatsStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsExpectedFormat()
+ {
+ // Setting underlying prop=true renders as "On"
+ // (boolean: XML True displays as On).
+ var stepTrue = ((SetUseSystemFormatsStep)SetUseSystemFormatsStep.Metadata.FromXml!(XElement.Parse(TrueStateXml)));
+ Assert.Equal("Set Use System Formats [ Use system formats: On ]", stepTrue.ToDisplayLine());
+
+ var stepFalse = ((SetUseSystemFormatsStep)SetUseSystemFormatsStep.Metadata.FromXml!(XElement.Parse(FalseStateXml)));
+ Assert.Equal("Set Use System Formats [ Use system formats: Off ]", stepFalse.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Set Use System Formats", out var metadata));
+ Assert.Equal(94, metadata!.Id);
+ Assert.Single(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetWebViewerStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetWebViewerStepTests.cs
new file mode 100644
index 0000000..4a13272
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SetWebViewerStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SetWebViewerStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SetWebViewerStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = SetWebViewerStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = SetWebViewerStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Set Web Viewer", out var metadata));
+ Assert.Equal(146, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetWindowTitleStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetWindowTitleStepTests.cs
new file mode 100644
index 0000000..81c5ebd
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SetWindowTitleStepTests.cs
@@ -0,0 +1,41 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SetWindowTitleStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SetWindowTitleStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = SetWindowTitleStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = SetWindowTitleStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Set Window Title", out var metadata));
+ Assert.Equal(124, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetZoomLevelStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetZoomLevelStepTests.cs
new file mode 100644
index 0000000..7725b3a
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SetZoomLevelStepTests.cs
@@ -0,0 +1,44 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SetZoomLevelStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SetZoomLevelStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = SetZoomLevelStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+
+ // Extract the tokens inside [ ... ] and feed through FromDisplay.
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = SetZoomLevelStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Set Zoom Level", out var metadata));
+ Assert.Equal(97, metadata!.Id);
+ Assert.Equal(2, metadata.Params.Count);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ShowAllRecordsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ShowAllRecordsStepTests.cs
new file mode 100644
index 0000000..548487a
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ShowAllRecordsStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for ShowAllRecordsStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class ShowAllRecordsStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ShowAllRecordsStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new ShowAllRecordsStep();
+ Assert.Equal("Show All Records", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = ShowAllRecordsStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Show All Records", out var metadata));
+ Assert.Equal(23, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ShowHideMenubarStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ShowHideMenubarStepTests.cs
new file mode 100644
index 0000000..ba31e28
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ShowHideMenubarStepTests.cs
@@ -0,0 +1,44 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ShowHideMenubarStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ShowHideMenubarStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = ShowHideMenubarStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+
+ // Extract the tokens inside [ ... ] and feed through FromDisplay.
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = ShowHideMenubarStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Show/Hide Menubar", out var metadata));
+ Assert.Equal(166, metadata!.Id);
+ Assert.Equal(2, metadata.Params.Count);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ShowHideTextRulerStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ShowHideTextRulerStepTests.cs
new file mode 100644
index 0000000..c5694b7
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ShowHideTextRulerStepTests.cs
@@ -0,0 +1,43 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ShowHideTextRulerStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ShowHideTextRulerStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsHrMappedValue()
+ {
+ var step = ShowHideTextRulerStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal("Show/Hide Text Ruler [ Action: Show ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void FromDisplay_ParsesHrValueBack()
+ {
+ var step = ShowHideTextRulerStep.Metadata.FromDisplay!(true, new[] { "Action: Show" });
+ Assert.True(XNode.DeepEquals(XElement.Parse(CanonicalXml), step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Show/Hide Text Ruler", out var metadata));
+ Assert.Equal(92, metadata!.Id);
+ Assert.Single(metadata.Params);
+ Assert.Equal("enum", metadata.Params[0].Type);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ShowHideToolbarsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ShowHideToolbarsStepTests.cs
new file mode 100644
index 0000000..881d90d
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ShowHideToolbarsStepTests.cs
@@ -0,0 +1,44 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ShowHideToolbarsStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ShowHideToolbarsStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_RoundTripsThroughFromDisplayParams()
+ {
+ var step1 = ShowHideToolbarsStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ var display = step1.ToDisplayLine();
+
+ // Extract the tokens inside [ ... ] and feed through FromDisplay.
+ var open = display.IndexOf('[');
+ var close = display.LastIndexOf(']');
+ var inner = display.Substring(open + 1, close - open - 1).Trim();
+ var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries);
+
+ var step2 = ShowHideToolbarsStep.Metadata.FromDisplay!(true, tokens);
+ Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Show/Hide Toolbars", out var metadata));
+ Assert.Equal(29, metadata!.Id);
+ Assert.Equal(3, metadata.Params.Count);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ShowOmittedOnlyStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ShowOmittedOnlyStepTests.cs
new file mode 100644
index 0000000..2940ec1
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ShowOmittedOnlyStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for ShowOmittedOnlyStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class ShowOmittedOnlyStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ShowOmittedOnlyStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new ShowOmittedOnlyStep();
+ Assert.Equal("Show Omitted Only", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = ShowOmittedOnlyStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Show Omitted Only", out var metadata));
+ Assert.Equal(27, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SortRecordsByFieldStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SortRecordsByFieldStepTests.cs
new file mode 100644
index 0000000..427cc04
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SortRecordsByFieldStepTests.cs
@@ -0,0 +1,35 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SortRecordsByFieldStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SortRecordsByFieldStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsOrderAndField()
+ {
+ var step = (SortRecordsByFieldStep)SortRecordsByFieldStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal("Sort Records by Field [ Descending ; Customer::name (#2) ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Sort Records by Field", out var metadata));
+ Assert.Equal(154, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SortRecordsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SortRecordsStepTests.cs
new file mode 100644
index 0000000..ff2bc06
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SortRecordsStepTests.cs
@@ -0,0 +1,38 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SortRecordsStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SortRecordsStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void SortList_IsTyped()
+ {
+ var step = (SortRecordsStep)SortRecordsStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.NotNull(step.Sort);
+ Assert.Single(step.Sort!.Fields);
+ Assert.Equal("Ascending", step.Sort.Fields[0].Type);
+ Assert.Equal("name", step.Sort.Fields[0].PrimaryField.Name);
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Sort Records", out var metadata));
+ Assert.Equal(39, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SpeakStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SpeakStepTests.cs
new file mode 100644
index 0000000..8eb4274
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SpeakStepTests.cs
@@ -0,0 +1,47 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class SpeakStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ private const string WithoutOptionsXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SpeakStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void RoundTrip_WithoutOptions_IsPreserved()
+ {
+ var source = XElement.Parse(WithoutOptionsXml);
+ var step = SpeakStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsTextOnly()
+ {
+ var step = (SpeakStep)SpeakStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal("Speak [ \"text_to_speak\" ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Speak", out var metadata));
+ Assert.Equal(66, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/SpellingOptionsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SpellingOptionsStepTests.cs
new file mode 100644
index 0000000..79d2dc1
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/SpellingOptionsStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for SpellingOptionsStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class SpellingOptionsStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = SpellingOptionsStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new SpellingOptionsStep();
+ Assert.Equal("Spelling Options", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = SpellingOptionsStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Spelling Options", out var metadata));
+ Assert.Equal(107, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/TriggerClarisConnectFlowStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/TriggerClarisConnectFlowStepTests.cs
new file mode 100644
index 0000000..6786290
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/TriggerClarisConnectFlowStepTests.cs
@@ -0,0 +1,29 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class TriggerClarisConnectFlowStepTests
+{
+ // The catalog records this step with id: null (unconfirmed). We default
+ // to 0 but preserve whatever id appears in the source XML.
+ private const string CanonicalXml = """
+ https://flow.example.com$response
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = TriggerClarisConnectFlowStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Trigger Claris Connect Flow", out _));
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/TruncateTableStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/TruncateTableStepTests.cs
new file mode 100644
index 0000000..28e74af
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/TruncateTableStepTests.cs
@@ -0,0 +1,36 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class TruncateTableStepTests
+{
+ private const string CanonicalXml = """
+
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = TruncateTableStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsDialogAndTable()
+ {
+ var step = (TruncateTableStep)TruncateTableStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ // NoInteract state="True" in the canonical XML ⇒ dialog suppressed ⇒ "With dialog: Off".
+ Assert.Equal("Truncate Table [ With dialog: Off ; Table: Clients ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Truncate Table", out var metadata));
+ Assert.Equal(182, metadata!.Id);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/UndoRedoStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/UndoRedoStepTests.cs
new file mode 100644
index 0000000..2aea994
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/UndoRedoStepTests.cs
@@ -0,0 +1,43 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class UndoRedoStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = UndoRedoStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsHrMappedValue()
+ {
+ var step = UndoRedoStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal("Undo/Redo [ Action: Undo ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void FromDisplay_ParsesHrValueBack()
+ {
+ var step = UndoRedoStep.Metadata.FromDisplay!(true, new[] { "Action: Undo" });
+ Assert.True(XNode.DeepEquals(XElement.Parse(CanonicalXml), step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Undo/Redo", out var metadata));
+ Assert.Equal(45, metadata!.Id);
+ Assert.Single(metadata.Params);
+ Assert.Equal("enum", metadata.Params[0].Type);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/UnsortRecordsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/UnsortRecordsStepTests.cs
new file mode 100644
index 0000000..442b7cd
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/UnsortRecordsStepTests.cs
@@ -0,0 +1,51 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+///
+/// Zero-param POCO tests for UnsortRecordsStep. Fixture is inline per the
+/// pilot pattern; no FixtureLoader, no file I/O.
+///
+public class UnsortRecordsStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = UnsortRecordsStep.Metadata.FromXml!(source);
+
+ Assert.IsType(step);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsBareName()
+ {
+ var step = new UnsortRecordsStep();
+ Assert.Equal("Unsort Records", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void Disabled_RoundTrips()
+ {
+ var source = XElement.Parse("""""");
+ var step = UnsortRecordsStep.Metadata.FromXml!(source);
+
+ Assert.False(step.Enabled);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Unsort Records", out var metadata));
+ Assert.Equal(21, metadata!.Id);
+ Assert.Empty(metadata.Params);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/ViewAsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ViewAsStepTests.cs
new file mode 100644
index 0000000..50ea730
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/ViewAsStepTests.cs
@@ -0,0 +1,43 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class ViewAsStepTests
+{
+ private const string CanonicalXml = """""";
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = ViewAsStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Display_EmitsHrMappedValue()
+ {
+ var step = ViewAsStep.Metadata.FromXml!(XElement.Parse(CanonicalXml));
+ Assert.Equal("View As [ View: Cycle ]", step.ToDisplayLine());
+ }
+
+ [Fact]
+ public void FromDisplay_ParsesHrValueBack()
+ {
+ var step = ViewAsStep.Metadata.FromDisplay!(true, new[] { "View: Cycle" });
+ Assert.True(XNode.DeepEquals(XElement.Parse(CanonicalXml), step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("View As", out var metadata));
+ Assert.Equal(30, metadata!.Id);
+ Assert.Single(metadata.Params);
+ Assert.Equal("enum", metadata.Params[0].Type);
+ }
+}
diff --git a/tests/SharpFM.Tests/Scripting/Steps/WriteToDataFileStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/WriteToDataFileStepTests.cs
new file mode 100644
index 0000000..6c0a6df
--- /dev/null
+++ b/tests/SharpFM.Tests/Scripting/Steps/WriteToDataFileStepTests.cs
@@ -0,0 +1,28 @@
+using System.Xml.Linq;
+using SharpFM.Model.Scripting.Registry;
+using SharpFM.Model.Scripting.Steps;
+using Xunit;
+
+namespace SharpFM.Tests.Scripting.Steps;
+
+public class WriteToDataFileStepTests
+{
+ private const string CanonicalXml = """
+ $variable
+ """;
+
+ [Fact]
+ public void RoundTrip_CanonicalXml_IsPreserved()
+ {
+ var source = XElement.Parse(CanonicalXml);
+ var step = WriteToDataFileStep.Metadata.FromXml!(source);
+ Assert.True(XNode.DeepEquals(source, step.ToXml()));
+ }
+
+ [Fact]
+ public void Registry_HasStep()
+ {
+ Assert.True(StepRegistry.ByName.TryGetValue("Write to Data File", out var metadata));
+ Assert.Equal(192, metadata!.Id);
+ }
+}