From 7222bbc2e685e61a3a38c4226fd9a19a23ed15fb Mon Sep 17 00:00:00 2001 From: Mohamed Zaki Date: Wed, 25 Feb 2026 22:42:49 +0200 Subject: [PATCH 1/4] Add CI test pipeline with Key Vault secrets and log redaction --- .gitignore | 7 +- ci/azure-pipelines.yml | 12 ++++ ci/generate-subscription-file.yml | 17 +++++ ci/load-build-secrets.sh | 88 +++++++++++++++++++++++++ speech/conversation_transcriber_test.go | 66 +++++++++---------- speech/translation_recognizer_test.go | 10 ++- 6 files changed, 165 insertions(+), 35 deletions(-) create mode 100644 ci/generate-subscription-file.yml create mode 100644 ci/load-build-secrets.sh diff --git a/.gitignore b/.gitignore index 73fed67..648e543 100644 --- a/.gitignore +++ b/.gitignore @@ -330,4 +330,9 @@ ASALocalRun/ .mfractor/ # temp files -tmp_* \ No newline at end of file +tmp_* + +# Test secrets (generated by CI or local dev) +secrets/ +test.subscriptions.regions.json +test.certificates.json diff --git a/ci/azure-pipelines.yml b/ci/azure-pipelines.yml index 59ea034..2721576 100644 --- a/ci/azure-pipelines.yml +++ b/ci/azure-pipelines.yml @@ -59,3 +59,15 @@ steps: export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$HOME/carbon/current/lib/x64" go build -v ./... displayName: 'Build' + +- template: generate-subscription-file.yml + +- script: | + set +x # NEVER enable trace mode + export CGO_CFLAGS="-I$HOME/carbon/current/include/c_api" + export CGO_LDFLAGS="-L$HOME/carbon/current/lib/x64 -lMicrosoft.CognitiveServices.Speech.core" + export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$HOME/carbon/current/lib/x64" + + source ci/load-build-secrets.sh + go test -v ./speech 2>&1 | global_redact + displayName: 'Run Go tests' diff --git a/ci/generate-subscription-file.yml b/ci/generate-subscription-file.yml new file mode 100644 index 0000000..c493dc8 --- /dev/null +++ b/ci/generate-subscription-file.yml @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft. All rights reserved. +# Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +# +# This file defines a reusable step that downloads the subscriptions JSON from Azure Key Vault +# and writes it to a local file for use by load-build-secrets.sh. +steps: +- task: AzureKeyVault@2 + inputs: + azureSubscription: 'ADO -> Speech Services - DEV - SDK' + keyVaultName: "CarbonSDK-CICD" + secretsFilter: 'CarbonSubscriptionsJson' +- task: file-creator@6 + inputs: + filepath: '$(Build.SourcesDirectory)/secrets/test.subscriptions.regions.json' + filecontent: '$(CarbonSubscriptionsJson)' + fileoverwrite: true + displayName: "Ensure subscriptions .json file" diff --git a/ci/load-build-secrets.sh b/ci/load-build-secrets.sh new file mode 100644 index 0000000..53a1608 --- /dev/null +++ b/ci/load-build-secrets.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# Copyright (c) Microsoft. All rights reserved. +# Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +# +# Loads test secrets from the subscriptions JSON file generated by +# generate-subscription-file.yml and exports the environment variables +# required by the Go test suite. +# +# Usage: +# source ci/load-build-secrets.sh +# +# The script expects the JSON at $BUILD_SOURCESDIRECTORY/secrets/test.subscriptions.regions.json +# (set by Azure DevOps) or falls back to ./secrets/test.subscriptions.regions.json. + +set -euo pipefail + +# CRITICAL: Disable trace mode before handling secrets. If the caller +# had 'set -x' enabled (e.g. for debugging), bash would echo every +# variable assignment — including the subscription key — to stderr +# before global_redact is available to filter it. +set +x + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +SECRETS_DIR="${BUILD_SOURCESDIRECTORY:-$REPO_ROOT}/secrets" +SUBSCRIPTIONS_FILE="$SECRETS_DIR/test.subscriptions.regions.json" + +if [[ ! -f "$SUBSCRIPTIONS_FILE" ]]; then + echo "ERROR: Subscriptions JSON not found at $SUBSCRIPTIONS_FILE" + echo "Run the generate-subscription-file.yml step first." + exit 1 +fi + +# Require jq +if ! command -v jq &>/dev/null; then + echo "ERROR: jq is required but not installed." + exit 1 +fi + +# Extract speech subscription key and region +export SPEECH_SUBSCRIPTION_KEY=$(jq -jr '.UnifiedSpeechSubscription.Key' "$SUBSCRIPTIONS_FILE") +export SPEECH_SUBSCRIPTION_REGION=$(jq -jr '.UnifiedSpeechSubscription.Region' "$SUBSCRIPTIONS_FILE") + +if [[ -z "$SPEECH_SUBSCRIPTION_KEY" || "$SPEECH_SUBSCRIPTION_KEY" == "null" ]]; then + echo "ERROR: Failed to extract UnifiedSpeechSubscription.Key from $SUBSCRIPTIONS_FILE" + exit 1 +fi + +if [[ -z "$SPEECH_SUBSCRIPTION_REGION" || "$SPEECH_SUBSCRIPTION_REGION" == "null" ]]; then + echo "ERROR: Failed to extract UnifiedSpeechSubscription.Region from $SUBSCRIPTIONS_FILE" + exit 1 +fi + +echo "Loaded speech subscription for region: $SPEECH_SUBSCRIPTION_REGION" + +# --- Redaction helpers (prevent secrets from leaking in CI logs) --- + +GLOBAL_STRINGS_TO_REDACT=( + "$SPEECH_SUBSCRIPTION_KEY" +) + +URL_ENCODED_SPEECH_SUBSCRIPTION_KEY=$(jq -nr --arg v "$SPEECH_SUBSCRIPTION_KEY" '$v | @uri') +if [[ -n "$URL_ENCODED_SPEECH_SUBSCRIPTION_KEY" && "$URL_ENCODED_SPEECH_SUBSCRIPTION_KEY" != "$SPEECH_SUBSCRIPTION_KEY" ]]; then + GLOBAL_STRINGS_TO_REDACT+=("$URL_ENCODED_SPEECH_SUBSCRIPTION_KEY") +fi +unset URL_ENCODED_SPEECH_SUBSCRIPTION_KEY + +redact_input_with() { + # Redacts known sensitive strings from stdin. + # Avoids repeated invocation overhead by using perl streaming. + perl -MIO::Handle -lpe \ + 'BEGIN { + STDOUT->autoflush(1); + STDERR->autoflush(1); + if (@ARGV) { + $re = sprintf "(?:%s)", (join "|", map { quotemeta $_ } splice @ARGV); + $re = qr/$re/ + } + } + $re and s/$re/***/gi' "$@" +} + +global_redact() { + redact_input_with "${GLOBAL_STRINGS_TO_REDACT[@]}" +} + +export -f redact_input_with +export -f global_redact diff --git a/speech/conversation_transcriber_test.go b/speech/conversation_transcriber_test.go index 8f50222..264efd1 100644 --- a/speech/conversation_transcriber_test.go +++ b/speech/conversation_transcriber_test.go @@ -30,7 +30,7 @@ func setupConversation(t *testing.T) (teardown func()) { logLines.WriteString(diagnostics.GetMemoryLogLine(i)) } - t.Log(logLines.String()) + t.Log(redactSecrets(logLines.String())) } } } @@ -222,14 +222,14 @@ func TestConversationTranscriberSingleSpeaker(t *testing.T) { transcribingFuture := make(chan bool) transcribedFuture := make(chan bool) - + transcribedHandler := func(event ConversationTranscriptionEventArgs) { defer event.Close() t.Log("Transcribed text: ", event.Result.Text) t.Log("Speaker ID: ", event.Result.SpeakerID) transcribedFuture <- true } - + transcribingHandler := func(event ConversationTranscriptionEventArgs) { defer event.Close() t.Log("Transcribing text: ", event.Result.Text) @@ -239,16 +239,16 @@ func TestConversationTranscriberSingleSpeaker(t *testing.T) { default: } } - + transcriber.Transcribed(transcribedHandler) transcriber.Transcribing(transcribingHandler) - + // Start transcribing err := <-transcriber.StartTranscribingAsync() if err != nil { t.Error("Got error: ", err) } - + // Wait for transcribing event select { case <-transcribingFuture: @@ -256,7 +256,7 @@ func TestConversationTranscriberSingleSpeaker(t *testing.T) { case <-time.After(5 * time.Second): t.Error("Timeout waiting for Transcribing event.") } - + // Wait for transcribed event select { case <-transcribedFuture: @@ -264,7 +264,7 @@ func TestConversationTranscriberSingleSpeaker(t *testing.T) { case <-time.After(5 * time.Second): t.Error("Timeout waiting for Transcribed event.") } - + // Stop transcribing err = <-transcriber.StopTranscribingAsync() if err != nil { @@ -290,65 +290,65 @@ func TestConversationTranscriberContinuousRecognition(t *testing.T) { t.Error("Got an error ", err.Error()) } defer format.Close() - + stream, err := audio.CreatePushAudioInputStreamFromFormat(format) if err != nil { t.Error("Got an error ", err.Error()) } defer stream.Close() - + audioConfig, err := audio.NewAudioConfigFromStreamInput(stream) if err != nil { t.Error("Got an error ", err.Error()) } defer audioConfig.Close() - + transcriber := createConversationTranscriberFromAudioConfig(t, audioConfig) if transcriber == nil { t.Error("Transcriber creation failed") return } defer transcriber.Close() - + firstResult := true transcribedFuture := make(chan string, 10) transcribingFuture := make(chan string, 10) sessionStoppedFuture := make(chan bool, 1) canceledFuture := make(chan bool, 1) - + // Channel to collect speaker IDs speakerIDsChan := make(chan string, 200) - + transcribedHandler := func(event ConversationTranscriptionEventArgs) { defer event.Close() firstResult = true t.Log("Transcribed: ", event.Result.Text) t.Log("Speaker ID: ", event.Result.SpeakerID) - + // Send speaker ID to the channel if it's not empty if event.Result.SpeakerID != "" && event.Result.SpeakerID != "Unknown" { speakerIDsChan <- event.Result.SpeakerID } - + transcribedFuture <- "Transcribed" } - + transcribingHandler := func(event ConversationTranscriptionEventArgs) { defer event.Close() t.Log("Transcribing: ", event.Result.Text) t.Log("Speaker ID: ", event.Result.SpeakerID) - + // Send speaker ID to the channel if it's not empty if event.Result.SpeakerID != "" && event.Result.SpeakerID != "Unknown" { speakerIDsChan <- event.Result.SpeakerID } - + if firstResult { firstResult = false transcribingFuture <- "Transcribing" } } - + transcriber.Transcribed(transcribedHandler) transcriber.Transcribing(transcribingHandler) transcriber.SessionStopped(func(event SessionEventArgs) { @@ -364,18 +364,18 @@ func TestConversationTranscriberContinuousRecognition(t *testing.T) { } t.Error("Canceled was not due to EOS " + event.ErrorDetails) }) - + err = <-transcriber.StartTranscribingAsync() if err != nil { t.Error("Got error: ", err) } - + // Pump audio data into the stream pumpFileIntoStream(t, "../test_files/katiesteve_mono.wav", stream) pumpFileIntoStream(t, "../test_files/katiesteve_mono.wav", stream) pumpSilenceIntoStream(t, stream) stream.CloseStream() - + // Wait for first transcribing event select { case <-transcribingFuture: @@ -383,7 +383,7 @@ func TestConversationTranscriberContinuousRecognition(t *testing.T) { case <-time.After(30 * time.Second): t.Error("Didn't receive first Transcribing event.") } - + // Wait for first transcribed event select { case <-transcribedFuture: @@ -391,7 +391,7 @@ func TestConversationTranscriberContinuousRecognition(t *testing.T) { case <-time.After(30 * time.Second): t.Error("Didn't receive first Transcribed event.") } - + // Wait for second transcribing event select { case <-transcribingFuture: @@ -399,7 +399,7 @@ func TestConversationTranscriberContinuousRecognition(t *testing.T) { case <-time.After(30 * time.Second): t.Error("Didn't receive second Transcribing event.") } - + // Wait for second transcribed event select { case <-transcribedFuture: @@ -407,12 +407,12 @@ func TestConversationTranscriberContinuousRecognition(t *testing.T) { case <-time.After(30 * time.Second): t.Error("Didn't receive second Transcribed event.") } - + err = <-transcriber.StopTranscribingAsync() if err != nil { t.Error("Got error: ", err) } - + // Wait for session stopped event select { case <-sessionStoppedFuture: @@ -420,22 +420,22 @@ func TestConversationTranscriberContinuousRecognition(t *testing.T) { case <-time.After(30 * time.Second): t.Error("Timeout waiting for SessionStopped event.") } - + // Close the speaker ID channel to signal we're done collecting IDs close(speakerIDsChan) - + // Collect unique speaker IDs uniqueSpeakerIDs := make(map[string]bool) for speakerID := range speakerIDsChan { uniqueSpeakerIDs[speakerID] = true } - + // Verify that more than one speaker ID was detected if len(uniqueSpeakerIDs) <= 1 { - t.Errorf("Expected more than 1 unique speaker ID, but got %d: %v", + t.Errorf("Expected more than 1 unique speaker ID, but got %d: %v", len(uniqueSpeakerIDs), getKeysFromMap(uniqueSpeakerIDs)) } else { - t.Logf("Successfully detected %d unique speaker IDs: %v", + t.Logf("Successfully detected %d unique speaker IDs: %v", len(uniqueSpeakerIDs), getKeysFromMap(uniqueSpeakerIDs)) } } diff --git a/speech/translation_recognizer_test.go b/speech/translation_recognizer_test.go index 497fe97..834d867 100644 --- a/speech/translation_recognizer_test.go +++ b/speech/translation_recognizer_test.go @@ -14,6 +14,14 @@ import ( "github.com/Microsoft/cognitive-services-speech-sdk-go/diagnostics" ) +func redactSecrets(s string) string { + key := os.Getenv("SPEECH_SUBSCRIPTION_KEY") + if key != "" { + s = strings.ReplaceAll(s, key, "***") + } + return s +} + func setup(t *testing.T) (teardown func()) { logLineAtStart := diagnostics.GetMemoryLogLineNumNewest() diagnostics.StartMemoryLogging() @@ -30,7 +38,7 @@ func setup(t *testing.T) (teardown func()) { logLines.WriteString(diagnostics.GetMemoryLogLine(i)) } - t.Log(logLines.String()) + t.Log(redactSecrets(logLines.String())) } } } From b3fd98b95b7f82b8dc388d0237b13a754b740ac5 Mon Sep 17 00:00:00 2001 From: Mohamed Zaki Date: Wed, 25 Feb 2026 23:35:02 +0200 Subject: [PATCH 2/4] Fix flaky Recognizing event assertions in translation and speech tests The Speech service may skip Translation.Hypothesis (Recognizing) events for short audio or under service load, jumping straight to the final Translation.Phrase (Recognized). Tests were treating these as mandatory, causing intermittent failures. Changes: - Buffer event channels (cap 1 for RecognizeOnce, cap 10 for continuous) - Wait for Recognized events first (guaranteed by the service) - Check Recognizing events non-fatally via default/drain pattern - Increase timeouts from 5s to 10-15s for service variability Verified stable: 29/29 tests PASS, 10/10 on -count=10 for previously flaky TestTranslationRecognizeOnce. --- speech/speech_recognizer_test.go | 62 ++++++++++++++++----------- speech/translation_recognizer_test.go | 58 ++++++++++++++----------- 2 files changed, 70 insertions(+), 50 deletions(-) diff --git a/speech/speech_recognizer_test.go b/speech/speech_recognizer_test.go index ca7ce7a..c1889ca 100644 --- a/speech/speech_recognizer_test.go +++ b/speech/speech_recognizer_test.go @@ -120,13 +120,13 @@ func TestRecognizeOnce(t *testing.T) { return } defer recognizer.Close() - recognizedFuture := make(chan string) + recognizedFuture := make(chan string, 1) recognizedHandler := func(event SpeechRecognitionEventArgs) { defer event.Close() t.Log("Recognized: ", event.Result.Text) recognizedFuture <- "Recognized" } - recognizingFuture := make(chan string) + recognizingFuture := make(chan string, 1) recognizingHandle := func(event SpeechRecognitionEventArgs) { defer event.Close() t.Log("Recognizing: ", event.Result.Text) @@ -138,17 +138,20 @@ func TestRecognizeOnce(t *testing.T) { recognizer.Recognized(recognizedHandler) recognizer.Recognizing(recognizingHandle) result := recognizer.RecognizeOnceAsync() - select { - case <-recognizingFuture: - t.Log("Received at least one Recognizing event.") - case <-time.After(5 * time.Second): - t.Error("Didn't receive Recognizing event.") - } + // Wait for Recognized event first — this is guaranteed by the service. + // Then check result. Recognizing (hypothesis) events are best-effort; + // the service may skip them for short audio. select { case <-recognizedFuture: t.Log("Received a Recognized event.") - case <-time.After(5 * time.Second): - t.Error("Didn't receive Recognizing event.") + case <-time.After(10 * time.Second): + t.Error("Didn't receive Recognized event.") + } + select { + case <-recognizingFuture: + t.Log("Received at least one Recognizing event.") + default: + t.Log("No Recognizing events received (service may skip hypotheses for short audio).") } select { case <-result: @@ -180,8 +183,8 @@ func TestContinuousRecognition(t *testing.T) { } defer recognizer.Close() firstResult := true - recognizedFuture := make(chan string) - recognizingFuture := make(chan string) + recognizedFuture := make(chan string, 10) + recognizingFuture := make(chan string, 10) recognizedHandler := func(event SpeechRecognitionEventArgs) { defer event.Close() firstResult = true @@ -206,30 +209,37 @@ func TestContinuousRecognition(t *testing.T) { pumpFileIntoStream(t, "../test_files/turn_on_the_lamp.wav", stream) pumpSilenceIntoStream(t, stream) stream.CloseStream() - select { - case <-recognizingFuture: - t.Log("Received first Recognizing event.") - case <-time.After(5 * time.Second): - t.Error("Didn't receive first Recognizing event.") - } + // Wait for Recognized events first — these are guaranteed by the service. + // Recognizing (hypothesis) events are best-effort and may be skipped + // under service load or for short audio segments. select { case <-recognizedFuture: t.Log("Received first Recognized event.") - case <-time.After(5 * time.Second): + case <-time.After(15 * time.Second): t.Error("Didn't receive first Recognized event.") } select { - case <-recognizingFuture: - t.Log("Received second Recognizing event.") - case <-time.After(5 * time.Second): - t.Error("Didn't receive second Recognizing event.") - } - select { case <-recognizedFuture: t.Log("Received second Recognized event.") - case <-time.After(5 * time.Second): + case <-time.After(15 * time.Second): t.Error("Didn't receive second Recognized event.") } + // Check Recognizing events (best-effort, non-fatal) + recognizingCount := 0 + for { + select { + case <-recognizingFuture: + recognizingCount++ + default: + goto doneChecking + } + } +doneChecking: + if recognizingCount > 0 { + t.Logf("Received %d Recognizing event(s).", recognizingCount) + } else { + t.Log("No Recognizing events received (service may skip hypotheses under load).") + } err = <-recognizer.StopContinuousRecognitionAsync() if err != nil { t.Error("Got error: ", err) diff --git a/speech/translation_recognizer_test.go b/speech/translation_recognizer_test.go index 834d867..8f467da 100644 --- a/speech/translation_recognizer_test.go +++ b/speech/translation_recognizer_test.go @@ -159,7 +159,7 @@ func TestTranslationRecognizeOnce(t *testing.T) { return } defer recognizer.Close() - recognizedFuture := make(chan string) + recognizedFuture := make(chan string, 1) recognizedHandler := func(event TranslationRecognitionEventArgs) { defer event.Close() t.Log("Recognized text: ", event.Result.Text) @@ -168,7 +168,7 @@ func TestTranslationRecognizeOnce(t *testing.T) { t.Log("French translation: ", translations["fr"]) recognizedFuture <- "Recognized" } - recognizingFuture := make(chan string) + recognizingFuture := make(chan string, 1) recognizingHandle := func(event TranslationRecognitionEventArgs) { defer event.Close() t.Log("Recognizing text: ", event.Result.Text) @@ -183,19 +183,22 @@ func TestTranslationRecognizeOnce(t *testing.T) { recognizer.Recognized(recognizedHandler) recognizer.Recognizing(recognizingHandle) result := recognizer.RecognizeOnceAsync() - select { - case <-recognizingFuture: - t.Log("Received at least one Recognizing event.") - case <-time.After(5 * time.Second): - t.Error("Didn't receive Recognizing event.") - } + // Wait for Recognized event first — this is guaranteed by the service. + // Then check result. Recognizing (hypothesis) events are best-effort; + // the service may skip them for short audio. select { case <-recognizedFuture: t.Log("Received a Recognized event.") - case <-time.After(5 * time.Second): + case <-time.After(10 * time.Second): t.Error("Didn't receive Recognized event.") } select { + case <-recognizingFuture: + t.Log("Received at least one Recognizing event.") + default: + t.Log("No Recognizing events received (service may skip hypotheses for short audio).") + } + select { case outcome := <-result: if outcome.Error != nil { t.Error("Got an error: ", outcome.Error) @@ -288,30 +291,37 @@ func ImplTranslationContinuousRecognition(t *testing.T, runToEnd bool) { pumpFileIntoStream(t, "../test_files/turn_on_the_lamp.wav", stream) pumpSilenceIntoStream(t, stream) stream.CloseStream() - select { - case <-recognizingFuture: - t.Log("Received first Recognizing event.") - case <-time.After(5 * time.Second): - t.Error("Didn't receive first Recognizing event.") - } + // Wait for Recognized events first — these are guaranteed by the service. + // Recognizing (hypothesis) events are best-effort and may be skipped + // under service load or for short audio segments. select { case <-recognizedFuture: t.Log("Received first Recognized event.") - case <-time.After(5 * time.Second): + case <-time.After(15 * time.Second): t.Error("Didn't receive first Recognized event.") } select { - case <-recognizingFuture: - t.Log("Received second Recognizing event.") - case <-time.After(5 * time.Second): - t.Error("Didn't receive second Recognizing event.") - } - select { case <-recognizedFuture: t.Log("Received second Recognized event.") - case <-time.After(5 * time.Second): + case <-time.After(15 * time.Second): t.Error("Didn't receive second Recognized event.") } + // Check Recognizing events (best-effort, non-fatal) + recognizingCount := 0 + for { + select { + case <-recognizingFuture: + recognizingCount++ + default: + goto doneChecking + } + } +doneChecking: + if recognizingCount > 0 { + t.Logf("Received %d Recognizing event(s).", recognizingCount) + } else { + t.Log("No Recognizing events received (service may skip hypotheses under load).") + } if !runToEnd { err = <-recognizer.StopContinuousRecognitionAsync() if err != nil { @@ -321,7 +331,7 @@ func ImplTranslationContinuousRecognition(t *testing.T, runToEnd bool) { select { case <-canceledFuture: t.Log("Cancled EOS") - case <-time.After(5 * time.Second): + case <-time.After(15 * time.Second): t.Error("Didn't receive Canceled event.") } } From e8ea7f5e2d29dd052bdd63c99d5184a28ed50370 Mon Sep 17 00:00:00 2001 From: Mohamed Zaki Date: Wed, 25 Feb 2026 23:41:15 +0200 Subject: [PATCH 3/4] Refactor load-build-secrets.sh to remove outdated comments and improve clarity --- ci/load-build-secrets.sh | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/ci/load-build-secrets.sh b/ci/load-build-secrets.sh index 53a1608..319bd20 100644 --- a/ci/load-build-secrets.sh +++ b/ci/load-build-secrets.sh @@ -1,24 +1,9 @@ #!/usr/bin/env bash # Copyright (c) Microsoft. All rights reserved. # Licensed under the MIT license. See LICENSE.md file in the project root for full license information. -# -# Loads test secrets from the subscriptions JSON file generated by -# generate-subscription-file.yml and exports the environment variables -# required by the Go test suite. -# -# Usage: -# source ci/load-build-secrets.sh -# -# The script expects the JSON at $BUILD_SOURCESDIRECTORY/secrets/test.subscriptions.regions.json -# (set by Azure DevOps) or falls back to ./secrets/test.subscriptions.regions.json. set -euo pipefail - -# CRITICAL: Disable trace mode before handling secrets. If the caller -# had 'set -x' enabled (e.g. for debugging), bash would echo every -# variable assignment — including the subscription key — to stderr -# before global_redact is available to filter it. -set +x +set +x # Never trace secrets SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" @@ -27,17 +12,14 @@ SUBSCRIPTIONS_FILE="$SECRETS_DIR/test.subscriptions.regions.json" if [[ ! -f "$SUBSCRIPTIONS_FILE" ]]; then echo "ERROR: Subscriptions JSON not found at $SUBSCRIPTIONS_FILE" - echo "Run the generate-subscription-file.yml step first." exit 1 fi -# Require jq if ! command -v jq &>/dev/null; then echo "ERROR: jq is required but not installed." exit 1 fi -# Extract speech subscription key and region export SPEECH_SUBSCRIPTION_KEY=$(jq -jr '.UnifiedSpeechSubscription.Key' "$SUBSCRIPTIONS_FILE") export SPEECH_SUBSCRIPTION_REGION=$(jq -jr '.UnifiedSpeechSubscription.Region' "$SUBSCRIPTIONS_FILE") @@ -53,8 +35,6 @@ fi echo "Loaded speech subscription for region: $SPEECH_SUBSCRIPTION_REGION" -# --- Redaction helpers (prevent secrets from leaking in CI logs) --- - GLOBAL_STRINGS_TO_REDACT=( "$SPEECH_SUBSCRIPTION_KEY" ) @@ -66,8 +46,6 @@ fi unset URL_ENCODED_SPEECH_SUBSCRIPTION_KEY redact_input_with() { - # Redacts known sensitive strings from stdin. - # Avoids repeated invocation overhead by using perl streaming. perl -MIO::Handle -lpe \ 'BEGIN { STDOUT->autoflush(1); From 9ee24d5d7385e791c9206e7ac76b7ad02b25aad0 Mon Sep 17 00:00:00 2001 From: Mohamed Zaki Date: Wed, 25 Feb 2026 23:51:14 +0200 Subject: [PATCH 4/4] Simplify Recognizing event drain: replace goto with len() --- speech/speech_recognizer_test.go | 14 ++------------ speech/translation_recognizer_test.go | 14 ++------------ 2 files changed, 4 insertions(+), 24 deletions(-) diff --git a/speech/speech_recognizer_test.go b/speech/speech_recognizer_test.go index c1889ca..0e8a9f3 100644 --- a/speech/speech_recognizer_test.go +++ b/speech/speech_recognizer_test.go @@ -225,18 +225,8 @@ func TestContinuousRecognition(t *testing.T) { t.Error("Didn't receive second Recognized event.") } // Check Recognizing events (best-effort, non-fatal) - recognizingCount := 0 - for { - select { - case <-recognizingFuture: - recognizingCount++ - default: - goto doneChecking - } - } -doneChecking: - if recognizingCount > 0 { - t.Logf("Received %d Recognizing event(s).", recognizingCount) + if n := len(recognizingFuture); n > 0 { + t.Logf("Received %d Recognizing event(s).", n) } else { t.Log("No Recognizing events received (service may skip hypotheses under load).") } diff --git a/speech/translation_recognizer_test.go b/speech/translation_recognizer_test.go index 8f467da..b55927e 100644 --- a/speech/translation_recognizer_test.go +++ b/speech/translation_recognizer_test.go @@ -307,18 +307,8 @@ func ImplTranslationContinuousRecognition(t *testing.T, runToEnd bool) { t.Error("Didn't receive second Recognized event.") } // Check Recognizing events (best-effort, non-fatal) - recognizingCount := 0 - for { - select { - case <-recognizingFuture: - recognizingCount++ - default: - goto doneChecking - } - } -doneChecking: - if recognizingCount > 0 { - t.Logf("Received %d Recognizing event(s).", recognizingCount) + if n := len(recognizingFuture); n > 0 { + t.Logf("Received %d Recognizing event(s).", n) } else { t.Log("No Recognizing events received (service may skip hypotheses under load).") }