Public Access
The WebView sat inside the Hosts grid, so navigating to Files or the keychain hid every open terminal and the strip that named them. A connection you had opened was invisible from four of the five screens. The window now has two surfaces rather than one: a nav rail that says which page you are on, and a terminal strip that is always there and switches the whole content area to a shell. Screen keeps meaning "which page" and never becomes a sixth kind of page, which is why this is two properties instead of one enum with a terminal member in it. Every screen lives inside one wrapper panel that collapses when a terminal is showing. That is not tidiness — the WebView hosts a Win32 child window that composites above everything Avalonia draws, so a screen left visible over its rectangle is a screen sliced in half, and this window has shipped that defect once already. One decision point, IsTerminalShowing, and a nested panel rather than five compound bindings nobody would remember to extend. The focus choreography is the part no test in this repo can see. Every reveal path now focuses in the same turn the WebView appeared, so all three of them post at DispatcherPriority.Loaded and let the native control re-push its bounds first. Going the other way had a real bug: the screen-changed branch called a bare Focus() where it had to release the keyboard from the native child, so switching from a terminal to Files silently ate the first keystrokes. Rare before this commit and the primary gesture after it. The tab strip grew a cross inside each tab, a plus that opens the quick-connect palette, and middle-click close. Nested buttons are correct here: Avalonia handles a left press on the cross and deliberately does not handle other buttons, which is exactly what lets middle-click bubble up from the cross as well as the tab. The test is PointerUpdateKind rather than IsMiddleButtonPressed, because the latter reports button state and is also true for a left press made while the middle button happens to be held. The handler is on the tab and not the strip, so the background closes nothing by construction. Plus opens the palette rather than a flyout, since a menu dropping into the WebView's rectangle may or may not composite above a child HWND and this repo does not make rendering claims it has not photographed. Everything a user reads now says keychain. The wire, the database and the cryptographic spec still say vault, deliberately: renaming those is a migration and a protocol change for a word. That split is written down rather than left to be rediscovered as an inconsistency. Four things that were squeezed into the keychain's category rail, or into nothing at all, now have screens. Pinned host keys get one, with fingerprints never truncated and a filter that matches them, because comparing what you have against what the operator published is the whole workflow; the approved date is read out of the item's UUIDv7 rather than added as a column, and says so, since it means first approval and not last use. Keys can be generated in the client, which needed the openssh-key-v1 container written by hand — there is no BCL or NSec helper, and the PKCS#8 route is unverified in the SSH library this uses. The armour carries no passphrase: encrypting it needs bcrypt_pbkdf, which is Blowfish with a swizzle, in a project whose crypto is otherwise entirely libsodium, for a protection the key's own remarks argue is redundant inside a vault. Generation fills the existing editor and stops, so SAVE stays the one thing that writes. ~/.ssh/config can be imported behind a preview that is ticked per row and writes nothing until the button; IdentityFile records the path and imports the key material only on an explicit opt-in, because reading somebody's private key into a vault is precisely the act this product exists to make deliberate. Match blocks and ProxyJump are reported rather than obeyed — one cannot be evaluated statically and the other has nothing behind it to route with, and a preview that implied otherwise would be worse than one that admits it. Files can be dragged in all four directions that are honestly available. Remote to Explorer does not ship and is not pretended to: the shell wants the bytes during the drop, which needs a virtual file and a native COM data object, outside what Avalonia offers. Note for the next person that Avalonia 12 replaced the drag model outright — DataObject and DataFormats are no-op stubs and IDataObject is not in the reference assembly, so every tutorial written for 11 does not compile here. Hosts can be grouped, flat and never nested. A parent id merged as a scalar lets two offline clients each re-parent A under B and B under A, producing a cycle inside an encrypted payload that no server can police and every reader would have to detect for ever. Membership lives in that payload rather than in the one plaintext concession ADR 0001 allows, whose test is that the relay cannot function without it — nothing on the server reads a group, so what plaintext would hand over is a clustering of the estate for nothing. The plaintext column reserved for it is dropped, provably always null, and the server now refuses a client that sends one; it was never populated, was copied on apply, and was not cleared on delete, so a group id would have outlived the host it described. Snippets insert through xterm rather than through the pump, because xterm is the only thing that knows whether the remote has bracketed paste on, and that is what makes a shell treat embedded newlines as text instead of as execute. The host process moves opaque bytes and never parses output, so it would have to guess, and guessing wrong runs every line. Running is off by default and the copy says the text goes into whatever is there — the terminal has no notion of being at a prompt, and may be in vi or at a password prompt with echo off, so the Enter the user presses themselves is the entire safety property. Connections and keychain changes are recorded as synced encrypted items, which is what makes them auditable by a team later and costs the server knowledge of connection rate and timing from row counts alone. ADR 0001 already concedes it cannot hide that class of metadata; the trade is now written into it rather than left implicit. A connection entry is written once, at close, which is what makes a synced log tractable: nothing to merge, one outbox row, no chance of colliding with itself. Live sessions come from memory, not from the log. The write is void by contract and posts to a bounded channel, because putting an encrypt-and-write on the teardown path of every session is how closing the application comes to take four seconds. A ticket opened before a lock still closes afterwards, since a shell outlives the vault. The activity log hooks the one generic repository every kind writes through, so it cannot miss a caller — which is also why the log kinds themselves declare they are not audited, or the first entry would write an entry about writing an entry. It records the names of the fields that changed and never their values; a log with an old password in it would be a plaintext credential store with no vault around it. Retention is 90 days or 5,000 entries, whichever bites first, pruned on the sync loop rather than on a second timer. That log traffic then broke the status line, which is worth recording because the fix is a shape and not a patch: background sync counted its own log rows as pushed items, so the quiet rule stopped being quiet and every action's message was overwritten a second later by a sync report. The report now separates log rows from user items and the rule reads the latter. S3 buckets appear as a remote in the file browser, behind the same interface an SFTP session implements, so the queue and both panes did not have to learn what they are talking to. Uploads go through a pipe, because the queue wants to write and the SDK wants to read; memory is then bounded by the part size instead of buffering a file to disk twice. Finally, the Windows device key store moved out of the session project, which was the one thing keeping it from being portable — everything else in it is platform-neutral, and a Windows CNG dependency in the middle of the vault code meant a second head could not reference it without dragging Windows along. The seam that made the move free was already there. docs/android-port.md is the audit behind that: what ports, what does not, in order of cost, the four decisions taken, and an inventory of every screen and state the interface has to carry, written so a design can be made from it directly. dotnet build, dotnet test and dotnet format --verify-no-changes are all clean: 1240 tests at zero warnings, including the end-to-end suite against real containers. The manual checks that headless Avalonia cannot make — the drag from Explorer, a generated key against a real host, twelve tabs at the minimum window width — are listed in docs/manual-checks.md and are still outstanding.
583 lines
27 KiB
Markdown
583 lines
27 KiB
Markdown
# 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:
|
|
|
|
- **`MainWindow` cannot be laid out by a test.** WebView2's adapter refuses the headless dispatcher's MTA
|
|
thread — see `LayoutHarnessTests.WhyTheWindowItselfIsNeverShown`. Anything that has to be measured lives
|
|
on a `UserControl` instead, 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 widen the window to its minimum
|
|
(880px), then 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's `User` should 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.
|
|
- `Match` blocks should be counted in the warnings and their settings should not have leaked onto any host.
|
|
- A `ProxyCommand` should 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:
|
|
|
|
```bash
|
|
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.
|