diff --git a/.github/workflows/compile-release-payload.yml b/.github/workflows/compile-release-payload.yml index eb7726a69..e400e6fe2 100644 --- a/.github/workflows/compile-release-payload.yml +++ b/.github/workflows/compile-release-payload.yml @@ -135,12 +135,13 @@ jobs: - name: Build hosted qualification subjects env: + EXPECTED_SOURCE_SHA: ${{ inputs.source_sha }} PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }} VERSION: ${{ inputs.version }} run: | set -euo pipefail test "$(tr -d '\n' < VERSION)" = "${VERSION}" - test "$(git rev-parse HEAD)" = "${{ inputs.source_sha }}" + test "$(git rev-parse HEAD)" = "${EXPECTED_SOURCE_SHA}" ./scripts/build-secure-runtime-qualification.sh \ "${VERSION}" \ "$RUNNER_TEMP/secure-runtime-qualification" \ diff --git a/cmd/pulse-agent/collector_lifecycle_test.go b/cmd/pulse-agent/collector_lifecycle_test.go index 2efbc1715..540984a08 100644 --- a/cmd/pulse-agent/collector_lifecycle_test.go +++ b/cmd/pulse-agent/collector_lifecycle_test.go @@ -12,6 +12,7 @@ import ( "testing" "github.com/rcourtman/pulse-go-rewrite/internal/collectorlifecycle" + internalsecurity "github.com/rcourtman/pulse-go-rewrite/internal/securityutil" ) func TestCollectorLifecycleCommandReducesAuthorityWithFileBearer(t *testing.T) { @@ -112,5 +113,8 @@ func writeCollectorLifecycleToken(t *testing.T, bearer string) string { if err := os.WriteFile(path, []byte(bearer), 0600); err != nil { t.Fatal(err) } + if err := internalsecurity.HardenPrivatePath(path, 0600); err != nil { + t.Fatalf("harden collector lifecycle token: %v", err) + } return path } diff --git a/cmd/pulse-agent/main_test.go b/cmd/pulse-agent/main_test.go index c57de0458..2dc0bdeae 100644 --- a/cmd/pulse-agent/main_test.go +++ b/cmd/pulse-agent/main_test.go @@ -1752,7 +1752,7 @@ func TestRunConfiguresTypedPrivilegeHelperFromInstallerEnvironment(t *testing.T) newHostAgent = originalHost }() - const socketPath = "/run/pulse-agent/helper.sock" + socketPath := filepath.Join(t.TempDir(), "helper.sock") t.Setenv("PULSE_AGENT_HELPER_SOCKET", socketPath) configuredPath := "" configuredUpdatePath := "" @@ -1817,7 +1817,7 @@ func TestRunRejectsUnhealthyTypedPrivilegeHelper(t *testing.T) { newHostAgent = originalHost }() - t.Setenv("PULSE_AGENT_HELPER_SOCKET", "/run/pulse-agent/helper.sock") + t.Setenv("PULSE_AGENT_HELPER_SOCKET", filepath.Join(t.TempDir(), "helper.sock")) helper := &helperHealthStub{healthErr: errors.New("incompatible helper")} newPrivilegeHelperTelemetry = func(string) (hostagent.PrivilegedTelemetry, error) { return helper, nil diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 886da60e0..576d8df3d 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -6059,6 +6059,15 @@ the governed frontend toolchain and must therefore select Node.js `24`, in parity with the release and integration workflows. That build prerequisite does not widen agent runtime or installer authority, but drift to another Node major invalidates the native lifecycle job as release evidence. +Platform-native lifecycle proof must exercise the platform's real filesystem +boundary rather than importing Unix test assumptions. Collector bearer files +use owner and mode checks on Unix and protected owner/DACL checks on Windows; +test credentials must be hardened through that same platform abstraction. +Privilege-helper test paths must be absolute under the executing platform, and +download fixtures must derive the release asset OS and architecture from the +native runner. A fixture that silently requests amd64 on arm64, or a Windows +test rejected only because `/run/...` is not a Windows absolute path, is not +valid lifecycle evidence. Normal release promotion requires platform-native identity for desktop agent binaries. macOS agents must be Developer ID signed, submitted successfully to diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 8c860f86f..217875614 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -1909,7 +1909,10 @@ artifact-selection behaviour. immutable action SHAs, GitHub-hosted jobs must target an explicit Ubuntu LTS runner image instead of `ubuntu-latest`, and checked-in CI/test Dockerfiles under this subsystem must pin base images by immutable `@sha256` digest and - must not depend on floating `:latest` base tags. + must not depend on floating `:latest` base tags. Workflow dispatch inputs, + secrets, and attacker-controlled event metadata must enter generated runner + scripts through explicit environment variables; `${{ }}` interpolation in + a `run` program is not an acceptable data boundary. Whenever that policy changes, update the owning workflow/install proof files in `scripts/installtests/build_release_assets_test.go` and `scripts/release_control/release_promotion_policy_*` in the same slice. diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 163eb8987..07b1b9b43 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,31 +1,22 @@ { "version": 1, - "base_sha": "22fd662fb794f63efb9d3ca2158de73c4e07e1b8", - "verified_at": "2026-08-30T23:35:47Z", + "base_sha": "fda955627f3dd8d4c844de040e56d209864815e7", + "verified_at": "2026-08-31T01:58:05Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/api/notifications.ts", - "frontend-modern/src/features/alerts/AlertDeliveryHealthCard.tsx", - "frontend-modern/src/features/alerts/AlertDeliveryLogCard.tsx", - "frontend-modern/src/features/alerts/OverviewTab.tsx", - "frontend-modern/src/features/alerts/tabs/DestinationsTab.tsx", - "frontend-modern/src/features/alerts/useNotificationDeliveryLog.ts", "frontend-modern/src/utils/alertDestinationsPresentation.ts" ], "content_sha256": { - "frontend-modern/src/api/notifications.ts": "3351869d747f2b915173d4923c61ee020256930c35d545976c1050976d38e587", - "frontend-modern/src/features/alerts/AlertDeliveryHealthCard.tsx": "1f8109a0405636c6e28af4f182fa20c03fe466379962f613960bf9e8eb9aac50", - "frontend-modern/src/features/alerts/AlertDeliveryLogCard.tsx": "b3eb5fb9d465d70d2ce38d36cef287a15df6f79736139fad1f7186b015446760", - "frontend-modern/src/features/alerts/OverviewTab.tsx": "663960cfbc9980fac55593e7bcc20d9a72c7ba1947d6c9a73d7ffc242f9b561d", - "frontend-modern/src/features/alerts/tabs/DestinationsTab.tsx": "f09fe60111afdc634f5dd6dd631c5f0947f3b6fd7d8b213adb17f5b3c27c0534", - "frontend-modern/src/features/alerts/useNotificationDeliveryLog.ts": "047fe800ad7c4f8ed875f83214a5a161a4342c31799e5efeca5ca2c54054bed1", - "frontend-modern/src/utils/alertDestinationsPresentation.ts": "e46ef2147f70229dc5a78c3167c7ec2021059816799e585b7f3e092fa861fdd3" + "frontend-modern/src/utils/alertDestinationsPresentation.ts": "30cda34c77ce5b7f92b7ef71dc5715ddccf831419f258d4419204152c718baea" }, - "routes": ["/alerts/overview", "/alerts/notifications"], + "routes": [ + "/alerts/overview", + "/alerts/notifications" + ], "viewports": [ { - "width": 1280, - "height": 800 + "width": 1440, + "height": 1000 }, { "width": 390, @@ -33,17 +24,14 @@ } ], "states": [ - "degraded delivery health with one failed and two dead-lettered retained deliveries", - "mixed-retention activity with a named webhook, alert identifier, failure class, redacted-safe error text, and absolute timestamp", - "refresh in progress with the activity refresh control disabled", - "empty delivery activity after a successful refresh", - "unavailable delivery activity after a failed refresh", - "desktop and narrow layouts with health before activity and no horizontal overflow" + "active alert with a rate-limited delivery diagnosis", + "degraded delivery health with retained failures and 7/30-day retention", + "delivery activity interleaving a held notification and a delivered attempt", + "delivery activity after held-event refresh becomes unavailable" ], "interactions": [ - "followed Review delivery activity from Alerts Overview to the Notifications route at 1280x800 and 390x844", - "confirmed the Notifications activity request used the bounded limit=200 query", - "inspected seven-day completed and 30-day dead-letter retention copy plus semantic absolute timestamps at both viewports", - "refreshed activity through loading, restored, empty, and unavailable responses and confirmed loading disabled the refresh control" + "navigated from alert overview to notifications", + "focused and activated Refresh delivery status with the keyboard", + "verified retained delivered evidence remains after held-event refresh fallback" ] } diff --git a/frontend-modern/src/features/alerts/AlertDeliveryHealthCard.test.tsx b/frontend-modern/src/features/alerts/AlertDeliveryHealthCard.test.tsx index d3c89697d..6240142f7 100644 --- a/frontend-modern/src/features/alerts/AlertDeliveryHealthCard.test.tsx +++ b/frontend-modern/src/features/alerts/AlertDeliveryHealthCard.test.tsx @@ -74,7 +74,7 @@ describe('AlertDeliveryHealthCard', () => { 'Check destination credentials, tokens, and account permissions', ); expect(screen.getByRole('alert')).toHaveTextContent( - 'dismiss retained failures to clear this warning without deleting delivery history', + 'Dismiss retained failures to clear this warning without deleting delivery history', ); expect(screen.getByRole('link', { name: 'Review delivery activity' })).toHaveAttribute( 'href', diff --git a/frontend-modern/src/features/alerts/__tests__/AlertIncidentSynthesisSummary.test.tsx b/frontend-modern/src/features/alerts/__tests__/AlertIncidentSynthesisSummary.test.tsx index d265f5af8..6eb94ebc6 100644 --- a/frontend-modern/src/features/alerts/__tests__/AlertIncidentSynthesisSummary.test.tsx +++ b/frontend-modern/src/features/alerts/__tests__/AlertIncidentSynthesisSummary.test.tsx @@ -63,9 +63,7 @@ describe('AlertIncidentSynthesisSummary', () => { await fireEvent.click(screen.getByText('Review synthesis evidence')); expect(screen.getByText('Checkout')).toBeInTheDocument(); expect(screen.getByText('evidence_checkout')).toBeInTheDocument(); - expect( - screen.getByText(/compare its timing before accepting this cause/i), - ).toBeInTheDocument(); + expect(screen.getByText(/compare its timing before accepting this cause/i)).toBeInTheDocument(); }); it('labels contradictory correlation as an observation set without claiming root cause', () => { diff --git a/frontend-modern/src/i18n/__tests__/i18n.test.ts b/frontend-modern/src/i18n/__tests__/i18n.test.ts index c0985fd21..f385d1169 100644 --- a/frontend-modern/src/i18n/__tests__/i18n.test.ts +++ b/frontend-modern/src/i18n/__tests__/i18n.test.ts @@ -126,7 +126,9 @@ describe('i18n foundation', () => { expect(t('alerts.overview.group.acknowledge', { count: 3 }, locale)).toBeTruthy(); expect(t('alerts.overview.synthesis.title.supported', undefined, locale)).toBeTruthy(); expect(t('alerts.overview.synthesis.title.observationSet', undefined, locale)).toBeTruthy(); - expect(t('alerts.overview.synthesis.failure.applicationResponse', undefined, locale)).toBeTruthy(); + expect( + t('alerts.overview.synthesis.failure.applicationResponse', undefined, locale), + ).toBeTruthy(); expect(t('alerts.overview.synthesis.review', undefined, locale)).toBeTruthy(); } }); diff --git a/frontend-modern/src/i18n/messages.de.ts b/frontend-modern/src/i18n/messages.de.ts index 3cb03f8aa..c21f497cb 100644 --- a/frontend-modern/src/i18n/messages.de.ts +++ b/frontend-modern/src/i18n/messages.de.ts @@ -82,7 +82,8 @@ export const DE_MESSAGE_OVERRIDES = { 'alerts.overview.synthesis.failure.certificate': 'Zertifikat', 'alerts.overview.synthesis.failure.dependency': 'Abhaengigkeit', 'alerts.overview.synthesis.failure.evidenceCoverage': 'Evidenzabdeckung', - 'alerts.overview.synthesis.counts': '{affected} betroffene Ressourcen · {observations} Beobachtungen', + 'alerts.overview.synthesis.counts': + '{affected} betroffene Ressourcen · {observations} Beobachtungen', 'alerts.overview.synthesis.review': 'Synthese-Evidenz pruefen', 'alerts.overview.synthesis.challenge.supported': 'Alle verknuepften Signale unten oeffnen und ihre Zeitpunkte vergleichen, bevor diese Ursache akzeptiert wird.', diff --git a/frontend-modern/src/i18n/messages.es.ts b/frontend-modern/src/i18n/messages.es.ts index eec662653..831df47cb 100644 --- a/frontend-modern/src/i18n/messages.es.ts +++ b/frontend-modern/src/i18n/messages.es.ts @@ -81,7 +81,8 @@ export const ES_MESSAGE_OVERRIDES = { 'alerts.overview.synthesis.failure.certificate': 'Certificado', 'alerts.overview.synthesis.failure.dependency': 'Dependencia', 'alerts.overview.synthesis.failure.evidenceCoverage': 'Cobertura de evidencia', - 'alerts.overview.synthesis.counts': '{affected} recursos afectados · {observations} observaciones', + 'alerts.overview.synthesis.counts': + '{affected} recursos afectados · {observations} observaciones', 'alerts.overview.synthesis.review': 'Revisar la evidencia de la sintesis', 'alerts.overview.synthesis.challenge.supported': 'Expande cada senal vinculada y compara su tiempo antes de aceptar esta causa.', diff --git a/frontend-modern/src/utils/__tests__/alertDestinationsPresentation.test.ts b/frontend-modern/src/utils/__tests__/alertDestinationsPresentation.test.ts index f6093bf0f..61c556657 100644 --- a/frontend-modern/src/utils/__tests__/alertDestinationsPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/alertDestinationsPresentation.test.ts @@ -183,7 +183,7 @@ describe('alertDestinationsPresentation', () => { failureClassesAvailable: true, }), ).toBe( - '1 failed delivery retained for 7 days and 2 dead-lettered deliveries retained for 30 days. These notifications were not delivered. Most recent terminal failures were classified as authentication (3). Check destination credentials, tokens, and account permissions. Review delivery activity in Notifications for timestamps, destinations, alerts, and safely redacted errors. After correcting the destination, retry them; dismiss retained failures to clear this warning without deleting delivery history. Otherwise Pulse removes expired records hourly after their retention limit. Recoverable retry attempts do not trigger this warning.', + '1 failed delivery retained for 7 days and 2 dead-lettered deliveries retained for 30 days. These notifications were not delivered. Most recent terminal failures were classified as authentication (3). Check destination credentials, tokens, and account permissions. Review delivery activity in Notifications for timestamps, destinations, alerts, and safely redacted errors. After correcting the destination, retry them. Dismiss retained failures to clear this warning without deleting delivery history. Otherwise Pulse removes expired records hourly after their retention limit. Recoverable retry attempts do not trigger this warning.', ); expect( getAlertDestinationsDeliveryHealthDescription({ diff --git a/frontend-modern/src/utils/alertDestinationsPresentation.ts b/frontend-modern/src/utils/alertDestinationsPresentation.ts index 9cbd0e855..c988f743c 100644 --- a/frontend-modern/src/utils/alertDestinationsPresentation.ts +++ b/frontend-modern/src/utils/alertDestinationsPresentation.ts @@ -211,7 +211,7 @@ export function getAlertDestinationsDeliveryHealthDescription(input: { diagnostic = `Most recent terminal failures were classified as ${dominant[0].replace('_', ' ')} (${dominant[1]}). ${guidanceByClass[dominant[0]] ?? guidanceByClass.unknown}`; } } - return `${summary}. These notifications were not delivered. ${diagnostic} Review delivery activity in Notifications for timestamps, destinations, alerts, and safely redacted errors. After correcting the destination, retry them; dismiss retained failures to clear this warning without deleting delivery history. Otherwise Pulse removes expired records hourly after their retention limit. Recoverable retry attempts do not trigger this warning.`; + return `${summary}. These notifications were not delivered. ${diagnostic} Review delivery activity in Notifications for timestamps, destinations, alerts, and safely redacted errors. After correcting the destination, retry them. Dismiss retained failures to clear this warning without deleting delivery history. Otherwise Pulse removes expired records hourly after their retention limit. Recoverable retry attempts do not trigger this warning.`; } export function getAlertDestinationsDeliveryRefreshLabel() { @@ -266,7 +266,7 @@ export function getAlertDestinationsDeliveryLogDescription( completedRetentionDays: number, deadLetterRetentionDays: number, ) { - return `Delivery attempts and held notifications for real alerts. Completed attempts are retained for ${completedRetentionDays} days; failures that exhausted retries remain available for ${deadLetterRetentionDays} days. Test sends skip the queue and are not listed here.`; + return `Delivery attempts and held notifications for real alerts. Completed attempts are retained for ${completedRetentionDays} days. Failures that exhausted retries remain available for ${deadLetterRetentionDays} days. Test sends skip the queue and are not listed here.`; } export function getAlertDestinationsDeliveryLogEmpty() { diff --git a/internal/collectorlifecycle/client.go b/internal/collectorlifecycle/client.go index 3e060dfb6..be80244a3 100644 --- a/internal/collectorlifecycle/client.go +++ b/internal/collectorlifecycle/client.go @@ -261,10 +261,9 @@ func readPrivateBearer(path string, tokenOwnerUID *uint64) (string, error) { if err != nil { return "", fmt.Errorf("inspect collector lifecycle token file descriptor: %w", err) } - // The typed collector credential is root-owned 0640 with read access only - // for its dedicated service group. Group write/execute and all other access - // are forbidden; 0600 remains valid for legacy/root-only installs. - if !info.Mode().IsRegular() || info.Mode().Perm()&0037 != 0 { + // Apply the platform-native privacy boundary before validating the owner. + // Unix uses mode bits; Windows enforces the equivalent protected DACL below. + if !info.Mode().IsRegular() || !credentialFileModePrivate(info) { return "", errors.New("collector lifecycle token file must be a private regular file") } if err := validateCredentialFileOwner(path, info, tokenOwnerUID); err != nil { diff --git a/internal/collectorlifecycle/credential_mode_unix.go b/internal/collectorlifecycle/credential_mode_unix.go new file mode 100644 index 000000000..e202f1158 --- /dev/null +++ b/internal/collectorlifecycle/credential_mode_unix.go @@ -0,0 +1,12 @@ +//go:build !windows + +package collectorlifecycle + +import "os" + +func credentialFileModePrivate(info os.FileInfo) bool { + // The typed collector credential is root-owned 0640 with read access only + // for its dedicated service group. Group write/execute and all other access + // are forbidden; 0600 remains valid for legacy/root-only installs. + return info.Mode().Perm()&0037 == 0 +} diff --git a/internal/collectorlifecycle/credential_mode_windows.go b/internal/collectorlifecycle/credential_mode_windows.go new file mode 100644 index 000000000..6f2d0b92e --- /dev/null +++ b/internal/collectorlifecycle/credential_mode_windows.go @@ -0,0 +1,11 @@ +//go:build windows + +package collectorlifecycle + +import "os" + +func credentialFileModePrivate(_ os.FileInfo) bool { + // Windows does not enforce Unix permission bits. validateCredentialFileOwner + // applies the equivalent owner and protected-DACL policy after this check. + return true +} diff --git a/scripts/installtests/build_release_assets_test.go b/scripts/installtests/build_release_assets_test.go index 708b12ef9..5950afc92 100644 --- a/scripts/installtests/build_release_assets_test.go +++ b/scripts/installtests/build_release_assets_test.go @@ -2544,6 +2544,8 @@ func TestSecureRuntimeQualificationPacketIsHostedAndReleaseBound(t *testing.T) { "runs-on: ubuntu-24.04", "attestations: write", "id-token: write", + `EXPECTED_SOURCE_SHA: ${{ inputs.source_sha }}`, + `test "$(git rev-parse HEAD)" = "${EXPECTED_SOURCE_SHA}"`, "./scripts/build-secure-runtime-qualification.sh", "secure-runtime-compiler-subjects.sha256", "secure-runtime-compiler-provenance.sigstore.json", @@ -2555,6 +2557,9 @@ func TestSecureRuntimeQualificationPacketIsHostedAndReleaseBound(t *testing.T) { if strings.Contains(hostedJob, "PULSE_UPDATE_SIGNING_KEY") || strings.Contains(hostedJob, "PULSE_LICENSE_PUBLIC_KEY") { t.Fatal("hosted secure-runtime compiler must not receive private signing or license material") } + if strings.Contains(hostedJob, `test "$(git rev-parse HEAD)" = "${{ inputs.source_sha }}"`) { + t.Fatal("hosted secure-runtime compiler must pass the requested source SHA through env instead of generating shell source") + } builder := read("scripts", "build-secure-runtime-qualification.sh") for _, required := range []string{ diff --git a/scripts/installtests/install_mcp_test.go b/scripts/installtests/install_mcp_test.go index 3577057f5..ce275b652 100644 --- a/scripts/installtests/install_mcp_test.go +++ b/scripts/installtests/install_mcp_test.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strings" "testing" ) @@ -19,6 +20,7 @@ func TestInstallMCPRequiresSignedChecksumEvidence(t *testing.T) { } } + binaryName := "pulse-mcp-" + runtime.GOOS + "-" + runtime.GOARCH tests := []struct { name string manifest func(binary []byte) string @@ -31,7 +33,7 @@ func TestInstallMCPRequiresSignedChecksumEvidence(t *testing.T) { name: "valid signed manifest", manifest: func(binary []byte) string { digest := sha256.Sum256(binary) - return fmt.Sprintf("%x pulse-mcp-linux-amd64\n", digest) + return fmt.Sprintf("%x %s\n", digest, binaryName) }, wantSuccess: true, wantOutput: "release signature verified", @@ -40,7 +42,7 @@ func TestInstallMCPRequiresSignedChecksumEvidence(t *testing.T) { name: "manifest unavailable", manifest: func(binary []byte) string { digest := sha256.Sum256(binary) - return fmt.Sprintf("%x pulse-mcp-linux-amd64\n", digest) + return fmt.Sprintf("%x %s\n", digest, binaryName) }, missingAsset: "checksums.txt", wantOutput: "could not fetch checksums.txt; refusing unverified install", @@ -49,7 +51,7 @@ func TestInstallMCPRequiresSignedChecksumEvidence(t *testing.T) { name: "signature unavailable", manifest: func(binary []byte) string { digest := sha256.Sum256(binary) - return fmt.Sprintf("%x pulse-mcp-linux-amd64\n", digest) + return fmt.Sprintf("%x %s\n", digest, binaryName) }, missingAsset: "checksums.txt.sshsig", wantOutput: "could not fetch checksums.txt.sshsig; refusing unverified install", @@ -58,7 +60,7 @@ func TestInstallMCPRequiresSignedChecksumEvidence(t *testing.T) { name: "signature invalid", manifest: func(binary []byte) string { digest := sha256.Sum256(binary) - return fmt.Sprintf("%x pulse-mcp-linux-amd64\n", digest) + return fmt.Sprintf("%x %s\n", digest, binaryName) }, invalidSig: true, wantOutput: "cryptographic signature verification failed for checksums.txt", @@ -75,16 +77,16 @@ func TestInstallMCPRequiresSignedChecksumEvidence(t *testing.T) { name: "binary duplicated", manifest: func(binary []byte) string { digest := sha256.Sum256(binary) - return fmt.Sprintf("%x pulse-mcp-linux-amd64\n%x pulse-mcp-linux-amd64\n", digest, digest) + return fmt.Sprintf("%x %s\n%x %s\n", digest, binaryName, digest, binaryName) }, wantOutput: "checksums.txt must contain exactly one valid SHA256 entry", }, { name: "digest mismatch", manifest: func(_ []byte) string { - return strings.Repeat("0", 64) + " pulse-mcp-linux-amd64\n" + return strings.Repeat("0", 64) + " " + binaryName + "\n" }, - wantOutput: "sha256 mismatch for pulse-mcp-linux-amd64", + wantOutput: "sha256 mismatch for " + binaryName, }, } @@ -101,7 +103,7 @@ func TestInstallMCPRequiresSignedChecksumEvidence(t *testing.T) { } binary := []byte("test pulse-mcp executable\n") - writeTestFile(t, filepath.Join(fixtureDir, "pulse-mcp-linux-amd64"), binary, 0o644) + writeTestFile(t, filepath.Join(fixtureDir, binaryName), binary, 0o644) manifestPath := filepath.Join(fixtureDir, "checksums.txt") writeTestFile(t, manifestPath, []byte(tt.manifest(binary)), 0o644) diff --git a/tests/integration/tests/19-telemetry-disclosure.spec.ts b/tests/integration/tests/19-telemetry-disclosure.spec.ts index 7430762e3..fdff7dd19 100644 --- a/tests/integration/tests/19-telemetry-disclosure.spec.ts +++ b/tests/integration/tests/19-telemetry-disclosure.spec.ts @@ -10,7 +10,7 @@ import { import { createAuthenticatedStorageState } from "./helpers"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const EXPECTED_TELEMETRY_SCHEMA_VERSION = 15; +const EXPECTED_TELEMETRY_SCHEMA_VERSION = 16; type WorkerFixtures = { authStorageStatePath: string; diff --git a/tests/integration/tests/93-alert-operator-qualification.spec.ts b/tests/integration/tests/93-alert-operator-qualification.spec.ts index 94e77746e..9268223c1 100644 --- a/tests/integration/tests/93-alert-operator-qualification.spec.ts +++ b/tests/integration/tests/93-alert-operator-qualification.spec.ts @@ -272,8 +272,12 @@ test.describe('Alert operator qualification', () => { }); }); - test('shows per-alert delivery diagnosis and interleaves held notification evidence', async ({ page }) => { - await page.setViewportSize({ width: 1440, height: 1000 }); + test('shows per-alert delivery diagnosis and interleaves held notification evidence', async ({ page }, testInfo) => { + await page.setViewportSize( + testInfo.project.name.startsWith('mobile-') + ? { width: 390, height: 844 } + : { width: 1440, height: 1000 }, + ); await routeStateWithActiveAlerts(page, [activeDeliveryAlert]); await page.route('**/api/alerts/config', (route) => route.fulfill({ @@ -306,24 +310,33 @@ test.describe('Alert operator qualification', () => { status: 200, contentType: 'application/json', body: JSON.stringify({ - overall_healthy: true, + overall_healthy: false, queue: { - status: 'healthy', - healthy: true, + status: 'degraded', + healthy: false, pending: 0, sending: 0, sent: 1, - failed: 0, - dlq: 0, - terminal_failure_count: 0, - attention_required: 0, - reason_codes: [], + failed: 1, + dlq: 2, + terminal_failure_count: 3, + attention_required: 3, + reason_codes: ['retained_failed_deliveries', 'retained_dead_letter_deliveries'], completed_retention_days: 7, dead_letter_retention_days: 30, counts_are_retention_bounded: true, retry_attempts_affect_health: false, terminal_failures_affect_health: true, - failure_classes_7d: {}, + failure_classes_7d: { + authentication: 3, + rate_limited: 0, + connectivity: 0, + tls: 0, + configuration: 0, + rejected: 0, + server_error: 0, + unknown: 0, + }, failure_classes_available: true, failure_class_window_days: 7, }, @@ -336,6 +349,8 @@ test.describe('Alert operator qualification', () => { contentType: 'application/json', body: JSON.stringify({ window_days: 7, + completed_retention_days: 7, + dead_letter_retention_days: 30, entries: [ { notificationId: 'attempt-1', @@ -381,6 +396,17 @@ test.describe('Alert operator qualification', () => { await page.goto('/alerts/notifications', { waitUntil: 'domcontentloaded' }); await expect(page.getByRole('heading', { name: 'Notifications', exact: true })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Delivery activity' })).toBeVisible(); + await expect( + page.getByText( + /Dismiss retained failures to clear this warning without deleting delivery history/, + ), + ).toBeVisible(); + await expect( + page.getByText( + 'Completed attempts are retained for 7 days. Failures that exhausted retries remain available for 30 days.', + { exact: false }, + ), + ).toBeVisible(); await expect(page.getByText('Deferred')).toBeVisible(); await expect(page.getByText('Quiet hours')).toBeVisible(); await expect(page.getByText('Delivered', { exact: true })).toBeVisible();