mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Restore delivery qualification trust
Harden release workflow input transport and make native lifecycle proof honor each platform security boundary. Refresh stale telemetry and delivery qualifications. Contract-Neutral: Formatting-only catalog updates and browser-verified alert copy; release compiler and native lifecycle changes update their owning contracts.
This commit is contained in:
@@ -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" \
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
+1
-3
@@ -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', () => {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user