Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/deploy-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,23 @@ jobs:
exit 1
fi

helm_integration_conclusion="$(
gh api \
"repos/${GITHUB_REPOSITORY}/commits/${RELEASE_SHA}/check-runs" \
--jq '
.check_runs[] |
select(.name == "helm-test") |
.conclusion
' | \
head -n 1
)"
if [[ "${helm_integration_conclusion}" != "success" ]]; then
echo "Helm integration check did not pass for ${RELEASE_TAG}."
echo "Expected check run name: helm-test"
echo "Commit: ${RELEASE_SHA}"
exit 1
fi
Comment on lines +291 to +306

- name: Install cluster CLI
run: |
set -euo pipefail
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/helm-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,12 @@ jobs:
pod -l "${SELECTOR}=frontend" --timeout=120s

- name: Run helm test
run: helm test sre-simulator --timeout 4m
run: helm test sre-simulator --logs --timeout 4m

- name: Show test pod logs on failure
if: failure()
run: |
kubectl get pods
kubectl logs sre-simulator-test || true
kubectl describe pod sre-simulator-test || true
echo "--- Backend logs ---"
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ All notable changes to this project are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.0] - 2026-05-12

### Added (0.2.0)

- Added gameplay lifecycle analytics surfaces, including admin session summaries and difficulty/scenario breakdown views, with dedicated admin endpoint protection.
- Added server-owned scenario context binding to sessions so chat/command AI paths use trusted stored scenario payloads instead of mutable client context.

### Changed (0.2.0)

- Hardened runtime reliability with bounded timeout/cancellation behavior for scenario generation and chat streaming paths.
- Updated deployment wiring for secure proxy-aware origin handling and configurable admin analytics visibility in frontend and Helm values.
- Updated release/deploy gates so production deployment checks require successful Helm runtime integration checks.

### Security (0.2.0)

- Disabled persistent leaderboard writes by default behind `PERSISTENT_LEADERBOARD_ENABLED` to prevent forged client-submitted scoring from becoming persistent records.

## [0.1.2] - 2026-04-18

### Release Hardening
Expand Down
5 changes: 5 additions & 0 deletions backend/.env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ TURNSTILE_EXPECTED_HOSTNAME=localhost
ANTI_ABUSE_HMAC_SECRET=replace-with-long-random-anti-abuse-secret
GAMEPLAY_ADMIN_TOKEN=replace-with-long-random-gameplay-admin-token
AUTOMATED_TRAFFIC_TOKEN=replace-with-long-random-automated-traffic-token
TRUST_PROXY_HEADERS=false
# Keep false in production until server-owned scoring is fully deployed.
PERSISTENT_LEADERBOARD_ENABLED=false
# Optional rate limits (requests per minute)
# GAMEPLAY_TELEMETRY_RATE_LIMIT_MAX=60
# GAMEPLAY_ADMIN_RATE_LIMIT_MAX=15
Expand All @@ -35,6 +38,8 @@ AUTOMATED_TRAFFIC_TOKEN=replace-with-long-random-automated-traffic-token
# AI_REASONING_EFFORT=medium
# AI_MAX_CHAT_TOKENS=16384
# AI_MAX_COMMAND_TOKENS=8192
# AI_CHAT_TIMEOUT_MS=30000
# AI_COMMAND_TIMEOUT_MS=20000
# AI_SCENARIO_TIMEOUT_MS=30000
# COMPACTION_TOKEN_BUDGET=12000
# COMPACTION_TAIL_MESSAGES=4
4 changes: 2 additions & 2 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sre-simulator-backend",
"version": "0.1.2",
"version": "0.2.0",
"private": true,
"scripts": {
"dev": "npx tsx watch src/index.ts",
Expand Down
8 changes: 8 additions & 0 deletions backend/src/integration/game-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ let savedMockMode: string | undefined;
let savedTurnstileSecret: string | undefined;
let savedAuthSecret: string | undefined;
let savedAntiAbuseSecret: string | undefined;
let savedPersistentLeaderboardEnabled: string | undefined;
const githubAuthCookie = `${VIEWER_SESSION_COOKIE}=${createViewerSessionToken(
{
kind: "github",
Expand All @@ -57,10 +58,12 @@ async function createFullApp(): Promise<Express> {
savedTurnstileSecret = process.env.TURNSTILE_SECRET_KEY;
savedAuthSecret = process.env.AUTH_SESSION_SECRET;
savedAntiAbuseSecret = process.env.ANTI_ABUSE_HMAC_SECRET;
savedPersistentLeaderboardEnabled = process.env.PERSISTENT_LEADERBOARD_ENABLED;
process.env.AI_MOCK_MODE = "true";
process.env.TURNSTILE_SECRET_KEY = "test-secret";
process.env.AUTH_SESSION_SECRET = "test-secret";
process.env.ANTI_ABUSE_HMAC_SECRET = "test-hmac";
process.env.PERSISTENT_LEADERBOARD_ENABLED = "true";
const { initStorage } = await import("../lib/storage");
await initStorage();
const { default: express } = await import("express");
Expand Down Expand Up @@ -122,6 +125,11 @@ afterAll(() => {
} else {
process.env.ANTI_ABUSE_HMAC_SECRET = savedAntiAbuseSecret;
}
if (savedPersistentLeaderboardEnabled === undefined) {
delete process.env.PERSISTENT_LEADERBOARD_ENABLED;
} else {
process.env.PERSISTENT_LEADERBOARD_ENABLED = savedPersistentLeaderboardEnabled;
}
});

describe("health endpoints", () => {
Expand Down
2 changes: 2 additions & 0 deletions backend/src/lib/storage/json-session-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ export class JsonSessionStore implements ISessionStore {
sessions.set(token, {
token,
difficulty: input.difficulty,
scenarioId: input.scenarioId ?? null,
scenarioTitle: input.scenarioTitle,
scenarioPayload: input.scenarioPayload ?? null,
startTime: Date.now(),
used: false,
trafficSource: input.trafficSource ?? "player",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
IF COL_LENGTH('sessions', 'scenario_id') IS NULL
BEGIN
ALTER TABLE sessions
ADD scenario_id NVARCHAR(255) NULL;
END;

IF COL_LENGTH('sessions', 'scenario_payload') IS NULL
BEGIN
ALTER TABLE sessions
ADD scenario_payload NVARCHAR(MAX) NULL;
END;
18 changes: 18 additions & 0 deletions backend/src/lib/storage/mssql-session-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ export class MssqlSessionStore implements ISessionStore {
await this.pool.request()
.input("token", token)
.input("difficulty", input.difficulty)
.input("scenarioId", input.scenarioId ?? null)
.input("scenarioTitle", input.scenarioTitle)
.input("scenarioPayload", input.scenarioPayload ?? null)
.input("startTime", startTime)
.input("trafficSource", input.trafficSource ?? "player")
.input("identityKind", input.identityKind)
Expand All @@ -46,7 +48,9 @@ export class MssqlSessionStore implements ISessionStore {
INSERT INTO sessions (
token,
difficulty,
scenario_id,
scenario_title,
scenario_payload,
start_time,
traffic_source,
identity_kind,
Expand All @@ -58,7 +62,9 @@ export class MssqlSessionStore implements ISessionStore {
VALUES (
@token,
@difficulty,
@scenarioId,
@scenarioTitle,
@scenarioPayload,
@startTime,
@trafficSource,
@identityKind,
Expand Down Expand Up @@ -88,7 +94,9 @@ export class MssqlSessionStore implements ISessionStore {
.query<{
token: string;
difficulty: Difficulty;
scenario_id: string | null;
scenario_title: string;
scenario_payload: string | null;
start_time: number;
used: boolean;
traffic_source: "player" | "automated";
Expand All @@ -101,7 +109,9 @@ export class MssqlSessionStore implements ISessionStore {
SELECT
token,
difficulty,
scenario_id,
scenario_title,
scenario_payload,
start_time,
used,
traffic_source,
Expand All @@ -121,7 +131,9 @@ export class MssqlSessionStore implements ISessionStore {
return {
token: row.token,
difficulty: row.difficulty,
scenarioId: row.scenario_id,
scenarioTitle: row.scenario_title,
scenarioPayload: row.scenario_payload,
startTime: Number(row.start_time),
used: row.used,
trafficSource: row.traffic_source,
Expand All @@ -145,7 +157,9 @@ export class MssqlSessionStore implements ISessionStore {
.query<{
token: string;
difficulty: Difficulty;
scenario_id: string | null;
scenario_title: string;
scenario_payload: string | null;
start_time: number;
used: boolean;
traffic_source: "player" | "automated";
Expand All @@ -160,7 +174,9 @@ export class MssqlSessionStore implements ISessionStore {
OUTPUT
INSERTED.token,
INSERTED.difficulty,
INSERTED.scenario_id,
INSERTED.scenario_title,
INSERTED.scenario_payload,
INSERTED.start_time,
INSERTED.used,
INSERTED.traffic_source,
Expand All @@ -180,7 +196,9 @@ export class MssqlSessionStore implements ISessionStore {
return {
token: row.token,
difficulty: row.difficulty,
scenarioId: row.scenario_id,
scenarioTitle: row.scenario_title,
scenarioPayload: row.scenario_payload,
startTime: Number(row.start_time),
used: true,
trafficSource: row.traffic_source,
Expand Down
12 changes: 12 additions & 0 deletions backend/src/lib/storage/mssql-stores.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,12 @@ describe("MssqlSessionStore", () => {
const row = {
token: validUuid,
difficulty: "hard" as const,
scenario_id: "scenario_hard_001",
scenario_title: "Etcd Quorum Loss",
scenario_payload: '{"id":"scenario_hard_001"}',
start_time: 1700000000000,
used: true,
traffic_source: "player" as const,
identity_kind: "github" as const,
github_user_id: "12345",
github_login: "octocat",
Expand All @@ -88,9 +91,12 @@ describe("MssqlSessionStore", () => {
expect(result).toEqual({
token: validUuid,
difficulty: "hard",
scenarioId: "scenario_hard_001",
scenarioTitle: "Etcd Quorum Loss",
scenarioPayload: '{"id":"scenario_hard_001"}',
startTime: 1700000000000,
used: true,
trafficSource: "player",
identityKind: "github",
githubUserId: "12345",
githubLogin: "octocat",
Expand All @@ -104,9 +110,12 @@ describe("MssqlSessionStore", () => {
const row = {
token: validUuid,
difficulty: "medium" as const,
scenario_id: "scenario_medium_001",
scenario_title: "Bad Egress",
scenario_payload: '{"id":"scenario_medium_001"}',
start_time: 1700000000500,
used: false,
traffic_source: "player" as const,
identity_kind: "anonymous" as const,
github_user_id: null,
github_login: null,
Expand All @@ -121,9 +130,12 @@ describe("MssqlSessionStore", () => {
expect(result).toEqual({
token: validUuid,
difficulty: "medium",
scenarioId: "scenario_medium_001",
scenarioTitle: "Bad Egress",
scenarioPayload: '{"id":"scenario_medium_001"}',
startTime: 1700000000500,
used: false,
trafficSource: "player",
identityKind: "anonymous",
githubUserId: null,
githubLogin: null,
Expand Down
4 changes: 4 additions & 0 deletions backend/src/lib/storage/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ export type { TrafficSource } from "../../../../shared/types/leaderboard";
export interface GameSession {
token: string;
difficulty: Difficulty;
scenarioId: string | null;
scenarioTitle: string;
scenarioPayload: string | null;
startTime: number;
used: boolean;
trafficSource: TrafficSource;
Expand All @@ -22,7 +24,9 @@ export interface GameSession {

export interface CreateGameSessionInput {
difficulty: Difficulty;
scenarioId?: string | null;
scenarioTitle: string;
scenarioPayload?: string | null;
trafficSource?: TrafficSource;
identityKind: SessionIdentityKind;
githubUserId?: string | null;
Expand Down
28 changes: 28 additions & 0 deletions backend/src/routes/chat.mock-mode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,41 @@ describe("POST /api/chat mock mode", () => {

beforeEach(() => {
process.env.AI_MOCK_MODE = "true";
const sessionScenario = {
id: "scenario_mock_easy",
title: "Test Scenario",
difficulty: "easy",
description: "Mock scenario",
incidentTicket: {
id: "IcM-MOCK",
severity: "Sev3",
title: "Mock ticket",
description: "Mock description",
customerImpact: "Low",
reportedTime: "2026-05-01T10:00:00.000Z",
clusterName: "cluster-test",
region: "eastus",
},
clusterContext: {
name: "cluster-test",
version: "4.19.0",
region: "eastus",
nodeCount: 3,
status: "Healthy",
recentEvents: [],
alerts: [],
upgradeHistory: [],
},
};
mocks.getSessionStore.mockReturnValue({
get: mocks.sessionGet,
});
mocks.sessionGet.mockResolvedValue({
token: "session-123",
difficulty: "easy",
scenarioId: "scenario_mock_easy",
scenarioTitle: "Test Scenario",
scenarioPayload: JSON.stringify(sessionScenario),
startTime: Date.now(),
used: false,
trafficSource: "player",
Expand Down
Loading
Loading