Compare commits

...

3 Commits

Author SHA1 Message Date
Joseph Doherty 1f07da2e12 tools: upgrade Get-InfisicalSecret to stream separation, drop banner regex
rust / build / test / clippy / fmt (push) Has been cancelled
rust / cargo public-api drift check (F41) (push) Has been cancelled
Earlier fix (commit 047125b) filtered the infisical CLI's
"A new release of infisical is available" upgrade banner from
captured output via regex matching. That worked but coupled the
filter to specific banner-pattern strings — a future banner shape
("Update available" / "New version detected" / a localized
message) would slip through and break NTLM Type1 auth again.

The principled fix is to stop capturing stderr at all.
PowerShell's call operator (`&`) keeps stdout and stderr on
separate streams unless explicitly merged; the previous code's
`2>&1` was the actual mistake. Without it, the banner stays in
the error stream (visible on the console for diagnostics) and
the captured `$value` contains only the script's stdout — which
for `Get-Secret.ps1` is just the secret value from `infisical
secrets get --plain`.

Verified: live re-run of F54 (lmx_write_complete_live) passes
post-change with `MX_TEST_DOMAIN='DESKTOP-6JL3KKO'` clean and
the banner visibly logged to console (stderr) above each [SET]
line. No regex coupling to a specific banner-pattern remains.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 09:30:52 -04:00
Joseph Doherty 047125bc11 M6 live verification: re-run all 5 steps + filter infisical banner
Three doc fixes pinned by re-running today's full live-test sweep:

1. Bump status header from 2026-05-06 to "re-run 2026-05-07" with a
   note that all 5 steps still pass against the live AVEVA install.
   The first run of step 1 + step 5 today failed with
   `Error::Status { detail: 5 }` (DCE/RPC fault 0x00000005) traced
   to MX_TEST_DOMAIN being polluted with the infisical CLI's
   "A new release of infisical is available" upgrade banner. The
   banner was being concatenated onto the domain string by
   Setup-LiveProbeEnv.ps1's `2>&1` capture, causing NTLM Type1 to
   send a malformed domain field that NmxSvc rejected.

2. Fix tools/Setup-LiveProbeEnv.ps1 — Get-InfisicalSecret now splits
   captured output on newlines, filters lines matching the
   "^A new release of infisical is available" / "^Please upgrade"
   banner patterns, and returns the last non-empty line (the actual
   secret value from `infisical secrets get --plain`). Robust to
   future banner messages of similar shape.

3. Fix two drifted line citations in docs/M6-live-verification.md:
   `recover_connection_core (session.rs:1428-...)` is now at line
   1374 after F56/F45/F47 edits — strip the line number, keep the
   function name (`Session::recover_connection_core`). Same for
   `Session::unsubscribe (session.rs:2261)`.

4. Add "Workspace gate (no live infra needed)" subsection to the
   "Reproducing locally" recipe so a fresh contributor sees the
   full V1 verification recipe (live + workspace gate) in one place.

All 5 live tests pass post-fix:
  - F36 buffered subscribe (drained 1 raw NMX message; no scan
    activity on TestChangingInt today, matches 5/6 baseline)
  - F45 buffered recovery replay (2 pre + 2 post DataUpdate frames)
  - F47 buffered unsubscribe skip (returned Ok)
  - F40 metrics smoke (4 expected metric names present)
  - F54 OnWriteComplete (status detail 9 = WRITE_COMPLETE_OK)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 09:17:46 -04:00
Joseph Doherty d668d5b7b1 mxaccess: fix 9 unit tests broken silently by F56's ensure_publisher_connected
Workspace gate sweep flagged 9 unit tests in mxaccess::session that
had been silently failing since F56 landed (commit 5e11b30). Root
cause: F56 added ensure_publisher_connected (issuing
INmxService2::Connect + AddSubscriberEngine before each
AdviseSupervisory) but the in-process fake-NMX-server fixtures'
responses vec sizes weren't bumped. Once the fake server ran out of
responses mid-handshake, the connection was closed and the client
got ConnectionAborted (10053).

Fix: bumped each test's unauthenticated_server / recording_server
response count by 2 to cover the new pair of RPCs. Tests touched:

  - subscribe_then_unsubscribe_round_trip (2 → 4 responses)
  - two_subscribes_produce_distinct_correlation_ids (4 → 6)
  - subscription_stream_yields_data_change_for_matching_correlation (1 → 3)
  - subscription_stream_filters_out_mismatched_correlation_for_status (1 → 3)
  - subscription_stream_keeps_data_update_regardless_of_correlation (1 → 3)
  - subscribe_populates_registry_unsubscribe_clears_it (2 → 4)
  - read_returns_first_data_change_within_timeout (2 → 4)
  - read_returns_timeout_when_no_data_arrives (2 → 4)
  - unsubscribe_skips_un_advise_for_buffered_subscription (2 → 3
    + mid-flow assertion bumped from len()==1 to len()==3)

The two_subscribes test only adds 2 (not 4) extra responses because
the second subscribe hits the per-engine publisher_endpoints cache.

Workspace gate post-fix: 847 tests pass, 0 failed, 9 ignored
(live-only). Clippy + bench clean. Pinned in
docs/M6-live-verification.md "Workspace gate (2026-05-07)" so the
test-fixture lag is recorded for future audits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 04:44:18 -04:00
3 changed files with 149 additions and 29 deletions
+65 -3
View File
@@ -4,7 +4,9 @@ Per-feature evidence for the M6 work that landed unit-only and now needs end-to-
The sweep is gated on `MX_LIVE=1` env (populate via `tools/Setup-LiveProbeEnv.ps1`). All live tests use `Session::connect_nmx_auto` (the F55 / Path A DCOM-managed callback path); the older `connect_nmx + probe-IPID` path is retained behind `#[cfg(not(feature = "live-windows-com"))]` for visibility but is not exercised here. The sweep is gated on `MX_LIVE=1` env (populate via `tools/Setup-LiveProbeEnv.ps1`). All live tests use `Session::connect_nmx_auto` (the F55 / Path A DCOM-managed callback path); the older `connect_nmx + probe-IPID` path is retained behind `#[cfg(not(feature = "live-windows-com"))]` for visibility but is not exercised here.
## Status (2026-05-06) ## Status (re-run 2026-05-07)
All five steps re-run cleanly against the live AVEVA install on 2026-05-07; outputs match the 2026-05-06 baseline (no behavioural drift since the F56 fix landed). Only fixture-side change: `tools/Setup-LiveProbeEnv.ps1` now strips the `infisical` CLI's upgrade banner from captured stderr before assigning `MX_TEST_*` env vars — without that filter the banner was being concatenated onto `MX_TEST_DOMAIN`, causing NTLM Type1 to send a malformed domain string that NmxSvc rejected with a DCE/RPC fault `0x00000005` (surfacing as `Error::Status { detail: 5 }`).
| Step | Feature | Test | Outcome | | Step | Feature | Test | Outcome |
|---|---|---|---| |---|---|---|---|
@@ -64,7 +66,7 @@ recover_connection returned Ok — F45 buffered replay path executed
post-recovery: drained 2 NMX subscription messages post-recovery: drained 2 NMX subscription messages
``` ```
The replay branch in `recover_connection_core` (`session.rs:1428-...`) re-issues `RegisterReference` (NOT `AdviseSupervisory`) for the buffered entry, mirroring `MxNativeSession.ReAdviseSubscription` (`cs:538-569`). Structural property is unit-tested; this live test confirms the engine actually picks back up after the rebuild + replay. The replay branch in `Session::recover_connection_core` re-issues `RegisterReference` (NOT `AdviseSupervisory`) for the buffered entry, mirroring `MxNativeSession.ReAdviseSubscription` (`cs:538-569`). Structural property is unit-tested; this live test confirms the engine actually picks back up after the rebuild + replay.
## Step 3 — F47 buffered unsubscribe skip (PASS) ## Step 3 — F47 buffered unsubscribe skip (PASS)
@@ -80,7 +82,7 @@ buffered subscribed, correlation_id = [...]
buffered unsubscribe returned Ok — F47 skip path verified live buffered unsubscribe returned Ok — F47 skip path verified live
``` ```
`Session::unsubscribe` (`session.rs:2261`) probes the registry for the subscription's mode; if `Buffered { .. }`, it skips the `nmx.un_advise(...)` wire call entirely. Mirrors the .NET reference's `if (!subscription.IsBuffered)` guard at `MxNativeSession.cs:361-381`. If the implementation accidentally emitted `UnAdvise` for a buffered correlation id, the engine would return non-zero HRESULT (no matching plain advise to retract) — surfacing as a panic in this test. `Session::unsubscribe` probes the registry for the subscription's mode; if `Buffered { .. }`, it skips the `nmx.un_advise(...)` wire call entirely. Mirrors the .NET reference's `if (!subscription.IsBuffered)` guard at `MxNativeSession.cs:361-381`. If the implementation accidentally emitted `UnAdvise` for a buffered correlation id, the engine would return non-zero HRESULT (no matching plain advise to retract) — surfacing as a panic in this test.
## Step 4 — F40 metrics live smoke (PASS) ## Step 4 — F40 metrics live smoke (PASS)
@@ -128,6 +130,8 @@ The `WriteCompleteEvent { server_handle, item_handle, statuses, is_during_recove
## Reproducing locally ## Reproducing locally
### Live tests (require AVEVA + MX_LIVE env)
```powershell ```powershell
# 1. Populate live env from Infisical (dot-source so vars persist). # 1. Populate live env from Infisical (dot-source so vars persist).
. .\tools\Setup-LiveProbeEnv.ps1 . .\tools\Setup-LiveProbeEnv.ps1
@@ -155,6 +159,64 @@ cargo test -p mxaccess-compat --features live-windows-com `
--test buffered_unsubscribe_skip_live -- --ignored --nocapture --test buffered_unsubscribe_skip_live -- --ignored --nocapture
``` ```
### Workspace gate (no live infra needed)
```powershell
cd rust
cargo build --workspace --all-targets
cargo test --workspace --no-fail-fast
cargo clippy --workspace --all-targets -- -D warnings
cargo bench -p mxaccess-codec
```
Expected: build clean, 847 tests pass + 9 ignored (live-only), clippy `-D warnings` clean, bench under R12's < 5 allocs/write target. `cargo fmt --all -- --check` flags pre-existing workspace-wide drift unrelated to any session edit (see § "Workspace gate" below).
## Open work ## Open work
None. F49 sweep complete; F50 (residual Frida capture for Suspend/Activate) closed 2026-05-06 per `docs/F50-suspend-activate-evidence.md`. None. F49 sweep complete; F50 (residual Frida capture for Suspend/Activate) closed 2026-05-06 per `docs/F50-suspend-activate-evidence.md`.
## Workspace gate (2026-05-07)
End-of-session sanity sweep against `master` at commit `9ed4700` plus the F56 unit-test fixture fix that this gate flagged. Run from `rust/` on Windows x64.
| Gate | Command | Result |
|---|---|---|
| Build | `cargo build --workspace --all-targets` | **Pass** (19.81 s) |
| Tests | `cargo test --workspace --no-fail-fast` | **Pass** — 847 passed, 0 failed, 9 ignored (live-only) |
| Clippy | `cargo clippy --workspace --all-targets -- -D warnings` | **Pass** |
| Bench | `cargo bench -p mxaccess-codec` | **Pass** — R12 < 5 allocs/write target met |
The `cargo fmt --all -- --check` gate flags pre-existing workspace-wide rustfmt drift across 29 files (~1000 lines, mostly machine-generated `mxaccess-asb-nettcp/src/nbfs.rs`). Drift is unrelated to any individual session's edits and is documented here as a known workspace-hygiene gap; per-file formatting is applied to edited files at edit time.
### F56 test-fixture bug surfaced + fixed by this gate
The workspace test sweep flagged 9 failing unit tests in `mxaccess::session` that had been silently failing since F56 landed (commit `5e11b30`). Root cause: F56 added `ensure_publisher_connected` (issuing `INmxService2::Connect` + `AddSubscriberEngine` before each `AdviseSupervisory`) but the in-process fake-NMX-server fixtures' `responses` vec sizes weren't bumped to absorb the two new RPCs. Symptom was `ConnectionAborted (10053)` once the fake server's response budget ran out mid-handshake.
Fix: bumped each test's `unauthenticated_server` / `recording_server` response count by 2 to cover Connect + AddSubscriberEngine. Tests touched (all in `crates/mxaccess/src/session.rs::tests`):
- `subscribe_then_unsubscribe_round_trip` (2 → 4 responses)
- `two_subscribes_produce_distinct_correlation_ids` (4 → 6; second subscribe hits the per-engine cache)
- `subscription_stream_yields_data_change_for_matching_correlation` (1 → 3)
- `subscription_stream_filters_out_mismatched_correlation_for_status` (1 → 3)
- `subscription_stream_keeps_data_update_regardless_of_correlation` (1 → 3)
- `subscribe_populates_registry_unsubscribe_clears_it` (2 → 4)
- `read_returns_first_data_change_within_timeout` (2 → 4)
- `read_returns_timeout_when_no_data_arrives` (2 → 4)
- `unsubscribe_skips_un_advise_for_buffered_subscription` (2 → 3 + mid-flow assertion bumped from `len() == 1` to `len() == 3`)
Bench numbers post-fix (release profile, Windows x64):
| scenario | allocs/op |
|---|---|
| `write_message::encode` (Int32) | 2.00 |
| `write_message::encode` (Float32) | 2.00 |
| `write_message::encode` (Float64) | 2.00 |
| `write_message::encode` (Boolean) | 1.00 |
| `write_message::encode` (String, 5 chars) | 4.00 |
| `write_message::encode_to_bytes_mut` (Int32, F52.1) | 2.00 |
| `write_message::encode_into_bytes_mut` (Int32, pooled, F52.3) | 1.00 |
| `write_message::encode_into_bytes_mut` (Boolean, pooled, F52.3) | 0.00 |
| `MxReferenceHandle::from_names` (cache, F52.2) | 0.00 |
| `NmxSubscriptionMessage::parse_inner` (DataUpdate, Int32) | 1.00 |
All numbers match `design/M6-bench-baseline.md` § F52.{1,2,3}.
+69 -22
View File
@@ -2952,8 +2952,16 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn subscribe_then_unsubscribe_round_trip() { async fn subscribe_then_unsubscribe_round_trip() {
// Two RPCs: AdviseSupervisory + UnAdvise. Both return HRESULT 0. // Four RPCs: Connect + AddSubscriberEngine (F56's
let (addr, handle) = unauthenticated_server(vec![(0, Vec::new()), (0, Vec::new())]).await; // ensure_publisher_connected) + AdviseSupervisory + UnAdvise.
// All return HRESULT 0.
let (addr, handle) = unauthenticated_server(vec![
(0, Vec::new()),
(0, Vec::new()),
(0, Vec::new()),
(0, Vec::new()),
])
.await;
let resolver: Arc<dyn Resolver> = Arc::new(StaticResolver::new(&[( let resolver: Arc<dyn Resolver> = Arc::new(StaticResolver::new(&[(
"TestObj.TestInt", "TestObj.TestInt",
sample_metadata(), sample_metadata(),
@@ -3004,12 +3012,15 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn two_subscribes_produce_distinct_correlation_ids() { async fn two_subscribes_produce_distinct_correlation_ids() {
// Two AdviseSupervisory calls + two UnAdvise calls. // Six RPCs: Connect + AddSubscriberEngine (once, cached on the
// 2nd subscribe) + 2 AdviseSupervisory + 2 UnAdvise.
let (addr, handle) = unauthenticated_server(vec![ let (addr, handle) = unauthenticated_server(vec![
(0, Vec::new()), (0, Vec::new()),
(0, Vec::new()), (0, Vec::new()),
(0, Vec::new()), (0, Vec::new()),
(0, Vec::new()), (0, Vec::new()),
(0, Vec::new()),
(0, Vec::new()),
]) ])
.await; .await;
let resolver: Arc<dyn Resolver> = Arc::new(StaticResolver::new(&[( let resolver: Arc<dyn Resolver> = Arc::new(StaticResolver::new(&[(
@@ -3242,7 +3253,9 @@ mod tests {
async fn subscription_stream_yields_data_change_for_matching_correlation() { async fn subscription_stream_yields_data_change_for_matching_correlation() {
use futures_util::StreamExt; use futures_util::StreamExt;
let (addr, handle) = unauthenticated_server(vec![(0, Vec::new())]).await; // Three RPCs: Connect + AddSubscriberEngine (F56) + AdviseSupervisory.
let (addr, handle) =
unauthenticated_server(vec![(0, Vec::new()), (0, Vec::new()), (0, Vec::new())]).await;
let resolver: Arc<dyn Resolver> = Arc::new(StaticResolver::new(&[( let resolver: Arc<dyn Resolver> = Arc::new(StaticResolver::new(&[(
"TestObj.TestInt", "TestObj.TestInt",
sample_metadata(), sample_metadata(),
@@ -3287,7 +3300,9 @@ mod tests {
async fn subscription_stream_filters_out_mismatched_correlation_for_status() { async fn subscription_stream_filters_out_mismatched_correlation_for_status() {
use futures_util::StreamExt; use futures_util::StreamExt;
let (addr, handle) = unauthenticated_server(vec![(0, Vec::new())]).await; // Three RPCs: Connect + AddSubscriberEngine (F56) + AdviseSupervisory.
let (addr, handle) =
unauthenticated_server(vec![(0, Vec::new()), (0, Vec::new()), (0, Vec::new())]).await;
let resolver: Arc<dyn Resolver> = Arc::new(StaticResolver::new(&[( let resolver: Arc<dyn Resolver> = Arc::new(StaticResolver::new(&[(
"TestObj.TestInt", "TestObj.TestInt",
sample_metadata(), sample_metadata(),
@@ -3322,8 +3337,9 @@ mod tests {
use futures_util::StreamExt; use futures_util::StreamExt;
// 0x33 DataUpdate has no item_correlation_id; the .NET-style // 0x33 DataUpdate has no item_correlation_id; the .NET-style
// filter passes them through to all subscriptions. // filter passes them through to all subscriptions.
// Three RPCs: Connect + AddSubscriberEngine (F56) + AdviseSupervisory.
let (addr, handle) = unauthenticated_server(vec![(0, Vec::new())]).await; let (addr, handle) =
unauthenticated_server(vec![(0, Vec::new()), (0, Vec::new()), (0, Vec::new())]).await;
let resolver: Arc<dyn Resolver> = Arc::new(StaticResolver::new(&[( let resolver: Arc<dyn Resolver> = Arc::new(StaticResolver::new(&[(
"TestObj.TestInt", "TestObj.TestInt",
sample_metadata(), sample_metadata(),
@@ -3472,7 +3488,15 @@ mod tests {
// F16: every successful subscribe() inserts into the // F16: every successful subscribe() inserts into the
// SubscriptionEntry registry; unsubscribe() removes it. // SubscriptionEntry registry; unsubscribe() removes it.
// Recovery walks this registry to replay AdviseSupervisory. // Recovery walks this registry to replay AdviseSupervisory.
let (addr, handle) = unauthenticated_server(vec![(0, Vec::new()), (0, Vec::new())]).await; // Four RPCs: Connect + AddSubscriberEngine (F56) +
// AdviseSupervisory + UnAdvise.
let (addr, handle) = unauthenticated_server(vec![
(0, Vec::new()),
(0, Vec::new()),
(0, Vec::new()),
(0, Vec::new()),
])
.await;
let resolver: Arc<dyn Resolver> = Arc::new(StaticResolver::new(&[( let resolver: Arc<dyn Resolver> = Arc::new(StaticResolver::new(&[(
"TestObj.TestInt", "TestObj.TestInt",
sample_metadata(), sample_metadata(),
@@ -3802,8 +3826,15 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn read_returns_first_data_change_within_timeout() { async fn read_returns_first_data_change_within_timeout() {
// Server: AdviseSupervisory ack + UnAdvise ack. // Server: Connect + AddSubscriberEngine (F56) +
let (addr, handle) = unauthenticated_server(vec![(0, Vec::new()), (0, Vec::new())]).await; // AdviseSupervisory + UnAdvise.
let (addr, handle) = unauthenticated_server(vec![
(0, Vec::new()),
(0, Vec::new()),
(0, Vec::new()),
(0, Vec::new()),
])
.await;
let resolver: Arc<dyn Resolver> = Arc::new(StaticResolver::new(&[( let resolver: Arc<dyn Resolver> = Arc::new(StaticResolver::new(&[(
"TestObj.TestInt", "TestObj.TestInt",
sample_metadata(), sample_metadata(),
@@ -3851,9 +3882,16 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn read_returns_timeout_when_no_data_arrives() { async fn read_returns_timeout_when_no_data_arrives() {
// Server only handles the AdviseSupervisory + UnAdvise (no data // Server only handles the Connect + AddSubscriberEngine (F56) +
// injection). Read must hit the timeout branch. // AdviseSupervisory + UnAdvise (no data injection). Read must
let (addr, handle) = unauthenticated_server(vec![(0, Vec::new()), (0, Vec::new())]).await; // hit the timeout branch.
let (addr, handle) = unauthenticated_server(vec![
(0, Vec::new()),
(0, Vec::new()),
(0, Vec::new()),
(0, Vec::new()),
])
.await;
let resolver: Arc<dyn Resolver> = Arc::new(StaticResolver::new(&[( let resolver: Arc<dyn Resolver> = Arc::new(StaticResolver::new(&[(
"TestObj.TestInt", "TestObj.TestInt",
sample_metadata(), sample_metadata(),
@@ -4407,21 +4445,29 @@ mod tests {
/// the negative control; this test pins the buffered branch. /// the negative control; this test pins the buffered branch.
#[tokio::test] #[tokio::test]
async fn unsubscribe_skips_un_advise_for_buffered_subscription() { async fn unsubscribe_skips_un_advise_for_buffered_subscription() {
let (addr, recorded, handle) = // Three RPCs: Connect + AddSubscriberEngine (F56) +
recording_server(vec![(0, Vec::new()), (0, Vec::new())]).await; // AdviseSupervisory. The buffered unsubscribe MUST NOT add a
// fourth (F47 skips UnAdvise on buffered drop).
let (addr, recorded, handle) = recording_server(vec![
(0, Vec::new()),
(0, Vec::new()),
(0, Vec::new()),
])
.await;
let resolver: Arc<dyn Resolver> = Arc::new(StaticResolver::new(&[( let resolver: Arc<dyn Resolver> = Arc::new(StaticResolver::new(&[(
"TestObj.TestInt", "TestObj.TestInt",
sample_metadata(), sample_metadata(),
)])); )]));
let session = connect_test_session(addr, resolver).await.unwrap(); let session = connect_test_session(addr, resolver).await.unwrap();
// Issue a plain subscribe — server records AdviseSupervisory. // Issue a plain subscribe — server records Connect +
// AddSubscriberEngine + AdviseSupervisory.
let sub = session.subscribe("TestObj.TestInt").await.unwrap(); let sub = session.subscribe("TestObj.TestInt").await.unwrap();
let cid = sub.correlation_id; let cid = sub.correlation_id;
assert_eq!( assert_eq!(
recorded.lock().unwrap().len(), recorded.lock().unwrap().len(),
1, 3,
"subscribe should issue 1 RPC" "subscribe should issue 3 RPCs (Connect + AddSubscriberEngine + AdviseSupervisory)"
); );
// Mutate the registry entry's mode to Buffered (synthesise the // Mutate the registry entry's mode to Buffered (synthesise the
@@ -4438,13 +4484,14 @@ mod tests {
} }
// Unsubscribe the now-buffered entry. F47 contract: NO // Unsubscribe the now-buffered entry. F47 contract: NO
// UnAdvise is emitted on the wire; recorded count stays at 1. // UnAdvise is emitted on the wire; recorded count stays at 3
// (the Connect + AddSubscriberEngine + AdviseSupervisory from
// the original subscribe).
session.unsubscribe(sub).await.unwrap(); session.unsubscribe(sub).await.unwrap();
assert_eq!( assert_eq!(
recorded.lock().unwrap().len(), recorded.lock().unwrap().len(),
1, 3,
"buffered unsubscribe must not issue UnAdvise; recorded RPC count must stay at 1 \ "buffered unsubscribe must not issue UnAdvise; recorded RPC count must stay at 3"
(the original AdviseSupervisory)"
); );
// Registry is still cleared — F47's skip applies only to the // Registry is still cleared — F47's skip applies only to the
+15 -4
View File
@@ -67,12 +67,23 @@ function Set-LiveEnvVar {
function Get-InfisicalSecret { function Get-InfisicalSecret {
param([string]$Key, [string]$Env = 'infrastructure', [string]$Path = '/windows-hosts') param([string]$Key, [string]$Env = 'infrastructure', [string]$Path = '/windows-hosts')
# Capture stdout only — the infisical CLI writes its
# "A new release of infisical is available" upgrade banner (and any
# transient diagnostic noise) to STDERR. We deliberately do NOT use
# `2>&1` here so that banner stays in the error stream (visible on
# the console for diagnostics) and never pollutes the secret value.
# An earlier version of this function used `2>&1` plus a regex-based
# banner filter; that approach was brittle to future banner shapes,
# so it was replaced with stream separation. If a real error needs
# to be surfaced, $LASTEXITCODE catches it below.
try { try {
$value = & $GetSecret -Key $Key -Env $Env -Path $Path 2>&1 $value = & $GetSecret -Key $Key -Env $Env -Path $Path
if ($LASTEXITCODE -ne 0 -or -not $value) { if ($LASTEXITCODE -ne 0) {
throw "Get-Secret returned empty for $Env$Path/$Key (exit code $LASTEXITCODE)" throw "Get-Secret exit code $LASTEXITCODE for $Env$Path/$Key"
}
if (-not $value) {
throw "Get-Secret returned empty stdout for $Env$Path/$Key"
} }
# Trim any trailing whitespace from the CLI output
return ($value | Out-String).Trim() return ($value | Out-String).Trim()
} catch { } catch {
throw "Failed to fetch $Env$Path/$Key from Infisical: $_" throw "Failed to fetch $Env$Path/$Key from Infisical: $_"