feat(release): add phase10 tests, smoke load script, release checklist, and onboarding simulator flow

This commit is contained in:
2026-01-25 11:30:00 +00:00
parent 2580719e03
commit 3b61460d7e
6 changed files with 119 additions and 2 deletions

View File

@@ -0,0 +1,30 @@
# Release Checklist (Phase 10)
## Functional verification
- [ ] Device registration and heartbeat flow works for both camera and client roles.
- [ ] Camera-client linking and command dispatch/ack flows work end-to-end.
- [ ] Motion start/end events are persisted and notify linked clients.
- [ ] On-demand stream request/accept/end flow works with media credentials.
- [ ] Recording placeholder creation, finalize, and download URL generation work.
- [ ] Push fallback queue delivers notifications when realtime channel is offline.
## Security and reliability
- [ ] Rate limit thresholds validated in staging.
- [ ] Audit logs capture stream/event/recording sensitive operations.
- [ ] Env vars present: auth base URL, media provider, database, MinIO, secrets.
- [ ] Worker loops running: recordings worker and push worker.
## Observability
- [ ] `/ops/live` and `/ops/ready` healthy.
- [ ] `/ops/metrics` reports request counters and status counters.
- [ ] Structured request logs are visible in deployment log sink.
## Pre-release load and tests
- [ ] Unit tests pass (`bun test`).
- [ ] Smoke load script passes (`bun run load:smoke`).
- [ ] Manual run with simulator in two tabs confirms camera+client happy path.
## Rollout
- [ ] Deploy to staging and run checklist above.
- [ ] Rollout production gradually and watch error/status metrics.
- [ ] Keep rollback procedure ready (previous image/version + migration compatibility).

View File

@@ -39,6 +39,8 @@
"scripts": {
"start": "bun run index.ts",
"dev": "bun --watch index.ts",
"test": "bun test",
"load:smoke": "bun run scripts/load-smoke.ts",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",

View File

@@ -118,9 +118,19 @@
<div class="page">
<h1>Mobile Client Simulator</h1>
<p>
Use this page like a phone app: register device role, connect socket with bearer device token, and run camera/client
actions. You must already be signed in via Better Auth in this browser session.
Use this page like a phone app: authenticate, register device role, connect socket with bearer device token, and run
camera/client actions.
</p>
<section class="panel" style="margin-bottom:16px">
<h2>App-Like Onboarding Flow</h2>
<ol>
<li>Sign up/sign in in the <strong>Auth</strong> panel.</li>
<li>Pick role (<code>camera</code> or <code>client</code>) and register device.</li>
<li>Connect socket to become online.</li>
<li>Client links to camera, requests stream; camera accepts automatically via command handling.</li>
<li>Camera finalizes recording; client fetches download URL and polls push inbox when offline.</li>
</ol>
</section>
<div class="grid">
<section class="panel">

View File

@@ -0,0 +1,26 @@
const baseUrl = process.env.LOAD_BASE_URL ?? 'http://localhost:3000';
const rounds = Number(process.env.LOAD_ROUNDS ?? 25);
const run = async () => {
const start = Date.now();
for (let i = 0; i < rounds; i += 1) {
const [live, ready, metrics] = await Promise.all([
fetch(`${baseUrl}/ops/live`),
fetch(`${baseUrl}/ops/ready`),
fetch(`${baseUrl}/ops/metrics`),
]);
if (!live.ok || !ready.ok || !metrics.ok) {
throw new Error(`Smoke load failed at round ${i + 1}`);
}
}
const totalMs = Date.now() - start;
console.log(`Completed ${rounds} rounds in ${totalMs}ms`);
};
run().catch((error) => {
console.error(error);
process.exit(1);
});

View File

@@ -0,0 +1,24 @@
import { describe, expect, test } from 'bun:test';
import { createDeviceToken, verifyDeviceToken } from '../utils/device-token';
describe('device token', () => {
test('roundtrips valid payload', () => {
const token = createDeviceToken({
userId: 'user-1',
deviceId: 'device-1',
role: 'client',
}, 60);
const payload = verifyDeviceToken(token);
expect(payload).not.toBeNull();
expect(payload?.userId).toBe('user-1');
expect(payload?.deviceId).toBe('device-1');
expect(payload?.role).toBe('client');
});
test('rejects malformed tokens', () => {
expect(verifyDeviceToken('bad-token')).toBeNull();
});
});

25
Backend/tests/env.test.ts Normal file
View File

@@ -0,0 +1,25 @@
import { afterEach, describe, expect, test } from 'bun:test';
import { getBetterAuthBaseUrl, getFirstDefinedEnv } from '../utils/env';
const ORIGINAL_ENV = { ...process.env };
afterEach(() => {
process.env = { ...ORIGINAL_ENV };
});
describe('env helpers', () => {
test('getFirstDefinedEnv returns first set value', () => {
process.env.TEST_A = '';
process.env.TEST_B = ' value-b ';
expect(getFirstDefinedEnv('TEST_A', 'TEST_B')).toBe('value-b');
});
test('getBetterAuthBaseUrl prefers BETTER_AUTH_BASE_URL over legacy var', () => {
process.env.BETTER_AUTH_BASE_URL = 'http://base-url:4000';
process.env.BETTER_AUTH_URL = 'http://legacy:3000';
expect(getBetterAuthBaseUrl()).toBe('http://base-url:4000');
});
});