Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/architecture/clean-feature-separation.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ This document answers the architecture questions raised in [issue #33](https://g

## Findings

> **Update 2026-06-12:** extraction is well underway. Request execution (HTTP/GraphQL/WebSocket/SSE), response projection, response actions, the demo-server lifecycle, options persistence (auto-save/save/export/import via `ApplicationOptionsWorkflow`), the collections workflows (management incl. import/delete via `CollectionsManagementCoordinator`, inherited-headers autosave via `CollectionInheritedHeadersWorkflow`, filter/sort/group via `CollectionFilterWorkflow`), and the scheduled-jobs lifecycle (add/remove/load incl. auto-start via `ScheduledJobsWorkflow`) now live in feature-scoped Workflow/Coordinator classes; layout is partially extracted. `MainWindowViewModel` still owns the per-option UI projections, named-layout management, request tabs, and history. Current status and the ordered forward plan live in [`mainwindowviewmodel-split-plan.md`](mainwindowviewmodel-split-plan.md). The findings below describe the original state and are kept for context.
> **Update 2026-06-12:** extraction is well underway. Request execution (HTTP/GraphQL/WebSocket/SSE), response projection, response actions, the demo-server lifecycle, options persistence (auto-save/save/export/import via `ApplicationOptionsWorkflow`), the collections workflows (management incl. import/delete via `CollectionsManagementCoordinator`, inherited-headers autosave via `CollectionInheritedHeadersWorkflow`, filter/sort/group via `CollectionFilterWorkflow`), the scheduled-jobs lifecycle (add/remove/load incl. auto-start via `ScheduledJobsWorkflow`), the request-tab list and history load/filter (`RequestTabsWorkflow`, `RequestHistoryWorkflow`), and the named-layout collection (`SavedLayoutNames`, save-as-new/save-to-existing/restore-default/remove via `LayoutWorkflow`) now live in feature-scoped Workflow/Coordinator classes; layout is partially extracted. `MainWindowViewModel` still owns the per-option UI projections, the dock-tree/window-geometry layout pipeline, active-tab/response-state projection, and history-entry-to-editor application. Current status and the ordered forward plan live in [`mainwindowviewmodel-split-plan.md`](mainwindowviewmodel-split-plan.md). The findings below describe the original state and are kept for context.

- **Monolithic main view model**: `MainWindowViewModel` (~2,500 lines at the time of writing) owns request editing, response rendering, history, collections, environments, options, scheduling, layout persistence, and logging. All UI actions pass through this single type, so responsibilities are tightly coupled.
- **Child view models are thin proxies**: dockable VMs such as `RequestViewModel`, `ResponseViewModel`, `LeftPanelViewModel`, and `OptionsViewModel` simply forward to `MainWindowViewModel`. Reusing them elsewhere still drags the entire main VM with it.
Expand Down Expand Up @@ -93,7 +93,8 @@ Features/
UnhandledExceptionCollector, UnhandledExceptionEntry
Environments/ — EnvironmentsViewModel, EnvironmentVariableViewModel, EnvironmentsView
GraphQl/ — GraphQlViewModel, GraphQlRequestWorkflow, ManualGraphQlRequestCoordinator
HttpRequest/ — RequestEditorViewModel, RequestViewModel, RequestTabViewModel, ResponseViewModel,
History/ — RequestHistoryWorkflow
HttpRequest/ — RequestEditorViewModel, RequestViewModel, RequestTabViewModel, RequestTabsWorkflow, ResponseViewModel,
HttpRequestWorkflow, ManualHttpRequestCoordinator, HttpResponseProjectionWorkflow,
ResponseActionsViewModel, IResponseActionsContext,
RequestHeaderViewModel, RequestQueryParameterViewModel,
Expand Down
10 changes: 5 additions & 5 deletions docs/architecture/mainwindowviewmodel-split-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Context

`MainWindowViewModel` contains multiple feature areas and coordination responsibilities in one class (about 4,000 lines when this plan was written; about 3,090 lines as of 2026-06-12 after the scheduled-jobs slice). This slows down feature work, increases regression risk, and makes focused testing harder.
`MainWindowViewModel` contains multiple feature areas and coordination responsibilities in one class (about 4,000 lines when this plan was written; about 2,985 lines as of 2026-06-12 after the named-layouts slice). This slows down feature work, increases regression risk, and makes focused testing harder.

This document is a persisted implementation plan for splitting feature logic from `MainWindowViewModel` while keeping behavior stable.

Expand Down Expand Up @@ -111,10 +111,10 @@ Recommended extraction order (lowest coupling first):
| Collections persistence | `CollectionsWorkflow`, `CollectionsManagementCoordinator` | ✅ Done — import/delete moved into the coordinator (2026-06-12) |
| Collections filter/sort/group | `CollectionFilterWorkflow` | ✅ Done (2026-06-12) |
| Collections inherited-headers auto-save | `CollectionInheritedHeadersWorkflow` | ✅ Done (2026-06-12) — debounce, suppression scopes, pending/flush, persistence; Main keeps dispatcher marshalling and the live-preview editor sync |
| Layout tree/restore | `LayoutWorkflow`, `LayoutTreeWorkflow` | 🔶 Partial — named layouts, geometry, and persistence still in Main |
| Layout tree/restore | `LayoutWorkflow`, `LayoutTreeWorkflow` | 🔶 Partial (2026-06-12) — `LayoutWorkflow` now owns the named-layout collection (`SavedLayoutNames`, the saved-layout dictionary, and the `Layout {N}` name counter) plus save-as-new/save-to-existing/restore-default/remove orchestration and `BuildNamedLayouts()` for persistence; window geometry capture, dock-tree capture/apply (`CaptureLayoutSnapshot`/`ApplyLayoutSnapshot`/`ReapplyStartupLayout`), and `PersistCurrentLayout`/`CloseFloatingWindows` stay in Main by design (they read/write `Layout`/`IRootDock` and Avalonia window state directly) |
| Options persistence | `ApplicationOptionsWorkflow`, `ApplicationOptionsSnapshot`, `OptionsPersistenceOutcome` | ✅ Done — auto-save debounce, build/validate, save/export/import; the per-option `Apply*` UI projections stay in Main by design (they mutate Avalonia/app state) |
| Scheduled jobs add/remove | `ScheduledJobsWorkflow` | ✅ Done (2026-06-12) — add (interval clamped to `MinIntervalSeconds`), remove (stop + delete + dispose), load with auto-start-on-launch, and job VM disposal; Main keeps the commands and `LeftPanelTab` switch |
| Request tabs + history | — | ❌ Not started |
| Request tabs + history | `RequestTabsWorkflow`, `RequestHistoryWorkflow` | 🔶 Partial (2026-06-12) — tab add/close (`RequestTabsWorkflow`, `Features/HttpRequest/`) and history load/filter + editor projection (`RequestHistoryWorkflow`, `Features/History/`) extracted; `ApplyActiveRequestTab`, `SaveResponseStateForTab`/`RestoreResponseStateForTab`, and `_lastAppliedRequestTab` stay in Main by design (they read/write Main's `[Reactive]` response-state properties directly, matching the "Apply* UI projections stay in Main" precedent from the options/collections slices) |

<a id="remaining-slices--ordered-plan-2026-06-11"></a>
#### Remaining slices — ordered plan (2026-06-11)
Expand All @@ -124,8 +124,8 @@ Execute one slice per PR, in this order (lowest coupling first), following the W
1. ~~**Options workflow**~~ ✅ Done — `ApplicationOptionsWorkflow` (`Features/Options/`) owns the debounced auto-save pipeline (injectable `IScheduler`, `TestScheduler`-tested), nesting auto-save suppression scopes, snapshot-based `BuildOptions` validation, and the save/export/import persistence flows with `OptionsPersistenceOutcome` results. Main keeps the option `[Reactive]` properties, the per-option `Apply*` UI projections (theme/font/TLS mutate Avalonia and service state), and the file pickers. Tests: `ApplicationOptionsWorkflowTests` + `ApplicationOptionsWorkflowPersistenceTests`.
2. ~~**Collections UI workflows**~~ ✅ Done (2026-06-12) — `CollectionInheritedHeadersWorkflow` owns the inherited-headers debounced auto-save (injectable `IScheduler`, `TestScheduler`-tested), suppression scopes, pending/flush tracking, snapshot persistence, and the shared `BuildHeaders`/`MergeCollectionAndRequestHeaders`/`HeadersEqual` helpers; `CollectionFilterWorkflow` owns filter/sort/group with expansion-state preservation; import/delete moved into `CollectionsManagementCoordinator` (`ImportCollectionOutcome`/`DeleteCollectionOutcome`). Main keeps the `[Reactive]` form-visibility state (per the established convention), the dispatcher marshalling, the file picker, and the live-preview editor sync (`SyncActiveCollectionRequestInheritedHeaders` projects onto the request editor). DynamicData was considered and deliberately not adopted — the extracted pipeline stays behavior-identical; revisit if the history slice adopts it. Tests: `CollectionInheritedHeadersWorkflowTests`, `CollectionFilterWorkflowTests`, `CollectionsManagementCoordinatorTests`.
3. ~~**Scheduled jobs workflow**~~ ✅ Done (2026-06-12) — `ScheduledJobsWorkflow` (`Features/ScheduledJobs/`) owns the `Jobs` collection and its lifecycle: `AddJob` (interval clamped to the moved `MinIntervalSeconds` constant), `RemoveJobAsync` (stop + repository delete + dispose), `LoadJobsAsync` (replace + optional auto-start on launch), and disposal of job view models. Main keeps the `[ReactiveCommand]` entry points, the `ScheduledJobs` pass-through property for XAML, and the `LeftPanelTab` switch. Tests: `ScheduledJobsWorkflowTests` (uses a never-advanced `TestScheduler` on `ScheduledJobService` so started jobs never tick).
4. **Request tabs + history** *(next)* — extract tab lifecycle (`NewRequestTab`, `CloseRequestTab`, `ApplyActiveRequestTab`, `SaveResponseStateForTab`) and history load/filter (`LoadHistoryAsync`, `ApplyHistoryFilter`, `LoadHistoryRequest`) into feature-scoped classes.
5. **Layout management** — finish the layout slice: named layout save/restore/remove, window geometry, `PersistCurrentLayout`/`ApplyLayoutSnapshot`/`ReapplyStartupLayout` on top of the existing `LayoutWorkflow`/`LayoutTreeWorkflow`. Largest and riskiest; do it once the rest of Main is thin.
4. ~~**Request tabs + history**~~ 🔶 Partial (2026-06-12) — `RequestTabsWorkflow` (`Features/HttpRequest/`) owns the `Tabs` collection: `AddTab` (wrap an editor in a new tab) and `CloseTab` (remove + dispose, keeping at least one tab, returning the tab that should become active). `RequestHistoryWorkflow` (`Features/History/`) owns the `History` collection: `LoadAsync` (reload from the repository, ordered by `CreatedAtUtc` descending), `ApplyFilter` (name/URL/method search, preserving item identity for unchanged entries), and `BuildEditorProjection` (maps a `RequestHistoryEntry` to the editor fields `LoadHistoryRequest` applies). Main keeps the `RequestTabs`/`History` pass-through properties for XAML, the `[ReactiveCommand]` entry points, `ActiveRequestTab` and its `ApplyActiveRequestTab` projection (editor swap + `PrimaryActionLabel`/`RequestEditor` notifications), and `SaveResponseStateForTab`/`RestoreResponseStateForTab` (per-tab response-state snapshotting against Main's `[Reactive]` response properties) — these remain in Main per the "Apply* UI projections stay in Main" precedent. Tests: `RequestTabsWorkflowTests`, `RequestHistoryWorkflowTests`.
5. ~~**Layout management**~~ 🔶 Partial (2026-06-12) — `LayoutWorkflow` (`Features/Layout/`) now owns the named-layout collection: `SavedLayoutNames` (ordered alphabetically), the saved-layout dictionary, and the `Layout {N}` name counter; `LoadFromOptions` (replaces `ApplyLayoutOptions`'s dictionary/name bookkeeping), `TryGetLayout`, `SaveLayoutAsNew`, `SaveLayoutToExisting`, `RestoreDefaultLayout`, `RemoveLayout`, and `BuildNamedLayouts` (for persistence). Main keeps `SelectedLayoutName` (`[Reactive]`, bound to the layout combo box), the `[ReactiveCommand]` entry points, and the dock-tree/geometry pipeline (`CaptureLayoutSnapshot`/`ApplyLayoutSnapshot`/`ReapplyStartupLayout`/`PersistCurrentLayout`/`SetWindowGeometry`/`CloseFloatingWindows`) since these read/write `Layout` (`IRootDock`) and Avalonia window state directly — consistent with the "Apply* UI projections stay in Main" precedent. Tests: `LayoutWorkflowTests`.
6. **Phase 3 delegation cleanup** — remove `IResponseActionsContext` pass-throughs and other temporary delegation from Main once XAML binds to feature VMs directly.
7. **DockFactory feature registrations** — only after the slices above: replace the `MainWindowViewModel` constructor dependency in `DockFactory` with per-feature dockable registrations (step 2 in `clean-feature-separation.md`). Deliberately deferred so registrations can point at clean feature VMs.

Expand Down
17 changes: 17 additions & 0 deletions docs/coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ As of the latest build (2026-04-24, after adding Kestrel integration tests for W
- **Arbor.HttpClient.Desktop:** 65.6% line coverage (3334/5081 lines), 51.8% branch coverage (822/1587 branches)
- **Arbor.HttpClient.Testing:** ~13% line coverage (test infrastructure — indirect coverage is acceptable)

### New code introduced in the named-layouts workflow slice (2026-06-12)

> Measured with `dotnet test src/Arbor.HttpClient.Desktop.E2E.Tests/... -- --filter-class "...LayoutWorkflowTests" --coverage --coverage-output-format cobertura` (Microsoft.Testing.Extensions.CodeCoverage).

| Class | Line coverage | Notes |
|---|---|---|
| `LayoutWorkflow` | **100%** ✅ | `LoadFromOptions` (null/populated/invalid entries), `TryGetLayout`, `SaveLayoutAsNew` (default and name-collision skip), `SaveLayoutToExisting` (invalid name/capture, valid overwrite), `RestoreDefaultLayout` (null/non-null), `RemoveLayout` (missing/unselected/selected/last-remaining), `BuildNamedLayouts` — all paths covered by `LayoutWorkflowTests` |

### New code introduced in the scheduled-jobs workflow slice (2026-06-12)

> Measured with `dotnet test src/Arbor.HttpClient.Desktop.E2E.Tests/... -- --filter-class "...ScheduledJobsWorkflowTests" --coverage --coverage-output-format cobertura` (Microsoft.Testing.Extensions.CodeCoverage).
Expand All @@ -24,6 +32,15 @@ As of the latest build (2026-04-24, after adding Kestrel integration tests for W
|---|---|---|
| `ScheduledJobsWorkflow` | **100%** ✅ | Add (interval clamp), remove (null/persisted/unsaved), load (auto-start on/off, follow-redirects default, reload), dispose — all paths covered by `ScheduledJobsWorkflowTests` |

### New code introduced in the request tabs + history slice (2026-06-12)

> Measured with `dotnet test src/Arbor.HttpClient.Desktop.E2E.Tests/... -- --filter-class "...RequestTabsWorkflowTests" --filter-class "...RequestHistoryWorkflowTests" --coverage --coverage-output-format cobertura` (Microsoft.Testing.Extensions.CodeCoverage).

| Class | Line coverage | Notes |
|---|---|---|
| `RequestTabsWorkflow` | **100%** ✅ | Add tab, close (null tab, only tab, inactive tab, active tab with later tab, active last tab) — all paths covered by `RequestTabsWorkflowTests` |
| `RequestHistoryWorkflow` | **100%** ✅ | Load (ordering, search filter), `ApplyFilter` (case-insensitive match across name/url/method, empty-query restore, item identity preservation), `BuildEditorProjection` (null and non-null body) — all paths covered by `RequestHistoryWorkflowTests` |

### New code introduced in the collections UI workflows slice (2026-06-12)

> Measured with the `dotnet-coverage` CLI (`dotnet-coverage collect -f cobertura -- dotnet test src/Arbor.HttpClient.Desktop.E2E.Tests/...`) because the test projects had moved to Microsoft.Testing.Platform, which ignores the VSTest-based `coverlet.collector`. With this tool the Desktop project measured **70.4% line coverage** overall (not directly comparable to the coverlet baseline below, but no decrease). The tooling gap has since been fixed: the test projects now reference `Microsoft.Testing.Extensions.CodeCoverage`, so plain `dotnet test ... -- --coverage` works again — see [Local Coverage Workflow](#locally) and [CI Integration](#ci-integration).
Expand Down
Loading
Loading