The desktop v2 design is the other half of the one the phone took last commit, and this is its chrome: a 190-pixel labelled sidebar where the 54-pixel icon rail was, a titlebar with the search box centred in it, and session tabs drawn as pills. The palette was already here — it is shared, and moved when the phone's did — so what this changes is shape rather than colour. **The window's minimum grew, and by exactly what v2 added.** The sidebar is 136 wider and the chrome 14 taller, so 880x560 became 1016x574. That is not a round number somebody liked: it leaves every screen the same 826x464 it was designed against, which is the arithmetic the layout suite is built on. Four of the tables stop fitting at 690 wide, so widening the sidebar and leaving the window alone would have broken them somewhere no test was looking. LayoutHarness carries the new constants and the suite still passes at the minimum, which is the whole reason it exists. The rail's five-character abbreviations are gone with the width that caused them — PINS and SNIPS are Pins and Snippets again — and each row gains a glyph and a count. A count is drawn only where one is real, so SFTP, Logs and Preferences show nothing rather than a zero: a transfer queue's depth is not how many files a screen holds, and a log has no total until it is read. The count beside Pins is the vault's own, not the Pins screen's VisiblePins, which is the filtered list and would have made the sidebar count whatever somebody had just typed into a filter box on another screen. Teams has no count for a related reason: they are read from the server when that screen is opened rather than on unlock, so a number there would read 0 until somebody had already been to look. One colour moved with it, finishing what the repalette started: the live-session summaries on the unlock and sign-out cards were Info, so the two heads disagreed about a fact the phone paints green. They match again. **Buckets became a destination rather than a mode**, which is what the design draws and what the phone already does. The HOST / BUCKET pair inside the files screen is gone; ShellScreen.Buckets draws the same TransfersScreen with the other picker, and the sidebar entry is what sets it. That also settles an old disagreement rather than merely moving it: TotalItemCount is keys plus passwords and excludes buckets, so the number beside the keychain used to disagree with the list under it, and now counts what that screen shows. There is one session behind both file destinations, so asking for the other kind while something is open is refused rather than obeyed — and refusing means staying put. An earlier turn of this had it move anyway and only decline to switch the picker, which put the S3 entry in the sidebar over a screen still listing an SFTP host: two pieces of chrome disagreeing about where you are, which is worse than the navigation simply not happening. The message that says so goes to Transfers.Status, which turned out to be drawn in the same grid cell as the connected chip — survivable while it was mostly read before connecting, and not once a refusal reports itself there. It has its own column now. The design has nine entries' worth of screens and draws five. Pins, Teams, Import and Preferences are built, working screens, so they keep their entries — the sidebar is labelled now and has the room, and dropping an entry would have stranded a screen rather than simplified anything. The Team vault card the design pins to the foot is not drawn: it is a second route to a screen already in the list, carrying a seat count nothing here produces. **The status bar survives the design that deletes it**, cut down to one thing. Two of the three facts it carried moved into the titlebar with v2 — the sync word is beside its dot and the shortcut hint is inside the box that uses it — so those are gone from it rather than printed twice. The third is Vault.Status, the only channel this application has for saying a save failed or a merge picked a winner. The design is a mock-up of an afternoon that goes well and has nowhere to put a sentence like that; dropping the bar would have meant dropping the sentence or repeating it on nine screens. What v2 draws and this does not is in docs/design-import-gaps.md, and it is the same list as the phone's for the same reasons: the forwarding screen and both its chips, the host detail's fingerprint, tags and last-session cards, the keychain's rotate button, the logs' FOLLOW pill and severity filters, and the session footer's latency. The terminal is not inset behind a rounded frame either — it is a native child window that composites above everything Avalonia paints, so the frame would clip nothing, which is the same answer the phone gave. **The light theme is not built.** Its accent is #6D5AE6, a different hue rather than a tint of the dark one, so it needs every colour doubled, a variant to switch on, the renderer's own page switching with it, and contrast checked twice. That is a piece of work rather than a setting, and it is separable from the layout — which is why this commit is the layout. The screens themselves are restyled through the shared vocabulary rather than rebuilt: corner radii, chips, cards and the accent's ink, all in App.axaml, so every screen moves at once. Their layouts are left alone deliberately. The design draws read-only detail panes and these screens carry the editors and forms it has no equivalent of, so replacing a layout with the mock-up's would have lost the half that is actually used. Verified by the whole suite: 1309 tests over nineteen projects, none failing, including the 68 layout cases that stand up real Avalonia and measure every screen at the new minimum. Both heads build. Not run on a machine with a display — see docs/manual-checks.md for what wants looking at. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZE3u99BNt6LzgTC5jhbz2
32 KiB
Things a person still has to check
Automated tests cover what they can reach. This file is the rest: the checks that need a real window, a real network, a real remote host, or a real Explorer — and the reasons each one is out of reach.
Three constraints put things on this list, and they are worth knowing before adding to it:
MainWindowcannot be laid out by a test. WebView2's adapter refuses the headless dispatcher's MTA thread — seeLayoutHarnessTests.WhyTheWindowItselfIsNeverShown. Anything that has to be measured lives on aUserControlinstead, and what is left in the window is unmeasured by construction.- Headless Avalonia has no native window. So nothing about Win32 focus, about the WebView collapsing, or about a drag that crosses into another application can be asserted. A headless test of any of those would pass and confirm the wrong belief.
- No network, no container, no remote host in the ordinary suite. The SSH suites that do use one
(
DodoSSH.Client.Ssh.Tests,DodoSSH.SystemTests) need Docker and are the exception.
Each item says what to do, what a pass looks like, and what a failure would mean.
Phase 1 — the shell and the tab strip
1.1 No screen is sliced at the WebView's left edge · the important one
Open two terminals, then visit every nav rail entry in turn — HOSTS, FILES, KEYS, TEAM, PREFS.
Pass: each screen draws whole, its buttons all clickable, and the tab strip stays across the top of all five.
Failure means: a screen is not collapsing while the terminal shows. The terminal is a native child
window and composites above everything Avalonia paints, so the symptom is a screen cut off at the WebView's
left edge with the rest unreachable. This window has shipped that defect once. The single wrapper Panel
bound to IsShowingPages in MainWindow.axaml is what should make it impossible.
1.2 A tab clicked from another screen takes the keyboard
Go to FILES with a terminal open. Click the tab. Start typing immediately, without clicking anything else.
Pass: every character reaches the shell, including the first.
Failure means: FocusTerminalWhenLaidOut in MainWindow.axaml.cs is posting too early.
NativeControlHost re-pushes its bounds on the next layout pass, so focusing ahead of that pass races the
thing the focus depends on. The symptom is losing only the first keystroke or two, which is why this has to
be typed immediately rather than after a pause.
1.3 Leaving a terminal gives the keyboard back
With a terminal focused, click FILES. Type into the filter box.
Pass: the characters appear in the box.
Failure means: ReleaseKeyboardTo is missing on that path. Collapsing the WebView does not release
the keyboard — the native child window goes on holding Win32 focus and Avalonia then sees no key events
at all, so the screen that just appeared silently swallows everything. This was a latent bug before the
strip rework and is now on the hot path. See docs/platform-flags.md.
1.4 The middle click closes tabs and only tabs
Middle-click a tab (closes it), the strip background to the right of the last tab (closes nothing), and the
+ button (closes nothing, opens nothing).
Pass: as described. Covered by TerminalTabsTests headlessly, so this is a confirmation that headless
pointer input matches a real mouse rather than a first look.
1.5 Connecting from the palette while on another screen
Press Ctrl+K from the FILES screen and connect to a host whose key is not yet approved.
Pass: the window lands on HOSTS with the fingerprint prompt visible and answerable.
Failure means: the prompt is behind the screen that asked for it, and the connection is blocked on a question that cannot be reached.
Phase 2 — Known Hosts as its own page
2.1 The fingerprint column is readable end to end
Connect to two or three hosts, approving each fingerprint. Go to Pins and narrow the window to its minimum (1016px since the v2 sidebar; it was 880 while the rail was 54 wide), then widen it to something ordinary.
Pass: the full SHA256:… is on screen at both sizes, never cut off and never ellipsised.
Failure means: the one thing this screen is for has been broken. A truncated fingerprint cannot be
compared against a published one — it can only be glanced at, which is the habit pinning exists to replace.
TheHostKeysScreenFitsWithPinsAndOneSelected measures this at the minimum width, so a failure here is a
size the harness does not cover.
2.2 The APPROVED date is plausible
Pass: it is roughly the day you first connected to that host.
Failure means: the version 7 identifier is being read in the wrong byte order — the symptom is dates
tens of thousands of years out, not an error. Covered by Uuid7TimestampTests, so this is a confirmation
that the ids reaching the screen really are the ones this client minted. A pin restored from an older
client or another implementation shows —, which is correct rather than a failure.
2.3 Forgetting a pin reaches the server
With two machines signed in to the same account: forget a pin on one, sync the other.
Pass: the pin is gone on both, and the second machine asks you to check the fingerprint again on the next connection.
Failure means: the forward from the screen's command to the vault's has lost the push. Withdrawing trust that stays withdrawn only locally is the failure mode that matters here — the machines still refusing to reach a rebuilt server are the other ones.
Phase 2 — Generating a key
Most of this one is covered: KeyAuthenticationTests.AKeyThisClientGenerated_AuthenticatesAgainstARealServer
installs a generated public line on a real OpenSSH server in a container and connects with the private half,
for both algorithms. That is the claim that mattered, and it is automated. What is left is the interface
around it.
2.4 COPY PUBLIC KEY actually reaches the clipboard
Generate a key, save it, select it, press COPY PUBLIC KEY, then paste somewhere.
Pass: one ssh-ed25519 AAAA… comment line.
Failure means: the clipboard closure in App.axaml.cs is not finding the window. No test can see this —
the view models take a delegate precisely so they never touch a visual, which means the one real
implementation of that delegate is exercised by nothing but a person. CopyingAPublicKey_WithNoClipboard_SaysSo
covers only the branch where there is none.
2.5 RSA-4096 does not freeze the window
Choose RSA 4096 and press GENERATE. While it runs, drag the window and click around.
Pass: the window keeps painting and the status line says it is working.
Failure means: the Task.Run is not actually taking the work off the UI thread. Ed25519 is instant and
will not show this, so it has to be tried with RSA.
2.6 The generated key works end to end, by hand
Generate a key, save it, copy the public line, add it to a real host's ~/.ssh/authorized_keys, bind the
host to the key in its editor, and connect.
Pass: it connects without a password.
This duplicates the container test on purpose. The container runs one image; the thing worth knowing is that it works against whatever you actually run.
Phase 2 — Importing ssh_config
The parser has 22 cases over the shapes a real file contains, and the end-to-end path is covered by
ImportingAnSshConfig_ShowsItFirstAndThenStoresWhatWasTicked. What no test can do is read your file.
2.7 Scan your own ~/.ssh/config and read the preview against the file
Preferences → IMPORT HOSTS → SCAN. Do not press import yet.
Pass: every entry you would expect is listed, with the address and port you expect, and the warnings above the table account for anything missing.
What to look for specifically:
- A
Host *block'sUsershould appear on hosts that set none, and not override hosts that set one. - Entries whose name is a pattern (
*.internal,bastion-?) should be absent from the table and named in the warnings. Matchblocks should be counted in the warnings and their settings should not have leaked onto any host.- A
ProxyCommandshould be reported as dropped, not silently kept.
Failure means: the importer and ssh disagree about what your file means, which produces bookmarks
that nearly connect. That is worse than an import that refused, so it is worth reading the table properly
once.
2.8 Nothing is written until the button
Scan, then navigate away without importing.
Pass: the Hosts screen is unchanged.
2.9 Imported hosts are correct
Import a couple, then open one on the Hosts screen.
Pass: the address, port and username match the config, and the notes record any IdentityFile path and
any ProxyJump — with ProxyJump clearly stated as not routing. Connecting should ask for a password even
where the config named a key, because no key material is read; binding it to a key in the keychain is a
separate act.
Phase 2 — Drag and drop on the SFTP page
This is the least-covered thing in the repository, and unavoidably so. Headless Avalonia has no native
window and cannot synthesise a platform drag, so a test that claimed to drop a file from Explorer would
pass while confirming nothing. What is automated is the policy — TransferQueueingTests covers what may be
queued, what is skipped and what is said about it — and the wiring between a real drag and that policy is
covered by nothing at all.
Connect the SFTP page to a host first. All four of these should queue transfers.
2.10 Explorer → remote pane
Drag one file, then several, from Explorer onto the right-hand pane.
Pass: the pane outlines in accent colour while the pointer is over it, and the drop queues one transfer per file into the directory showing.
2.11 Local pane → remote pane
Pass: as above. This uses the same platform file format as the Explorer drag, so a failure here with 2.10 passing points at the drag source, not the drop target.
2.12 Remote pane → local pane
Pass: the left pane outlines and the drop queues a download.
2.13 Local pane → Explorer
Pass: the file copies out.
2.14 The highlight clears · the one most likely to be wrong
Drag something over a pane and then out of it again without dropping.
Pass: the outline appears and then goes away.
Failure means: an overlay is participating in hit testing. It lays out identically either way — which is
why the layout test cannot catch it — but once visible it swallows the DragOver events underneath it, so
the pointer appears to leave immediately, the highlight sticks, and the drop lands nowhere. The fix is
IsHitTestVisible="False" on the highlight Border in TransfersScreen.axaml.
2.15 Dropping while disconnected
Disconnect, then drag a file over the remote pane.
Pass: the pane outlines in red and says "Connect to a host first." Nothing is queued on drop.
2.16 A click still selects a row
Click rows in both panes, and drag a row a few pixels without releasing.
Pass: a click selects; a small movement does not start a drag.
Failure means: the 4-pixel threshold in TransfersScreen.axaml.cs is not doing its job, and selecting a
row has become impossible.
Not implemented: remote pane → Explorer
Dragging a remote file out to Explorer is deliberately absent. It needs the source to supply a virtual
file — on Windows, CFSTR_FILEDESCRIPTORW plus CFSTR_FILECONTENTS with delayed rendering — and Avalonia's
IDataTransfer marshals DataFormat.File only from a storage item that resolves to a real local path.
Pre-downloading to a temp file does not help: the shell demands the bytes during the drop. The only route is
a native COM IDataObject behind a platform interface, Windows-only and outside Avalonia's supported
surface. Use the ← button, or drag the file to the local pane first.
Phase 3 — Host groups and snippets
Two synced item kinds, a sidebar that now draws headings, and one new frame between the host process and the
renderer. The data half of all of that is covered: the payloads round-trip, the server refuses the plaintext
fields, the sidebar's grouping and the snippet policy are in ShellFlowTests, and both new screens are
measured. What is left here is the part that only exists inside a WebView, plus the two-machine cases no
single-process test can reach.
3.1 A keychain with no groups looks exactly as it did
Open the hosts screen without creating any group.
Pass: the sidebar list is the flat list of hosts it always was — no headings, no UNGROUPED, nothing saying the hosts are unfiled.
Failure means: the "invisible until used" property is gone, and every existing user gets a heading they
did not ask for. RebuildSidebarRows returns early when Groups is empty; that early return is the feature.
3.2 Filing hosts, and folding a heading
Make two groups, file some hosts into each through the host editor, then click a heading.
Pass: the heading's chevron flips and its hosts disappear; the count on the heading does not change, because it counts what is in the group rather than what is on screen. Clicking again brings them back.
Also check: clicking a heading does not change which host is selected — the buttons at the foot of the
sidebar go on acting on the same machine. This is asserted in a test, but the test drives the view model
directly; what it cannot see is whether the ListBox writes something else back through the binding first.
3.3 Deleting a group with hosts in it
Select a group with hosts and press DELETE.
Pass: the question names how many hosts are filed under it and says they stay. Agreeing removes the group; the hosts reappear under UNGROUPED with everything else about them unchanged.
Failure means: if the hosts vanish, the delete is rewriting host payloads, which it must not — see
HostGroupRepository.
3.4 A group deleted on another machine · needs two machines
Make a group on machine A, file a host into it, sync. On machine B, sync, then delete the group and sync again. Back on A, sync.
Pass: A shows the host under UNGROUPED. Open that host's editor: the group picker shows "(a group that is no longer here)" and keeps it selected. Change the port and save.
Failure means: if the picker opened on "No group", saving has just unfiled the host — quietly, as a side
effect of an unrelated edit. That is the case BuildGroupChoices adds the placeholder for.
3.5 A grouped host stays editable on an older build · needs two builds
Only worth doing before a release that ships alongside an older client. A host filed into a group is written at payload schema 4; an older build must show it and refuse to edit it, rather than editing it and dropping the group.
Pass: the older build says the host was written by a newer version. A host with no group still edits normally there — that is what makes the version a maximum over the fields present rather than a stamp.
3.6 Inserting a snippet · the one that cannot be tested here
Open a terminal, go to SNIPS, select a snippet with Press Enter after inserting this off, and press the
insert button.
Pass: the terminal comes forward with the command sitting at the prompt, not run. The button named the tab it was going to — check that it named the right one if several are open.
Failure means: if the command runs by itself, the flag byte or the JavaScript that reads it is wrong. If nothing appears at all, the frame reached a pane the page does not have.
3.7 A multi-line snippet does not run line by line · the reason the opcode exists
Save a snippet whose command is three lines — echo one, echo two, echo three — with the run flag off,
and insert it into a bash or zsh session.
Pass: all three lines sit at the prompt as one pending command, and nothing runs until Enter. Modern
shells turn bracketed paste on, xterm.js sees that in the output stream, and term.paste wraps the text.
Failure means: if the first two lines execute and the third waits, the text went through as plain input and the bracketing did not happen — which is the whole failure this frame was added to prevent.
Then insert the same snippet into something with bracketed paste off — a raw sh, or a session inside
vi. The lines will run there, and that is correct and unavoidable: without the mode there is no way to
distinguish pasted newlines from typed ones. It is why the screen says "types this into whatever is there"
rather than "runs this command".
3.8 RUN presses Enter
Select a snippet with the run flag on and press RUN.
Pass: the command runs.
Failure means: if the command appears and does not run, the \r is going through paste instead of
input — inside the bracketed wrapper it is literal text, so nothing executes.
3.9 Inserting into a tab whose remote has hung up
Open a terminal, exit it, leave the tab open, then insert a snippet at it.
Pass: the screen says that tab is no longer connected, and nothing is claimed to have been sent.
3.10 Both new kinds reach a second machine · needs two machines
Make a group and a snippet on A and sync; sync B.
Pass: both arrive, with their names and — for the snippet — its run flag intact. A snippet whose flag arrives set when it was saved unset is the one failure here worth stopping for.
Phase 4 — Logs
Both logs are synced item kinds with the whole pipeline covered: payloads round-trip, the server refuses every plaintext field, retention is tested against a real vault, the activity hook is tested through the repository every kind writes through, and the recursion guard has a test of its own. What is left here is the part that only happens across a lock, across a process exit, or across two machines.
4.1 A connection is recorded when the tab closes, not before
Connect to a host, leave the tab open, and open LOGS.
Pass: the connection is at the top of CONNECTIONS with a green dot and the words still open — not a dash, and not a duration. Close the tab and press REFRESH: the same connection now has a duration.
Failure means: a dash instead of "still open" reads as a recording that failed, which is the opposite of what is happening. A duration before the tab closes means an entry is being written at open, which would also mean it gets written twice.
4.2 The duration is plausible
Connect, wait a measured minute or two, disconnect.
Pass: the LASTED column agrees with the clock, rounded to whole units.
Failure means: a wildly wrong number points at the two timestamps coming from different clocks — both
should come from the workspace's own TimeProvider.
4.3 Closing the application records the tabs that were open · the one most likely to be wrong
Open two terminals and close the DodoSSH window without closing the tabs. Start it again, unlock, open LOGS.
Pass: both connections have entries, with durations running up to the moment you closed the window.
Failure means: they are the ordinary way a session ends, and the workspace's own close-outs happen while
it tears sessions down — after the vault they would be written into has gone. ConnectionRecorder's
DisposeAsync closes the tickets itself, before the session is disposed, and waits up to two seconds for
the queue. If the entries are missing, that ordering has been broken; if closing the window became slow,
the bounded wait has.
4.4 A shell open across a lock still gets its entry
Connect to a host, lock the keychain from the titlebar, unlock again, then close the tab.
Pass: the connection is recorded, into the vault it was made in.
Failure means: the ticket keeps the repository it was opened against precisely so this works. A missing entry means it is reading the current one instead, which would also mean an entry could be filed into the wrong vault once shared vaults land.
4.5 A refused host key is recorded
Connect to a host, approve its key, then change the key on the remote (or edit the pin) so the next connection is refused.
Pass: an entry appears with host key refused beside it. This is the row the connection log most exists for — a changed host key is refused with no way past it, so the status line is otherwise its only trace.
4.6 The SFTP session is recorded separately
Open the files screen and connect, then disconnect.
Pass: an entry with files in the KIND column, separate from any terminal entry.
Failure means: if it is missing, our log disagrees with the remote's own auth.log, which records the
second login. Anybody comparing the two would be right to believe the host.
4.7 The keychain log records names and never values
Edit a stored password: change both the password and the username. Open LOGS → KEYCHAIN.
Pass: one row saying changed, with Password, Username in the FIELDS column.
Failure means: if any part of the old or new password appears anywhere on that screen, stop — that is the one thing this payload must never carry, and it would now be synced to every machine in the vault.
4.8 A pin trusted at the prompt is recorded
Connect to a host you have never reached and approve the fingerprint.
Pass: the keychain log shows a KnownHostKey created. This write never goes through a screen, so it
is exactly the one a hook placed in the view models would have missed.
4.9 The pending count and the status line stay honest
Save a host while online and watch the titlebar and the status line for a minute.
Pass: the pending count returns to zero and stays there, and the status line keeps saying what the save said — it does not get overwritten a moment later by a sync report.
Failure means: every user action queues a log entry a moment afterwards. If the count sticks at 1 or the status line flickers to "Synchronised: 1 out", the log entries have stopped being excluded from the two numbers that are about the user's own work.
4.10 Both logs reach a second machine · needs two machines
Connect and edit something on A, sync; then sync B and open LOGS there.
Pass: both entries are on B, with A's device name on them. This is the claim the whole decision to sync these rests on.
4.11 Retention actually prunes · slow, or needs a clock
Only checkable honestly by leaving a vault in use for months, or by temporarily lowering
LogRetention.Default in a debug build and watching a prune remove the excess and push the tombstones.
Pass: the count comes down, and the second machine's copy comes down too at its next sync.
Failure means: these entries sync, so a prune that does not push leaves every other machine holding them — and this machine deleting them again on every pass.
Phase 6 — S3 as a remote in the file browser
The parts that are this client's own reasoning are covered: the path-to-key translation, what a bucket must have before it can be stored, the server's refusal of every plaintext field, and the item kind end to end. What is left needs a real endpoint, and a fake would only assert our reading of the protocol back at us.
Get a bucket first. MinIO in Docker is the cheapest way and exercises the harder path — path-style addressing, a custom endpoint, and a region that is ignored:
docker run -p 9000:9000 -e MINIO_ROOT_USER=dodossh -e MINIO_ROOT_PASSWORD=dodossh-secret minio/minio server /data
6.1 Adding a bucket
Keychain → BUCKETS → + BUCKET. Endpoint http://localhost:9000, path-style on, any region.
Pass: it saves, appears in the list with the bucket and endpoint under its name, and syncs.
Failure means: if saving is refused, read the message — the validation exists so the reason names the field rather than arriving later as an SDK error about resolving a URI.
6.2 Path-style addressing · the one most likely to be wrong
Save the same bucket with path-style off and open it.
Pass: it fails, and the failure is a name-resolution error mentioning bucket.localhost.
Why it is worth doing deliberately: that is exactly what a user gets when they leave the checkbox at its default against a self-hosted service, and the message names neither buckets nor the setting. Seeing it once is what makes the hint under the checkbox worth its space.
6.3 Browsing
Files → BUCKET → pick it → OPEN.
Pass: the right pane lists the bucket root. Prefixes appear as directories in the directory colour;
objects appear as files with sizes and dates. The PERMS column is empty — a bucket has no POSIX mode, and a
plausible -rw-r--r-- would be invented.
Also check that the timestamps agree with the local pane's. Both columns are UTC; if the bucket's are out
by your machine's offset, the DateTime.Kind handling in S3FileStore.Utc has regressed.
6.4 Upload, and the pipe underneath it
Upload a file of a few hundred megabytes.
Pass: it completes, the object is in the bucket at the right key and the right size, and the machine's memory does not grow with the file. The upload streams through a pipe into a multipart upload — nothing is buffered to disk twice and nothing is held whole in memory.
Failure means: if it hangs at the end, the pipe's writer is not being completed on disposal. If memory tracks the file size, the multipart path is not being taken.
6.5 A failed upload surfaces at the write
Start an upload and stop MinIO halfway.
Pass: the queue row fails with a message from the service, reasonably promptly.
Failure means: if it hangs instead, the background upload is failing without completing the pipe's
reader — and the copy is blocked on a pipe nobody is draining. That is the case S3UploadStream completes
the reader with the exception for.
6.6 Download, and resume
Download a large object, stop it partway, and resume.
Pass: it resumes from where it stopped, and the finished file matches the original. This direction is the one where a bucket is better than SFTP — a ranged GET is part of the protocol.
6.7 Upload resume is refused, and says why
Start an upload, stop it partway, and press RESUME.
Pass: it fails with a message saying an object cannot be written from the middle, so an interrupted upload starts again rather than resuming. RETRY from the start works.
Why this is not a bug: objects are immutable. Multipart could rebuild an interrupted transfer, but only by persisting the upload id and every part's ETag across the interruption. Refusing is honest; silently starting from zero would corrupt the file.
6.8 Delete refuses a prefix with anything under it
Try to delete a directory in the bucket that has objects in it.
Pass: refused, saying there are still objects under it. Deleting an empty one works.
6.9 The keys never appear anywhere they should not
After adding and editing a bucket, open LOGS → KEYCHAIN.
Pass: the entry says Secret access key in the FIELDS column and nowhere on that screen does any part of
the key itself appear.
6.10 Against real AWS · needs an account
Repeat 6.1 and 6.3 with the endpoint blank, a real region, and path-style off.
Pass: it lists. This is the path that exercises RegionEndpoint.GetBySystemName and virtual-host
addressing, neither of which MinIO covers.
Phase 7 — Connecting without waiting, and the host list's pointer
Connecting no longer holds the vault while it happens: a tab appears at once and the handshake runs behind
it. The state machine around that is covered in ShellFlowTests — the tab, the card, the two kinds of
failure, two connections at once — and the card itself is measured. What is left needs a real window: a
native drag, a real menu popup, and a host that takes its time answering.
7.1 The window stays usable while a connection is being made · the point of the change
Connect to something that will take a while — an address that is routable but silent, so the handshake sits
there rather than failing at once (a firewalled IP is ideal; 10.255.255.1 will do). While it runs: click
around the host list, open KEYS, edit something, connect to a second host.
Pass: everything works. The strip shows a tab named for the host with connecting… beside it, and the
terminal area shows the host, the address it is dialling, and the same word.
Failure means: the connect has ended up back inside RunAsync's busy gate, which is the one thing
VaultViewModel.ConnectAsync must never be put inside again.
7.2 Navigating away during a connection unlights the tab
While 7.1's connection is still running, click KEYS.
Pass: the tab stays in the strip and stops being marked as the active one. No accent bar along its top edge while a page is showing.
Failure means: the tab is bound to IsSelected again rather than to IsShowing, and the strip is
claiming to be what the window is showing when it is not.
7.3 A refusal ends up in the tab, not only in the status line
Connect to a host with the wrong password, and navigate to PREFS before it answers.
Pass: the tab goes grey and carries the refusal; selecting it shows the reason in the middle of the window with a CLOSE TAB button. Closing it returns to the page underneath.
Failure means: a connection that failed while the user was elsewhere has left no trace anywhere they would look.
7.4 An unknown host key still wins the window
Connect to a host whose key is not yet approved.
Pass: no tab is left behind, and the window is on HOSTS with the fingerprint prompt. Approving it connects and gives the tab a pane. This is 1.5 with the tab rework underneath it.
7.5 Giving up on a connection, and what arrives afterwards
Start 7.1's slow connection and press GIVE UP (or the tab's cross) while it is still trying.
Pass: the tab goes at once. If the host does eventually answer, a tab appears for the session that opened — it is a real shell, and one running with nothing naming it would be worse than one that comes back.
7.6 Dragging a host into a group · least covered, like all drag and drop
Make two groups and file a host into one. Drag a host row onto another group's heading; onto a host row inside another group; and onto UNGROUPED.
Pass: the row under the pointer washes accent while the pointer is over it, the cursor shows a move rather than a refusal, and the drop files the host — it moves under that heading and the counts on both headings change. Dropping onto its own group's heading is refused while still in the air.
Failure means: headless Avalonia cannot synthesise a platform drag, so nothing about this gesture is
automated. The write it performs is: MovingAHostToAGroup_FilesItAndLeavesItSelected.
7.7 A click still selects, and a double click still connects
Click host rows; drag one a few pixels without releasing; double-click one.
Pass: a click selects, a small movement starts nothing, and a double click connects.
Failure means: the 5-pixel threshold in HostSidebar.axaml.cs is not doing its job — the same failure
as 2.16 on the other screen, and here it would make the list unusable.
7.8 The highlight clears after a drag that goes nowhere
Drag a host over a heading and release outside the list, or press Escape mid-drag.
Pass: the wash goes away.
7.9 The right-click menu acts on the row under the pointer
With host A selected, right-click host B and choose Delete.
Pass: the question names B. Then right-click a group heading.
Pass: no menu opens at all, and the host selection has not moved.
Failure means: a menu acting on the selection rather than on the row under the pointer deletes the wrong
machine. HostSidebarTests covers both halves headlessly, so this is a confirmation that a real popup
behaves as the headless one did.
7.10 Clicking a host in the palette connects
Ctrl+K, then click a result with the mouse rather than pressing Enter.
Pass: the palette closes and the connection starts.
7.11 A host or a bucket made now can be picked on FILES now
With the files screen's picker open on an empty or short list, go to HOSTS and add a host — or to KEYS and add a bucket — then come back.
Pass: the new one is in the picker, without locking and unlocking.
Failure means: the screen has gone back to copying the vault's lists once at unlock. Covered by
TheTransfersScreen_FollowsTheVaultsHostList; this is the version of it with a real picker in front of it.