Repository navigation
Fix #15: version-stamped interop assemblies + tolerant module loader (Bsn.Host 0.2.2, Bsn.Common 0.1.1, Bsn.SystemManagement 0.2.1) - #16
Merged
Conversation
User report: Import-Module -Force after Update-Module failed with
FileLoadException ('Assembly with same name is already loaded').
Root cause (two layers):
1. Every shipped module version packaged the DLL with the default
AssemblyVersion 1.0.0.0 - identical identity across releases.
2. Verified with raw .NET (Assembly.LoadFrom of two versions in one
process): PowerShell/.NET cannot host two versions of the same
assembly NAME in one process, regardless of AssemblyVersion. A
same-session switch after Update-Module is a platform limitation
(same guidance as Az modules: restart the session).
What ships:
- build.ps1 stamps the interop assemblies with the module manifest
version (dotnet build -p:Version=<ModuleVersion>; psd1 stays the
single source of truth). Guard tests in Bsn.Host and
Bsn.SystemManagement assert DLL version == manifest version -
without distinct versions the loader could silently reuse
mismatched interop code.
- New private Import-BsnInteropAssembly helper in Bsn.Common (0.1.1);
both consumer psm1s load their DLL through it instead of the
manifest's RequiredAssemblies (removed):
* same version already loaded (Import-Module -Force) -> reuse
* different version already loaded -> actionable error naming both
versions, the loaded path, the restart remedy and the issue link
* otherwise -> Add-Type -Path
The loader picks the NEWEST loaded Bsn.Common instance (after an
update the session may hold old and new side by side; invoking an
array of module infos stringifies into a bogus command name).
- Dependencies: Bsn.Host 0.2.2 and Bsn.SystemManagement 0.2.1 now
require Bsn.Common >= 0.1.1 (the helper ships there).
- build.ps1 Analyze task now prepends repo src to PSModulePath around
Invoke-ScriptAnalyzer: PSScriptAnalyzer imports the module it
analyzes, and without the prepend it resolved the newest GALLERY
copy - so analysis silently ran against published code and left
that foreign 1.0.0.0 assembly in the process, which is exactly what
used to trip the tests' FileLoadException fallback (and broke the
new loader until diagnosed).
Verified locally (repro scripts in the issue):
- fresh import + -Force reimport of 0.2.2: silent reuse, no error
- gallery 0.2.1 loaded, import 0.2.2: clear guidance error
- full gate Analyze,Test: 81 tests green, analyzer clean
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars.
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…ded sessions Field report: 0.2.1 recordings of OpenCode still showed mojibake (box-drawing borders became CP437-style garbage like 'Gamma-ohmega'). The chunk-boundary decoder fix was correct but only covered ONE corruption mechanism. Probes against the real engine exposed the second: - a fresh pseudo console starts with the OEM output codepage (probe 'chcp.com' records: 'Active code page: 437') - OEM-codepage-decoding conhosts decode a child's stdout BYTES with it: one box-drawing character (U+2500, UTF-8 E2 94 80) arrives as three CP-decoded characters BEFORE the recorder sees anything - pwsh's own output is immune (WriteConsoleW), which is why synthetic pwsh probes passed while real byte-writing TUIs corrupted Fix: the recorded command line bootstraps [Console]::OutputEncoding/InputEncoding to UTF-8 (the programmatic 'chcp 65001') before the user's command or interactive shell runs. Verified end to end: the byte-writer probe now records all 60 border characters byte-for-byte with zero mojibake. New per-scenario system tests (all green): - recorded session reports codepage 65001 (the direct fix proof) - BYTE-WRITING child (raw console-handle output, Go/Node TUI style) asserted character-for-character (60 borders, corners, marker, zero U+FFFD, zero Latin-1-range mojibake) - real OpenCode TUI session (skips when opencode is not installed) - metacharacter round-trip through the REAL ConPTY engine (issue #7 previously only had mocked coverage) Unit tests updated for the wrapper prefix; changelog and gallery release notes updated.
added 3 commits
November 7, 2026 21:55
Resolves the three failing checks on PR #16: - CI (ubuntu/macos): the issue #5 codepage unit test assumed the default interactive shell is pwsh; on Unix it is $env:SHELL, which intentionally gets no inline bootstrap (Unix terminals are natively UTF-8). The test is now split into three platform-independent tests: the -Command wrapper assertions (always pwsh), the PowerShell interactive branch via a mocked Get-BsnDefaultShell (deterministic on every OS), and the bare-shell fallback. - SonarCloud duplication on new code (54 lines, 14.5% vs 3% allowed): the identical 14-line loader blocks in both psm1s and the identical 13-line version-guard tests. Import-BsnInteropAssembly now takes -ModuleRoot and resolves lib/<name>.dll itself, shrinking each consumer psm1 to a two-line invocation with distinct comments; the guard tests are shortened with module-specific comments. - Codecov patch coverage (52.38% vs 80% target): the 10 uncovered lines were the private Import-BsnInteropAssembly helper. New unit tests in Bsn.Common cover the missing-DLL guidance, the fresh Add-Type load (via a mocked session-state seam so it is deterministic regardless of prior process state), same-version silent reuse, and the cross-version actionable guidance. Adds the private Get-BsnLoadedInteropAssembly seam for the AppDomain lookup (static .NET state cannot be faked). Also refreshes the stale RequiredAssemblies reference in the Start-BsnWindowsPtyRecording help and the Bsn.Common changelog entry. Local gate: Analyze clean, 92 tests green (8 + 70 + 14); coverage of the helper is now complete; fresh-process -Force reimport verified.
…egion SonarCloud's second run showed the remaining 18 duplicated new lines are the two psm1 loader invocations sitting inside the long-standing shared loader scaffolding (Bsn.Common bootstrap, PSModulePath self-heal, function-file dot-sourcing) duplicated between Bsn.Host.psm1 and Bsn.SystemManagement.psm1 - any new line inside that region is counted as duplicated against the sibling file. Place the interop assembly loading after the dot-sourcing loop instead: the engine types resolve at call time, so the timing matches what RequiredAssemblies provided, and the new lines fall outside the duplicated region (zero new duplicated lines).
… race Round 2 still counted every new psm1 line as duplicated (4.4%): Sonar merges duplication blocks across single blank lines, and the two identical loader code lines ($commonModule = Get-Module ... / & invoke) form a cross-file token block that pulled the appended region back into the duplicated range. - Give each psm1 lexically distinct new lines: Bsn.SystemManagement now uses its own variable name ($systemInteropHost), its own parameter names ($moduleDirectory/$assemblySimpleName) and its own comment wording, so no token run between the files reaches the duplication block threshold; two blank lines keep the appended block out of the scaffolding's merged range. Zero duplicated new lines remain in the module code. - Fix the SonarCloud Gate workflow race: it accepted the first non-NONE quality-gate status, which is usually the PREVIOUS push's stale result (the workflow starts while Automatic Analysis is still running). It now waits for SonarCloud's own 'SonarCloud Code Analysis' check run of the head commit to complete before reading the gate API. Manual dispatch resolves the head SHA through the GitHub API.
|
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.



ITNOA
What you hit
Root cause (two layers, verified empirically)
AssemblyVersion1.0.0.0 (no csproj version, build.ps1 passed none):identical assembly identity across releases.
the same assembly name in one process - verified with raw
Assembly.LoadFromof two differently-versioned DLLs in one process:same
FileLoadException. A same-session switch afterUpdate-Moduleis aplatform limitation (Az modules give the same guidance: restart the
session).
What ships
build.ps1stamps assemblies with the manifest version (dotnet build -p:Version=<ModuleVersion>)AssemblyVersion== manifestModuleVersion(both interop modules)Import-BsnInteropAssemblyin Bsn.Common 0.1.1; both psm1s load their DLL through it (RequiredAssembliesremoved)-Forcereimport of the same version reuses the loaded assembly (no more FileLoadException); a different version already loaded fails with actionable guidance naming both versions, the loaded path, the restart remedy and the issue linkAnalyzetask prepends reposrcto PSModulePath aroundInvoke-ScriptAnalyzerVerified locally
-Forcereimport of 0.2.2: silent reuse, no errorCannot load Bsn.Host version 0.2.2.0: version 1.0.0.0 is already loaded in this session (from '...0.2.1\lib\Bsn.Host.dll'). PowerShell cannot host two versions of the same assembly in one process; restart the session (or open a new one) and import the module again.(instead of the cryptic raw FileLoadException)Analyze, Test: analyzer clean, 81 tests greenBonus fix: issue #5 (second root cause) - codepage mojibake in byte-writing TUIs
Field testing of 0.2.1 showed OpenCode recordings still corrupted: box-drawing
borders turned into CP437-style garbage while ASCII text survived. Probes
against the real engine exposed a second corruption mechanism the decoder fix
did not cover:
chcp.comrecords "Active code page: 437")box-drawing character (U+2500, UTF-8
E2 94 80) arrives as three CP-decodedcharacters before the recorder sees anything
WriteConsoleW), which is why synthetic pwshprobes passed while real byte-writing TUIs (Go/Bubble Tea such as OpenCode)
were corrupted
Fix: the recorded command line now bootstraps
[Console]::OutputEncoding/InputEncodingto UTF-8 (the programmaticchcp 65001) before the user's command or interactive shell runs. Verifiedend to end: a byte-writing child now records all 60 border characters
byte-for-byte with zero mojibake.
New per-scenario system tests (the reported failures, precisely)
chcp.commust report 65001╭───╮ │ SALAM ╰───╯asserted character-for-character (60 borders, 4 corners, zero U+FFFD, zero Latin-1-range mojibake)opencode(skips when not installed); zero U+FFFD, zero mojibake, UI text captureda|b&c<d>e^frecorded verbatim via actual ConPTY (previously only mocked)Existing #6 (stop-channel flush) and #8 (redirected stdio) coverage unchanged.
Versions in this PR
After merge: release
v0.2.2publishes all three (each is newer than the gallery).