Test Explorer hardening, and the debug-adapter defects its new suites found - #242
Merged
Conversation
The previous commit landed the release workflow and Makefile changes but not the scripts they call: `tools/dist/` was swallowed whole by the bare `dist/` pattern in .gitignore, which matches a directory of that name at ANY depth, not just the build output at the repo root. CI would have run `make _package-archive` against a missing `tools/dist/archive.sh`. Renamed to `tools/packaging/` rather than negating the ignore rule. These are source, not build output, and nothing named `dist` in the tree can be trusted to survive; the new name also says what the scripts do. Also removes src/editors/vscode/test-fixtures/workspace/.editorconfig, which promoted CS0219 to an error across the shared TestFixtures project. Refactor.cs carries an unused local on purpose — it is the fixture the CS0219 quick-fix test refactors — so the fixture build failed, and with it every VS Code chunk at pretest plus the five sidecar tests that build the same workspace. Nothing referenced the file: it set severities no test asserts on, and the quick-fix test gets CS0219 from the compiler regardless. Roslyn honours editorconfig severity over the csproj's TreatWarningsAsErrors=false, so a fixture that needs one must carry it in an isolated nested scope, never at the workspace root.
…ures
Test Explorer work plus the lint and format fixes CI enforces:
- `.split('\n')` in test-explorer-fixtures.ts was written with a literal newline
instead of the escape, so the suite could not compile (TS1002) and every VS
Code chunk died at pretest.
- Two `as vscode.TestItem` casts in test-explorer-adapter-ids.test.ts tripped
@typescript-eslint/non-nullable-type-assertion-style; applied eslint --fix.
- Reformatted four files prettier reported as unformatted.
CLAUDE.md: no SharpLsp code is "legacy" — code that does not match the specs
gets deleted, not preserved.
Prettier gate in ci-build.yml rejected it.
Running a class group left every theory reporting "No result reported". The two halves of a test id disagreed. `parseFullyQualifiedTestList` strips an adapter's appended unique ID when it builds the tree, so the id is the bare `Ns.Class.Method`. `toTestResult` built `fullyQualifiedName` straight from `TestMethod/@className` + `@name`, and xunit.runner.visualstudio 2.2.0 stamps that attribute with the unique ID — so the report keyed on a name no tree item carries and no outcome could be attributed back (issue #232). A theory made it worse: each row carries a DIFFERENT unique ID, so the rows never collapsed onto the single id they share — which is exactly what `worse()` and OUTCOME_SEVERITY in test-execution.ts already assume when they judge a data-driven test by its worst row. Stripped with the same rule at the one boundary where a TRX name becomes an id. `displayName` keeps the decoration: it is a label, not a key. NUnit's `Adds_Case(2,2,4)` still round-trips untouched — no space before the paren, and its contents are not hex.
SHARPLSP_DAP_TRACE showed only what arrived FROM the adapter, so a response the router synthesises or re-sequences itself — the attach retrier's, for one — was invisible, and an unanswered client request could not be told from an answered one. Same flag, same shape, on the way out.
Pressing Debug on a test reported the attach settled the moment `startDebugging` resolved, which is only "the session exists" — several DAP round trips before it can stop anywhere. The gesture handed control back while the debugger was still coming up, so the whole `debug-tests` chunk failed: no `configurationDone` in the handshake, no breakpoint bound, no stop, and every later test timed out behind the wedged run. The router now settles on ARMED rather than on "configuration was requested": netcoredbg has ANSWERED `configurationDone` — it answers ~80ms later and finishes the attach as it does — and every breakpoint it accepted has bound. A VSTEST host attached under `VSTEST_HOST_DEBUG` has not loaded the test assembly yet, so every breakpoint in the user's own test starts out `verified: false` and binds later by a `breakpoint` event ([DEBUG-FEATURES-BREAKPOINTS-VERIFY]); reporting "attached" before that is issue #233's Debug press that ends in silence. Also fixes Run/Debug Test at the cursor, which invoked the workbench commands `testing.runTests`/`testing.debugTests`. Neither exists, so the lens gesture died with "command not found" and ran nothing; it now presses the extension's own registered profile, the same entry point the Testing view uses. The multi-select expectation asserted breakpoints come back in the order they were armed. VS Code's debug model sorts them by uri then line (`sortAndDeDup`) before sending, and DAP requires the response array to correspond to the request array, so the adapter answers ascending — the expected array is corrected to the order the workbench provably sends, with all three breakpoints still required to bind. Local `make _run-vsix-suite CHUNK=debug-tests`: 22 passing, 0 failing (was 0 passing, 9 failing).
`statSync(path).size === 0` followed by `readFileSync(path)` is a check the read cannot rely on: the file may change between the two (CodeQL js/file-system-race, high). Read once and judge the bytes in hand — the emptiness check is then about the same bytes that get hashed, and a release archive is no longer walked twice.
…test `writeDebugTestFixture` only WROTE the project and solution, so the restore and compile were paid by whichever test ran first, inside its 50s `DEBUG_TEST_MS` budget. C# fits in that; F# — FSharp.Core plus a cold compiler start in a fresh scratch directory — does not. On Ubuntu the first F# test timed out mid-build and every later test in the run, F# and group alike, timed out queued behind the invocation still building: 12 passing, 10 failing, all ten exactly 50.0s apart with no work in between. The fixture is now built where the cost belongs, in `suiteSetup`, which already owns `FIXTURE_BUILD_MS`.
CI's extension log names the wedge exactly. The last C# debug test armed its session at 08:28:58.075 and passed at 08:28:59.0; its `dotnet test` was still running, because a Debug gesture resolves at the ATTACH and the invocation continues until the debugged tests finish. `suiteTeardown` then deleted the fixture directory at 08:29:00 — a fraction of a second before that invocation would have written its TRX and exited. `dotnet test` was left pointed at a directory that no longer existed and never exited, and because every invocation the controller makes is serialised behind one queue, it took the rest of the run with it: all four F# tests and all six group tests timed out at exactly 50.0s intervals with no work in between, 12 passing / 10 failing. The teardown now waits for that queue to drain before removing the tree, so it is ordered rather than lucky.
Prettier fits it on one line now that removeDirRecursive is gone.
…lorer suite Multitarget 6 to 12, adapter-ids 11 to 16, cancellation 11 to 15, coverage 9 to 12, lens-status 4 to 8, debug groups 6 to 9, F# debug 4 to 7. All end-to-end through the real extension host against real dotnet-built solutions, every assertion derived from TEST-EXPLORER-SPEC. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
vscode.executeCodeLensProvider resolves the URI against the text models the editor already holds - unlike most execute*Provider commands it creates no model reference of its own and throws a bare 'Illegal argument' for a file that is only on disk. testing-lens-status.test.ts passes fixture URIs it never opened, so its suiteSetup died in warmCodeLensPath with an error naming neither the file nor the reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
extractCSharpMethodName rejected every line starting with '[' - the guard that stops a bare [InlineData(2, 2, 4)] reading as a method called InlineData. '[Fact] public void Adds()' is idiomatic C# and the shape most one-line xUnit tests take, and it starts with '[' too, so those methods got NO lens at all: no status, no Run action, no Debug action. Leading attribute groups are now stripped instead, which leaves the bare attribute line rejected (nothing remains of it) and lets the combined form through. Brackets are counted rather than searched for, and a ']' inside a string argument is not treated as one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… lens status
Two lookups the status lens got wrong. extractFSharpFunctionName matched /^let\s+(\w+)/, and \w cannot match the backtick that opens 'let ``adds two numbers`` () =' - the way F# names a test so it reads as a sentence, and the shape every F# fixture here uses. Those bindings resolved to nothing and carried no lens at all: no status, no Run, no Debug. Both let and member now accept the double-backtick form and capture the INNER text, which is what the test id carries.
findResultByMethodName then split a cached id at its LAST dot, so a data-driven row - Ns.Class.Adds(a: 2, b: 2) - never matched the method it belongs to, and a [Theory] showed no status until a run replaced those ids with the merged bare name. The id is now cut at the first '(' before the last dot is taken, which also stops an argument carrying a dot from reading as the method name.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
[TEST-STATUS-LENS] pins $(circle-slash) Not run as one of the four titles the status lens renders, and statusLensTitle implements it - but nothing writes to the result cache until a run FINISHES, so no lookup could ever return it. A freshly discovered test carried Run and Debug and no status line at all, and the row only started reporting itself after the user had already run it, which is exactly when they no longer needed telling. No cached result IS the not-run result. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… claim 4 reportDirsOf counted EVERY directory under .sharplsp-coverage, but dotnet test points the TRX logger and the coverage collector at the same --results-directory, and the logger creates its own attachments folder there as soon as a run produces an attachment - which a coverage run always does. That third directory read as a third project's report. [TEST-COVERAGE] says 'one Cobertura report per test project, each in its own RUN-ID FOLDER one level down', so the helper now filters on the report, which is also the stronger claim: it counts reports findCoberturaFiles can actually load rather than folders that merely exist. The layout is now asserted in full - TRX files, run-id folders, and the attachments folder named for its TRX, which must hold no report one level down. The Debug profile's isDefault assertion could not hold: isDefault is scoped to a KIND, and VS Code writes it back to true on the only profile of a kind so that kind's button has something to press. Asserting false on Debug asserted that the Debug button does nothing. What the play button actually obeys - the Run kind's default - is pinned instead, along with the three kinds being three distinct profiles. Claim 4 of the suite's own header, that coverlet.collector omits the TEST assembly (IncludeTestAssembly is false), had no test. It has one now: every file named across both reports is the library's, neither test source appears, and the library's lines really were measured so the exclusion is not just an empty report. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parseCoberturaXml stashes per-line detail in a module map keyed by FILE URI, and addCoverage parsed every report in a loop and attached each entry. Two test projects covering one library therefore produced two entries for the same file, and the LAST report parsed overwrote the stash for both — so when VS Code resolved detail on expand, it got one project's lines for every entry. A function the other project had just executed came back uncovered: a wrong RED gutter, not merely a missing one, and exactly the loss [TEST-COVERAGE] warns about when it says taking only the first report drops every other project's coverage. addCoverage now merges per file: one FileCoverage per source file, its detail the union across reports, hits taken per line as the maximum because a line one project never executed is not evidence another did not. The suite proved this the way the product does it — parse ALL reports first, resolve detail afterwards — which is the order every other test avoided and the reason the defect survived. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…builds Two suites asserted a leaf's parent is the F# module by its full dotted path, on the premise that 'an F# module renders as Assembly -> Namespace -> Test: there is no class'. The tree documents and implements the opposite, naming this exact case: 'deterministic for C# namespaces and dotted F# modules alike (Fs.Xunit.Fixtures.adds two numbers -> Fs.Xunit / Fixtures / adds two numbers)'. That is also what the CLR does - an F# module compiles to a type, so Fs.Debug.Fixtures IS the type Fixtures in namespace Fs.Debug - and it matches the Assembly -> Namespace -> Class -> Test hierarchy the spec and test-tree.ts both describe. The assertions now pin the class row by type name, the namespace row above it, and that the two rejoin to the module the fixture declares. One correction, two chunks: this was the only debug-tests failure and one of six in testexplorer-cancellation. A cancelled dotnet tree on Windows also reports itself now. taskkill was spawned, unref'd and its exit code and stderr discarded, while the POSIX branch reports every signal it fails to deliver - so a kill that never happened looked exactly like one that worked, which is why a surviving testhost writing results for a stopped run left nothing in the log to explain it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Detail is stashed per file URI, so libraryLinesIn - which parses ONE report to ask what it alone covered - replaces the merged detail with that report's. The merged summary and total were asserted after that loop, comparing a single report's detail against the merged count, which holds only while both reports instrument an identical line set. It passes on Windows and is a latent order dependency on a module-global stash either way, so the merged numbers are now read before any re-parse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rtions Every one of the eight suites the density table called out now carries at least twenty assertions per test, all derived from the specification rather than from the implementation: test-explorer-adapter-ids 16 / 327 test-explorer-cancellation 15 / 310 test-explorer-coverage 14 / 292 test-explorer-multitarget 12 / 242 testing-lens-status 10 / 206 test-explorer-names 5 / 105 debug-test-groups-e2e 9 / 188 debug-test-fsharp-e2e 7 / 142 The new claims are closing interactions on the EXISTING tests, not new tests: the stripper is total, idempotent and prefix-preserving and sheds exactly a space plus forty hex digits ([TEST-DISCOVERY-FQN]); a decorated id builds a DIFFERENT filter than the bare one, which is the defect stated directly ([TEST-FILTER-ESCAPE]); a group id is never a test id and every row is a group or a leaf ([TEST-EXPLORER]); coverage reports sit exactly one directory down, the merge is the union of the per-report details, and merging every report covers strictly more than the first alone ([TEST-COVERAGE] claims 1-4); a cancelled run suppresses rather than fails, drains the queue and leaves discovery and the results directory clean ([TEST-RUN-TRX], [TEST-REACTIVITY]). Also formats the debug e2e files thickened in the previous rounds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ette VS Code passes NO argument when a view command is run from the palette or through `executeCommand` — [SE-CONTEXT-VALUES] makes the node the context menu's contribution, never a guarantee. Seven handlers declared the parameter as `ExplorerNode` and read through it immediately, so every one of them threw `Cannot read properties of undefined` the moment a user found it in the palette; the type was a promise the caller could not keep. Each signature now admits the absence. The three project-file commands share one `projectPathOf` resolver rather than repeating the same guard, which is also what keeps the warning single instead of a cascade, and the four symbol commands say what to do instead of failing silently. `sharplsp.nuget.addFromExplorer` also wore `%cmd.nuget.add%`, so the palette listed "SharpLsp: Add NuGet Package" twice with nothing to tell the two apart — and they differ: one asks which project, the other uses the row the user right-clicked. It now has its own title in all three localisations. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…he session `FSharpCodeLens.fs` did not compile. `FSharpSymbolUse.Range` returns the `range` STRUCT, so reading a member straight off the property call makes the compiler copy defensively and raise FS0052; warnings are errors there, so `dotnet publish` failed and PHASE 2 produced no F# sidecar for anything downstream to run against. The anchor range is now bound once and read from the local. The second fix is the reason the whole `lsp` chunk collapses after the SIGKILL recovery test. `ErrorAction.Shutdown` is TERMINAL in vscode-languageclient: `handleConnectionError` calls `stop()`, the client's state becomes `Stopped`, and `handleConnectionClosed` then returns early forever — so `closed()`, the only thing that ever answers `CloseAction.Restart`, is never reached. Escalating at four errors therefore SPENT the restart budget without ever using it: once the server died, the burst of `ERR_STREAM_DESTROYED` writes on the dead transport tripped the counter and the language client stopped for the life of the window. Every later suite saw `State.Stopped`, and a user's only way back was reloading VS Code. So the handler escalates only once the restart budget is genuinely spent, and lets the transport's own close drive recovery until then. The budget also stops being a lifetime allowance: five crashes three minutes apart are five unrelated faults, not a crash loop, and used to exhaust it as surely as five in a second. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t a session `sharplsp.profiler.revealOutput` and `copyOutputPath` read `item?.outputPath` and could not tell a missing ROW from a row whose trace file is missing. Run from the Command Palette, where VS Code passes no argument, they answered "Session has no output file yet." about a session the user never selected — a notice about state that does not exist. Both now share one `outputPathOf` resolver: an absent row is a silent no-op, and only a real row whose trace has yet to be written earns the message. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`AdapterWire.write` had two ways to fail and treated them oppositely. When `stdin.write` THREW, the adapter's death went through `onGone`, which settles every pending request, tells the user on the debug console and ends the session. When `stdin` was ALREADY destroyed, the very same death returned early and said nothing at all — even though the comment beside it says both failure modes mean the same thing. The frame VS Code is most likely to send into that window is `disconnect`. The router forwards it and answers nothing itself, so a dropped one leaves the workbench waiting on a response no live process exists to send: no `disconnect` response, no `terminated`, and a session stuck in the debug toolbar that the user cannot close without reloading the window. That is what stopping one of two concurrently paused sessions looked like. `onGone` is already guarded to fire once, so a child that exited cleanly and reported itself passes through the new branch as a no-op. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rtup `resolveByName` took ONE look at the process table and refused outright when nothing matched. A .NET process the user has just started is not in that table instantly — `dotnet App.dll` has to bring its runtime up before the assembly name appears in a command line at all — so "start the app, then attach", which is the entire reason to attach by name rather than by pid, answered "No running .NET process named 'X' was found to attach to." The attach REQUEST already retries on a ladder ([DEBUG-FEATURES-LAUNCH] attach rows); the name RESOLUTION that runs before it did not, so the retry never got its chance. Resolution now polls the same brief window. Ambiguity still answers at once — two matches is an ANSWER, not a not-yet, and waiting can only make it more ambiguous. The one-line `sleep` was private to dap-stack.ts. Rather than write a second copy it moves to utils.ts as `delay`, which both callers now take it from. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…it came from Roslyn groups related variants under a CONTAINER action whose title carries the meaning: `Introduce parameter for 'seed * 2'` holds `and update call sites directly`, `into extracted method to invoke at call sites` and `into new overload`. Visual Studio renders that as a submenu. LSP has no submenus, so the resolver flattens the tree — but it kept the children and DISCARDED the parent, so Ctrl-. offered three fragments beginning with "and" and "into" that say nothing about what they do or to what. Worse, the titles were also the deduplication key. Roslyn offers the same three variants a second time under `Introduce parameter for all occurrences of 'seed * 2'`, and with the parent dropped those collided with the first three and were discarded: half of Roslyn's variants for this refactoring were unreachable from the lightbulb entirely. Each flattened child now carries its ancestry. Measured against the real sidecar on the `RefactorCore` fixture, a caret on `seed * 2` went from three orphan fragments to all six variants, each a sentence about the edit it makes. A child whose title already starts with its parent's is left alone rather than made to stutter. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ng the session 40a6b87 reported an unwritable stdin through `onGone`, which ends the whole session, and that is too blunt: netcoredbg closes its stdin during a normal teardown while the process is still alive, and VS Code keeps polling `threads` through that window. A routine shutdown therefore started announcing itself as a death, and the `debug` suite lost `run-debug-commands` to a ten-second wait. The defect underneath is narrower and stays fixed. A REQUEST that cannot reach the adapter got no response at all, so the workbench waited on it forever — and the frame it is most likely to send into that window is `disconnect`, which the router forwards and answers nothing itself, leaving a session in the debug toolbar that the user cannot close. Such a frame is now answered locally. `disconnect` succeeds, because an adapter that is gone IS the disconnected state; anything else fails, which is what a request to a dead process deserves and what lets VS Code surface it rather than hang. Events and responses still drop silently — nothing is waiting on them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… is declared LSP 3.17 defines `fromRanges` as "the ranges at which the calls appear ... relative to the caller denoted by `this.from`". The host filled it with the caller's own declaration range instead, so every navigation from the call hierarchy landed on the calling method's NAME rather than on the call — and the shape had nowhere to put a second site, so a caller that calls the callee twice could only ever be reported once. Both engines knew the answer and threw it away. Roslyn hands back `SymbolCallerInfo.Locations` for incoming and the invocation node itself for outgoing; FCS has the range of every symbol use. The F# side went further and deduplicated CALLERS, so `quadruple`, which calls `double` twice, arrived as one entry carrying no sites at all. The sidecars now send the sites alongside the item, over a call-specific wire record — `prepare` and type hierarchy still answer with a bare item, and the MessagePack encoding is positional, so they could not share one shape. A call that reports no site still lists once at the declaration, so an engine that cannot supply ranges degrades to the old behaviour rather than dropping out of the tree. C# outgoing calls also merge: two invocations of one method were two identical rows that expand to identical children, where LSP wants one row with two ranges. `incomingCalls` / `outgoingCalls` keep their item-returning signatures and become one-line projections of the site-carrying versions, so there is still exactly one implementation of each walk. Measured on the `fsharp` fixture: `double` now reports `quadruple` with two sites, both covering the identifier, and `answer` with one. 30 Rust call/type-hierarchy tests and 67 sidecar hierarchy tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… clicking in it Roslyn finds a refactoring's target with `TryGetRelevantNode`, which needs the span to sit inside ONE node. A selection over an invocation's method name lands inside the identifier, not inside the invocation — so, measured against Roslyn 5.3 on the `InlineMethodTarget` fixture, `Inline 'Doubled(int value)'` is offered for a caret on `Doubled`, and for a selection over `Doubled(seed)`, and withheld for a selection over `Doubled` alone. Double-clicking a word before pressing Ctrl-. is the most ordinary gesture there is, and it took refactorings away. Visual Studio asks Roslyn about the selection AND the caret; so does this now. The selection goes first, so where both answer, the user's own selection is what survives the existing duplicate check. A caret costs nothing extra — there is no second span to ask about. Measured on the same span the suite uses: the lightbulb gains `Inline 'Doubled(int value)'`, its "Inline and keep" variant, and the two Wrapping refactorings, none of which a selection could reach before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Windows `lsp` chunk failed eighteen tests from one cause: the manual Restart. `LanguageClient.restart()` is `stop()` then `start()`, and the library's `stop()` allows `shutdown` two seconds before it throws WITHOUT starting anything — so a server busy with a 2.7s F# `workspace/diagnostics` answered too late, the restart was abandoned, and every later suite in the chunk timed out against a client left Stopped. A hung server is exactly when a user reaches for Restart, so `client.restart` now stops with a real budget and starts a fresh server whether or not the old one bowed out in time. The host no longer makes them wait either: `shutdown` is answered on a fast-path thread ahead of the dispatch loop, which LSP 3.17 permits — the answer need not wait for work already in flight. DISTRIBUTION-SPEC rule 6 and the tier-1 bullet in SHARPLSP-SPEC now state both contracts. The remaining changes correct assertions that asserted the wrong thing: - manifest: resolve `%key%` through package.nls.json the way VS Code does, exclude the `<view>.focus`/`.open` commands the HOST registers for every contributed view, and accept a fixture setting pinned to its own default. - symbols: VS Code serialises `DocumentSymbol` without `children`, so every `JSON.stringify(symbols).includes(name)` check walked a tree it could not see. They flatten it now. - hover: line 5 of HoverReject.cs is the `namespace` keyword, not blank. - inlay hints: `var total` earns a Type hint on the call line; the parameter assertions filter to parameter-shaped hints and the Type hint is asserted as the only other kind allowed there. - selection ranges: VS Code merges its own word-part provider into the chain, so the identifier is a level of the chain rather than the innermost one. - refactoring: Encapsulate field resolves as `refactor.rewrite`, which is what the resolver assigns and what the kind's own doc lists. - coverage: run-id folder names are joined through the results directory, and an assembly-root run reports every project because a run is ONE `dotnet test` for the whole selection ([TEST-RUN-TRX]) — one report carries executed lines, the rest are empty. Plain and Debug runs are asserted to leave the directory as the Coverage run left it, not to empty it. - lens status: the C# project declares its tests in TWO files, so lenses are gathered from both. - cancellation: three blocks asserted that unselected long tests had run. They assert the opposite now, against a pre-run baseline. - test debugging ATTACHES to the waiting test host (DEBUGGING-SPEC:664), so the group suite asserts `attach`, never `launch`. - call stack: a console app's managed stack bottoms out at `Main`, so "distinguishable from runtime frames" is the walk reaching Main with no user frame marked subtle. - exceptions: the filter suite reads the recorded request and translates it with the shipped `filterOptionsFrom`, instead of inspecting the local object it sent. An unhandled throw breaks whatever the filters say — there is nothing after it to continue to — and [DEBUG-FEATURES-EXCEPTIONS] now says so. - F# quick fixes: the FCS cold start is paid once in `suiteSetup`, not charged to the first scenario's ceiling. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
e5e7111 prefixed EVERY flattened child with its container, which renamed actions that already named themselves. `Inline and keep 'Double(int value)'` became `Inline 'Double(int value)': Inline and keep 'Double(int value)'`, and `Introduce local constant for '1 + 2'`, `Convert to binary` and `Wrap expression` all stuttered the same way. Those titles are what users read and what callers address, and it cost nine passing assertions in the `lsp` chunk — my regression, and the reason that chunk went from 10 failures to 19. Roslyn writes nested children in two shapes and the difference shows in the first character. `Inline and keep '...'`, `Introduce local constant for '...'`, `Convert to binary` are sentences. `and update call sites directly`, `into extracted method to invoke at call sites`, `into new overload` are continuations of the PARENT's sentence and say nothing alone — which is the orphan-fragment menu the original commit set out to fix. Only continuations are joined now, and with a space, so the result reads as the one sentence Roslyn wrote: `Introduce parameter for '1 + 2' and update call sites directly`. That still keeps the "for all occurrences" group reachable. Its three children are continuations too, so they take their own parent's prefix and no longer collide with the first group's under the duplicate check — which is how half of Roslyn's variants had been disappearing before e5e7111. Measured on the real sidecar: the self-contained titles are back verbatim, both introduce-parameter groups are present, and nothing stutters. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…othing 9a15912 asked every provider about both the selection and the collapsed caret unconditionally. That fixed the real gap — a selection over an invocation's method name could not reach `Inline 'Doubled(int value)'` — but it also pulled in actions for whatever sub-expression the caret happens to land inside. Selecting `1 + 2` began offering `Introduce constant for '1'`, `Introduce local constant for '1'` and six `Introduce parameter for '1'` variants beside the ones for `'1 + 2'`. The user selected an expression; the menu should be about that expression. The caret is now a per-provider FALLBACK, asked only where the provider had nothing to say about the selection. Inline method still appears, because that provider genuinely answers nothing for a selection over the identifier alone, and introduce-constant no longer widens, because it answered. Measured on the fixture: the constant case drops from 26 offered actions to 17, every one removed being about a sub-expression that was not selected, while `Inline 'Double(int value)'` and `Inline and keep 'Double(int value)'` both remain for a selection over `Double(3)`. Warm Ctrl-. holds at ~20 ms, and every provider that answers the selection is now asked once rather than twice. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…udged The router emulates `hitCondition` and `logMessage` because netcoredbg ignores them ([DEBUG-ADAPTER-GAPS]), and `BreakpointEmulator` documents that it indexes armed lines by the line the adapter BOUND — "because that is the line a stop's top frame will report". It only ever learned that line from the `setBreakpoints` RESPONSE, and every breakpoint of a test-host attach is answered before the test assembly is loaded, so the response carries no line and the index keeps the line the user typed. The real bind arrives later as a `breakpoint` event ([DEBUG-FEATURES-BREAKPOINTS-VERIFY]) that nothing fed back, so a stop on the bound line missed the index, was judged unknown, and was forwarded — with the hit count the user typed silently ignored. A hit count of 2 on a two-row [Theory] stopped on row one. `rebind` re-keys the entry to the line the adapter announced and carries the visit count across with it; the router calls it from the `breakpoint` branch, beside `noteBreakpointBind` and in the same child id space `record` uses. Two assertions were asserting the wrong thing: - `assertBoundAtLines` compared bound lines in the order the CALLER armed them. DAP answers `setBreakpoints` in the order of the request, and the request is the workbench's own breakpoint list, which it keeps sorted by line — so an adapter returning [9, 16] for a test that armed 16 then 9 was right and the assertion was wrong. It compares the set now, which is the real claim: every armed line came back bound to itself and none drifted to a neighbour. - test-explorer-cancellation contradicted itself twice. Both tests run the FAST test alone, then demanded every long test's marker on disk — markers only a run of the long tests can write. The recovery test now requires the marker directory to be EMPTY after the fast-only run, which is the real proof the queue rebuilt the filter instead of replaying the cancelled selection, and a fifth interaction runs the whole fixture uncancelled so every original assertion — markers complete, long tests finished, outcomes real — is kept where it is true. The late-Stop test snapshots the marker directory as the finished run left it and requires it unchanged, which is what "changes nothing" means. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ck at the call
Two Windows-only reds. One test was asserted the slow way, one asserted
the wrong thing:
- hover.test.ts `resolveTreeItem uses LSP hover` walked every symbol node
of the whole solution — several hundred — and paid two sidecar round
trips per node one after another, plus a workbench open/close of the
model for every hover on a closed file. That is 36s on Linux and past
the 45s sweep budget on Windows, where a hover measures ~80ms rather
than ~25ms. Every per-symbol claim is kept; the walk now opens each file
once, as the user's own files are, and resolves the tooltips
concurrently. One tautology went: `includes(name) || includes('```')`
asserted straight after `includes('```')` proved nothing.
- debug-test-debugging-e2e `helper reached FROM the test` compared
`trace()` output — `Method@line` labels — with the bare method name, so
it could never hold. Linux never reached it because Step Over fails
first. It now requires the test frame on the stack AT the call it is
waiting on, which is the frame a user clicks to see why the helper ran.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…in as it was Two lsp failures and one workspace failure, all wrong assertions the code already contradicts: - lsp-refactor-spec-gaps generate-constructor polled for `GenerateConstructorTarget(int, string)`, a title Roslyn never writes: it names the PARAMETERS, each derived from the field it seeds — `_count` -> `count`, `_label` -> `label`. The action was in the list under `(int count, string label)` the whole time, so the poll timed out on a title that could not appear. New sidecar test GenerateConstructorFromMembersTests drives the real provider through WorkspaceManager and pins BOTH shapes: a two-field selection offers `Target(int count, string label)`, a caret on the type name offers the parameterless `Target()`. That is where the title came from. - lsp-refactor-spec-gaps inline requeried the ORIGINAL range after the declaration above the call was deleted, so the position translated off the end of the shifted document and the sidecar answered null, not an array. It now requeries the line the call moved to and asserts the action is GONE, an inlined call being nothing left to inline. - tree-config-e2e demanded the fixture's `logging.level` restore to `info`, but the committed fixture pins NOTHING at workspace scope now (a pin there hides every user-scope write behind it), so the value to restore is "removed", not "info". It asserts the override lands at workspace scope, then that the key is unset again and the getter reads the manifest default. - extension-manifest-kit tolerated a workspace pin equal to the default; the fixture pins nothing, so the true claim is the strict one: unset at workspace scope, full stop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every stop in a TEST-HOST attach cost the client a `stackTrace` it waited
149ms for, against 2ms from netcoredbg. The trace says where it went:
40.375 stopped {threadId: 23024, allThreadsStopped: true}
40.382 stackTrace {threadId: 23024, levels: 20}
40.384 netcoredbg answers it
40.519 next {threadId: 8404} <- a ".NET Long Running Task" thread
40.531 the router finally emits the stackTrace response
40.531 next -> "Failed command 'next' : 0x80004005"
netcoredbg is right to refuse: 8404 never stopped. VS Code sent it because
`workbench.action.debug.stepOver` steps `viewModel.focusedThread`, and the
workbench focuses the stopped thread only once `fetchCallStack()` resolves;
before it does, the command falls back to the first thread in the list.
Two things were spending that time, and neither could produce an answer:
- `recoverChain` evaluated `Task.s_currentActiveTasks` on every stop. That
registry only exists once `s_asyncDebuggingEnabled` is set, and only a
LAUNCH gets the entry stop that sets it -- an attach never arms it, so the
walk could only ever read `null`, at a measured 113ms per stop.
`StackDelivery` now remembers whether arming succeeded and skips the walk
when it did not. Launch sessions are unchanged: both async-stack suites
(`debug-callstack-e2e`, `debug-fsharp-inspection-e2e`) drive `startDebuggee`,
which launches, so they still arm and still walk.
- `asyncThreadStacks` fetched up to sixteen other threads' FULL stacks one
after another. A test host parks a dozen runtime and thread-pool threads;
that was 147ms of round trips for a stitch candidate set that is discarded
unless exactly one thread qualifies. The probes are issued together now --
same threads, same data, same order, 7ms.
The client's `stackTrace` now lands in 20ms and the step reaches the thread
that actually stopped.
Also: `[dap=>]` logged only headers, so the trace answered "what came back"
but not "what did we send" -- which is the whole reason the router keeps one.
It carries the outbound body now, under the same payload budget as the other
two directions. That is what made the above diagnosable at all.
Spec: [DEBUG-ARCHITECTURE-ROUTER], [DEBUG-FEATURES-STEPPING].
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
[DIST-CI-VSIX-SHARDS-TIMEOUTS] owns the tier table, states the numbers are "derived from measured behaviour on the CI agents", and requires evidence to change one. `main` matches that table exactly. This branch does not: fe4b146 cut every tier, roughly in half, and never amended the spec. FAST_MS 1s->500ms COMMAND_MS 5s->1s SETTINGS_WRITE_MS 30s->12s LSP_RESPONSE_MS 15s->10s DEBUG_SESSION_MS 45s->20s DEBUG_TEST_MS 50s->25s PROCESS_START_MS 30s->15s DOTNET_CLI_MS 120s->60s LSP_SWEEP_MS 60s->45s SERVER_RESTART_MS 120s->60s ACTIVATION_MS 60s->20s SIDECAR_COLD_MS 90s->45s FIXTURE_BUILD_MS 240s->180s REAL_REPO_MS 600s->480s REAL_REPO_WARMUP_MS 480s->360s WHOLE_RUN_MS 20min->15min Measured on a Windows `debug-tests` shard, MOCHA_FILES over the multisession, groups and debugging suites: eleven failures at the cut ceilings, four at the published ones. The seven that came back are not slow tests -- they finish in 10-12s. `debugging the NAMESPACE row leaves the OTHER namespace alone` 11966ms, `debugging the CLASS row breaks in every test the class contains` 10047ms, `an F# debug run leaves the tree and the spaced ids exactly as they were` 11506ms, `a MULTI-SELECT of two classes debugs both, and nothing else` 12441ms. That last one is why the Windows leg also reported `ONE selection is ONE session; started 2`. Mocha's timeout does not cancel the test body: the timed-out test's `debugRun` was still in flight when teardown ran and the next test's `setup()` installed a fresh `DebugSessionRecorder`, so the leaked `startDebugging` resolved into the new recorder. A cascade, not a second bug. Two comments went back with the values they were rewritten to justify (COMMAND_MS's "one second, and that is the whole budget", SETTINGS_WRITE_MS's). Everything fe4b146 added that is still true is kept -- the "ONE initialization per suite" preamble, and SETTLE_MS, which is new on this branch and is now in the spec's table too so the two agree in both directions. No assertion is touched, no test is skipped, nothing is suppressed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`DebugSessionRecorder.liveSessions` was append-only. `onDidStartDebugSession` pushed to it; `onDidTerminateDebugSession` recorded the id in `terminatedIds` and left `liveSessions` alone. So `liveOurs` -- documented as "Live session objects of the SharpLsp debug type" -- meant every session ever started. `debug-multisession-e2e.test.ts:309` stops the FIRST of two sessions and polls `liveOurs.map(l => l.id)` until it no longer contains `first.id`. That could never hold, so the test spent DEBUG_SESSION_MS and failed with both ids as its last observed value -- which is exactly what CI reported. It also made the assertion after it vacuous: `liveOurs.some(l => l.id === second.id)`, "ending the first session must not take the second down with it", was true of an array nothing is ever removed from. Both are real claims now. The recorder drops a session when the workbench says it terminated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e refactor The generate-constructor row now finds its action — the title was corrected last commit — but asserted `kind: 'refactor'`, and the sidecar classifies a code-GENERATING refactoring as `refactor.rewrite`: it is not inline, not extraction, not organize-imports, so RefactoringKind returns its default, the same kind every other rewrite family in this file already carries. LSP 3.17 has no `refactor.generate`, and the spec names no kind for it, so the sidecar's classification is the contract and the assertion was wrong. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`SharpLspTestController.enqueue` serialises every `dotnet` invocation, and its
own comment says why: discovery BUILDS the solution and a run rebuilds the same
projects, so two overlapping invocations race on the shared `bin/`/`obj/` and
VSTest dies with "The application to execute does not exist: testhost.dll".
A debug run was on that queue too. Under `VSTEST_HOST_DEBUG` its `dotnet test`
does not exit until the user has finished debugging, so the queue was held for
as long as a breakpoint was held -- and for that whole time the Test Explorer
could not discover anything and no other run could start. Pressing Refresh in
the Testing view while paused simply hung.
Measured, from a Windows trace of `debug-test-fsharp-e2e`:
13:19:46.077 Loading solution into state: DebugTestTargetFsSln.slnx
13:19:46.914 Symbols loaded
... 39 seconds of nothing ...
13:20:26.055 Test debug: the run ended with 1 result(s)
13:20:28.738 Test discovery: 1 item(s) from 1 target(s)
The sweep did not fail and was not slow. It waited for the debuggee, then took
2.7 seconds.
The run now holds the queue for the BUILD and releases it once a host is
waiting and its attach has settled -- strictly after the race the queue exists
to prevent, since a host that is waiting has already been built. A run that
dies before any host waits releases it the same way, through the same race.
Two tests come back with it, both of which read the tree after their last
breakpoint stop and had been waiting on a debuggee that was never going to
exit: `an F# [<Theory>] breaks once per row, each with its own arguments`
(22.7s) and `debugging the ASSEMBLY root debugs every namespace under it, in
one session` (14.6s). Both need the restored DEBUG_TEST_MS as well; neither
fits the ceiling this branch had cut it to.
Spec: [TEST-RUN-TRX], [DEBUG-FEATURES-TESTS].
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s rows in `hitCondition: '2'` promises one thing: the debuggee stops on the SECOND HIT and not on the first. The test also required that the second hit be the second `[InlineData]` in DECLARATION order, which xUnit does not guarantee. `DefaultTestCaseOrderer` sorts a class's cases by a hash of the test case's unique id, so the order is stable per method and arbitrary between methods. This very fixture proves it. The C# `Adds_Rows` and the F# `adds rows` declare the same two rows in the same order, run under the same runner, in the same push -- and the C# theory executes (10, 20, 30) first while the F# one executes (1, 2, 3) first. The C# `[Theory] stops ONCE PER ROW` test beside this one already knows: it sorts before comparing. From the wire, the emulation is exactly right: 31.367 stopped "breakpoint" first execution 31.372 [router] continue ok=true swallowed, count 1 of 2 31.382 stopped "breakpoint" second execution 31.386 [=>] stopped ... hitBreakpointIds:[1000001] the ONE stop the user sees Nothing about the product changes here, and nothing is weakened. "Only one stop happened" is what proves a hit was skipped, and the test already asserts it at the end. What replaces the declaration-order assertion claims MORE than it did: the three locals must form ONE COHERENT ROW of the theory, so a frame answering `left` from one row and `expected` from the other -- a debugger showing a state that never existed -- now fails where it used to pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Linux still lost the step. `VS Code / debug-tests` on 9b54124: 52 passing, and `the 'workbench.action.debug.stepOver' gesture must not reject: Failed command 'next' : 0x80004005` — while the SAME job on Windows passed 53 of 54. The earlier fix took the client's `stackTrace` from 149ms to 20ms, which is enough to win that race on one platform and not the other. 20ms of nothing is still 20ms. The workbench focuses the stopped thread only once `fetchCallStack()` resolves. Until it does there is no focused thread, and `workbench.action.debug.stepOver` falls back to `getAllThreads()[0]` — a thread that never stopped, which netcoredbg then refuses. So the response has to be immediate, not merely fast. It can be. Without the async-task registry — which only a LAUNCH arms, so never in a test-host attach — `recoverChain` has nothing to read, and a tail CONTINUES a chain, so with no chain recovered there is nothing to continue either. The rebuild's whole output is the frames the adapter already returned, bought with a full re-fetch and a queue hop. An unarmed session now answers from those frames directly, in the same tick netcoredbg answered in. Two changes, both statements about what the reconstruction can produce rather than about speed: - `deliver` rebuilds only when the registry is armed AND the response carries state-machine frames. Unarmed, the enriched frames ARE the answer. - `assemble` stitches a tail only onto a chain that was actually recovered and cut. Splicing another thread's frames onto a stack with no chain would invent a caller the debuggee never had — wrong regardless of what it costs. Launch sessions are untouched: both async-stack suites (`debug-callstack-e2e`, `debug-fsharp-inspection-e2e`) drive `startDebuggee`, which launches, arms at the entry stop, and still walks the heap exactly as before. Spec: [DEBUG-ARCHITECTURE-ROUTER], [DEBUG-FEATURES-STEPPING]. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
0b27c29 gated `stitchedTail` on a chain that was actually recovered, reasoning that a tail CONTINUES a chain so an empty one has nothing to continue. That is wrong, and `stitchedTail`'s own documentation says why: "a stop can freeze the debuggee while an awaiter is mid-suspension: its box exists but its continuation is not yet hooked, and the awaiting methods are still PHYSICAL frames on the thread that is suspending them". An empty chain with a tail on another thread is not the degenerate case — it is the case the stitch was written for. It cost four green jobs. `debug-breakpoints` and `debug-inspection` went red on both platforms, and locally: an F# task {} chain reports the logical await stack [DEBUG-FEATURES-STACK-ASYNC] applies to F# `task {}` verbatim ... The awaiting frame must be injected. Frames: FsStepTarget.Program.leafTask() One frame, because the awaiting frame lives on the suspending thread and only the stitch goes and gets it. Reverted to the line that was green, verbatim. The other half of 0b27c29 stands and is untouched: an UNARMED session still answers `stackTrace` straight from the adapter's frames, which is what the Linux stepping race needs, and no armed session reaches that branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…opped
The last red in the pipeline, and latency was not the answer. Making the
client's `stackTrace` synchronous for an attach did not win the Linux race:
`VS Code / debug-tests` still reports
the 'workbench.action.debug.stepOver' gesture must not reject:
Failed command 'next' : 0x80004005
while the same job passes on Windows. The wire says why, and it is not timing
at heart. The stop is on thread 23024; VS Code sends `next {"threadId": 8404}`
-- a ".NET Long Running Task" that never stopped. `workbench.action.debug.
stepOver` steps `viewModel.focusedThread`, and the workbench focuses the
stopped thread only once `fetchCallStack()` has resolved; before that there is
no focused thread and the command falls back to `getAllThreads()[0]`. In a test
host that is a runtime or thread-pool thread. netcoredbg keeps ONE current
thread and refuses every other, so the user's F10 surfaces as a raw HRESULT.
Shortening the window made Windows win it. It cannot make the window zero: the
workbench's focus is its own asynchronous step, and any user quick enough on
F10 loses the same way against any adapter that only steps its current thread.
So the refusal is rescued instead. The router already remembers what the
adapter announced; a step that comes back refused with `0x80004005` or
`0x80131309` is re-issued against that thread and the retry answers the
client's original sequence number.
Rescued, never pre-empted. A step the adapter performs is forwarded untouched,
so a user who deliberately selected a different stopped thread is unaffected --
which a blanket retarget could not promise. E_FAIL means no step happened, so
re-issuing cannot double-step. It is the same shape as the `0x80070057` attach
retry that already lives next door.
Implements [DEBUG-ADAPTER-GAPS] for the [DEBUG-FEATURES-STEPPING] rows.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Test Explorer hardening: adapter-decorated ids, multi-targeted assembly roots, the Debug run profile, and a Test Explorer suite that can actually fail.
Product changes
fix(vscode): attribute outcomes for adapter-decorated test names.xunit.runner.visualstudio2.2.0 — still pinned by real projects, FluentValidation among them — reportsNs.Class.Method (d87517d9…), appending the test case's 40-hexUniqueIDafter a SPACE. Taken verbatim as an id that breaks four surfaces at once: the tree labels the test with a hex blob,--filter FullyQualifiedName=escapes the parentheses and matches nothing, the TRX report keys on the bareclassName.nameso every test errors "No result reported", and the Run/Debug lens resolves nothing. Newtest-names.tsstrips it, and only it: the NUnitAdds_Case(2,2,4)shape has no space before the(and no hex inside, and both conditions are what tell them apart ([TEST-DISCOVERY-FQN], Test Explorer: every run reports "No result reported" — discovered ids carry xUnit's unique-ID suffix #232).fix(vscode): arm a test-debug session before reporting it attached, and drain the controller before deleting a debug fixture.chore(vscode): trace the client-bound DAP messages too, so a failing session shows both directions.fix(release): restore packaging scripts; hash a release archive from the bytes actually read.Tests
Every spec section of
TEST-EXPLORER-SPEC.mdwas audited for whether its fixture could fail. Four could not:[TEST-COVERAGE]ran against ONE test project, so "one Cobertura report per test project" and "every one of them is parsed" were the same list, andreports.length >= 1passed forever. Newtest-coverage-fixtures.tsbuilds TWO test projects over one library, each exercising a different function of it, so a reader that keeps only the first report paints a just-executed function as dead code. Newtest-explorer-coverage.test.ts(12 tests) also covers the freshly-emptied results directory — a planted sentinel and a fake run-id folder must both vanish — two runs of different selections that must not bleed into one another, the Run and Debug profiles collecting nothing, and an empty-but-valid report when the run loaded no library code.#if NET8_0exists in only one assembly" — ran against two frameworks compiling IDENTICAL sources, so union and first-wins were indistinguishable. The fixture now compiles an#if-guarded test into each framework's assembly, lists each assembly separately to prove the exclusivity, and runs the merged root, the conditional class row, a cross-framework multi-select and one framework-exclusive test (4 → 12 tests).[TEST-STATUS-LENS]'s status half was asserted only as a pure function, which cannot show that any of the four titles reaches an editor. Newtesting-lens-status.test.ts(8 tests) drivesexecuteCodeLensProviderover a built, discovered and RUN solution: "Not run" first, then pass/fail/skip on their own methods, a reactive refresh with the editor open and the document never edited, the Run action pressed on the lens itself, a coverage run painting identical statuses, and the result surviving a close/reopen.test-explorer-names.test.tstables six decorated forms against thirteen near misses that must survive verbatim — 39 and 41 hex digits, no leading space, non-hex, empty and trailing brackets, and NUnit'sAdds_Case(2,2,4).Density across the rest:
test-explorer-cancellationtest-explorer-adapter-idstest-explorer-multitargetdebug-test-groups-e2edebug-test-fsharp-e2eCancellation now covers every gesture that starts a run: Stop on the play button, on Run with Coverage, on a namespace row, on the assembly root, on a multi-select, on a single test, a token already cancelled before the handler started (which must spawn nothing — asserted on disk after the sleep would have elapsed), a Stop that races the run, a late Stop, two cancellations back to back proving the single
dotnetqueue was released, a full recovery run afterwards, and a refresh that must still re-discover.F# stays first throughout: backtick names carrying SPACES are debugged, filtered, covered and lensed; the F# module row is a group; and the F# side of the split coverage fixture is the half that covers
Multiply.CI
Three new chunks —
testexplorer-coverage,testexplorer-lens,debug-tests— split out because each builds and runs real solutions. Registered intest-chunks.jsonthrough a JSON round-trip and in both spec tables;make _check-vsix-chunksreports 90 VS Code suites, 29 Ubuntu chunks, 26 Windows chunks, 0 excluded ([DIST-CI-WIN-VSIX], [DIST-CI-VSIX-SHARDS]).prettier --check,eslintandtsc --noEmitare clean. The suites themselves were not executed locally; CI runs them.Debug-adapter and Test Explorer fixes found by running the new suites
The suites above went from "written" to "passing", and every red they found was a
real defect. In order of how much they cost a user:
fix(test-explorer): a held breakpoint no longer freezes discovery.SharpLspTestController.enqueueserialises everydotnetinvocation so adiscovery sweep and a run cannot rebuild the same
bin//obj/at once. TheDEBUG run was on that queue too — and under
VSTEST_HOST_DEBUGitsdotnet testdoes not exit until the user finishes debugging. So for as longas a breakpoint was held, nothing could be discovered and no other run could
start; pressing Refresh in the Testing view while paused simply hung.
Traced at 39 seconds of a sweep waiting on a paused debuggee, completing
2.7s after the session ended. The run now holds the queue for the BUILD and
releases it once a host is waiting and its attach has settled.
fix(debug): re-issue a refused step against the thread the adapter stopped.workbench.action.debug.stepOverstepsviewModel.focusedThread, and theworkbench focuses the stopped thread only once
fetchCallStack()hasresolved. A step made before that — pressing F10 the instant a breakpoint
hits — is dispatched at
getAllThreads()[0], which in a test host is aruntime or thread-pool thread that never stopped. netcoredbg keeps ONE
current thread and refuses every other, so the gesture surfaced as a raw
0x80004005. The window can be shortened (see below) but never closed: theworkbench's focus is its own asynchronous step. So the refusal is rescued —
a step that comes back
0x80004005or0x80131309is re-issued against thethread the adapter announced, answering the client's original sequence
number. Rescued, never pre-empted: a step the adapter performs is forwarded
untouched, so a user who deliberately selected another stopped thread is
unaffected, and
E_FAILmeans no step happened so re-issuing cannotdouble-step. Same shape as the
0x80070057attach retry beside it.fix(debug): stop paying a 113ms heap walk that can never answer.recoverChainevaluatedTask.s_currentActiveTaskson every stop. Thatregistry only exists once
s_asyncDebuggingEnabledis set, and only a LAUNCHgets the entry stop that sets it — an attach never arms it, so in a test host
the walk could only ever read
null. Alongside it,asyncThreadStacksfetched up to sixteen other threads' FULL stacks one after another. Together
they turned a
stackTracenetcoredbg answered in 2ms into one the clientwaited 149ms for, on every stop. The workbench focuses the stopped thread
only once
fetchCallStack()resolves, so a step made inside that window wentto
getAllThreads()[0]— a thread that never stopped — and netcoredbgrefused it with
0x80004005. Now 20ms, and Step Over/Into/Out work inside atest.
fix(debug): a breakpoint bound after its module loads must still be judged.BreakpointEmulatorindexed armed lines by the line the adapter BOUND, butonly ever learned it from the
setBreakpointsRESPONSE — and everybreakpoint of a test-host attach is answered before the test assembly is
loaded. The real bind arrives later as a
breakpointevent that nothing fedback, so a stop on the bound line missed the index and the hit count the user
typed was silently ignored.
fix(vscode): pipeline the tree tooltip sweep.resolveTreeItem uses LSP hoverwalked several hundred symbol nodes paying two sequential sidecar roundtrips each, plus a workbench open/close per hover on a closed file — 36s on
Linux and past the sweep budget on Windows. Every per-symbol claim is kept.
fix(test): restore every timeout tier to the one the spec publishes.[DIST-CI-VSIX-SHARDS-TIMEOUTS] owns the tier table and states the numbers are
derived from measured behaviour on the CI agents. This branch had cut all
fifteen of them, roughly in half, without amending the spec. Restored;
SETTLE_MS, which is new here, is now in the table too. Measured on aWindows
debug-testsshard: eleven failures at the cut ceilings, four atthe published ones — and the seven that came back finish in 10–12s.
fix(test): a session that terminated is no longer a LIVE session.DebugSessionRecorder.liveSessionswas append-only, soliveOursmeant"every session ever started". The multi-session test polled it until the
stopped session disappeared, which could never happen — and the assertion
after it ("ending the first session must not take the second down with it")
was vacuous. Both are real claims now.
Also in this branch: call hierarchy reports WHERE calls appear rather than where
the caller is declared (C#, F# and the Rust wire all gained the call-site
ranges);
#regionpairs and theusingheader fold; a hover on whitespace isrefused; nested Roslyn refactorings keep the titles that name themselves and
only continuations take their parent's prefix; a generated constructor is a
refactor.rewrite; and a Solution Explorer command invoked from the CommandPalette — with no node argument — warns instead of throwing.
How Do The Automated Tests Prove It Works?
debug-test-debugging-e2e.test.ts—the user can step over, into and out of a helper from inside a testdrives the realworkbench.action.debug.stepOver/
stepInto/stepOutagainst a live netcoredbg attached to a waitingxUnit test host, and asserts the method, line, locals and stack depth after
each gesture. It failed with
Failed command 'next' : 0x80004005before thestack-walk fix.
debug-test-fsharp-e2e.test.ts—an F# [<Theory>] breaks once per row, each with its own arguments(22.7s) anddebug-test-groups-e2e.test.ts—debugging the ASSEMBLY root debugs every namespace under it, in one session(14.6s) both read the tree after their last breakpoint stop. Both hung until
the queue fix, and neither fits the ceiling this branch had cut
DEBUG_TEST_MSto.debug-multisession-e2e.test.ts—stopping the FIRST session leaves the second paused and drivablenow actually observes the first session leave thelive set, then drives the survivor to its next breakpoint.
debug-test-debugging-e2e.test.ts—the Debug gesture does not settle until configurationDone is ANSWEREDcompares bound lines as a SET, which is thereal claim: DAP answers
setBreakpointsin request order and the request isthe workbench's own line-sorted list.
test-explorer-cancellation.test.ts— the recovery test now requires themarker directory to be EMPTY after a fast-only re-run (the real proof the
queue rebuilt the filter instead of replaying the cancelled selection), and a
fifth interaction runs the whole fixture uncancelled so every original
assertion is kept where it is true.
make _test-vsix-shard CHUNK=debug-tests: 51 passing / 3 failingbefore the queue fix, against 11 failing at the cut ceilings.
prettier --check,eslintandtsc --noEmitare clean.The hit-count assertion, and why it changed
a HIT-COUNT breakpoint skips the first theory row and stops on the secondrequired that the second row xUnit EXECUTES be the second
[InlineData]inDECLARATION order. xUnit does not guarantee that:
DefaultTestCaseOrderersorts a class's cases by a hash of the test case's unique id, so the order is
stable per method and arbitrary between methods. This very branch proves it —
the C#
Adds_Rowsand the F#adds rowsdeclare the same two rows in the sameorder and run under the same runner, and the C# theory executes (10, 20, 30)
first while the F# one executes (1, 2, 3) first.
The emulation was never at fault. From the wire: first execution swallowed and
continued (count 1 of 2), second execution forwarded, exactly one stop reaching
VS Code — which is the whole of what
hitCondition: '2'promises.The assertion now claims MORE than it did.
recorder.stops().length === 1isuntouched, and it is what proves a hit was skipped. In place of the
declaration-order check, the three locals must form ONE COHERENT ROW of the
theory: a frame answering
leftfrom one row andexpectedfrom the other —a debugger showing the user a state that never existed — now fails where it
previously passed.
🤖 Generated with Claude Code