test: strengthen deployment verification
This commit is contained in:
+93
-12
@@ -4,7 +4,10 @@ import { createRequire } from 'node:module';
|
||||
import { TextEncoder } from 'node:util';
|
||||
import { chromium } from 'playwright';
|
||||
import { setTimeout as delay } from 'node:timers/promises';
|
||||
import { config as loadEnv } from 'dotenv';
|
||||
import { RoomServiceClient } from 'livekit-server-sdk';
|
||||
|
||||
loadEnv({ path: 'backend/.env', quiet: true });
|
||||
const frontendUrl = process.env.FRONTEND_URL ?? 'http://127.0.0.1:4173/';
|
||||
const shouldStartPreview = !process.env.FRONTEND_URL;
|
||||
const doFetch = globalThis.fetch;
|
||||
@@ -73,7 +76,7 @@ async function connectPublisher(roomName) {
|
||||
}),
|
||||
});
|
||||
const room = new Room();
|
||||
await room.connect(url, token);
|
||||
await room.connect(url, token, { autoSubscribe: true });
|
||||
return room;
|
||||
}
|
||||
|
||||
@@ -107,15 +110,23 @@ async function publishIntervention(room, podId) {
|
||||
reliable: true,
|
||||
topic: DATA_TOPIC,
|
||||
});
|
||||
return intervention;
|
||||
}
|
||||
|
||||
async function publishDataMessage(room, message) {
|
||||
await room.localParticipant.publishData(new TextEncoder().encode(JSON.stringify(message)), {
|
||||
reliable: true,
|
||||
topic: DATA_TOPIC,
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForInterventionCard(page, room, podId) {
|
||||
const cardText = 'Verification collision: two engineers are editing frontend/src/App.tsx.';
|
||||
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||
await publishIntervention(room, podId);
|
||||
const intervention = await publishIntervention(room, podId);
|
||||
try {
|
||||
await page.getByText(cardText).waitFor({ timeout: 5_000 });
|
||||
return;
|
||||
return intervention;
|
||||
} catch (error) {
|
||||
if (attempt === 3) throw error;
|
||||
await delay(500);
|
||||
@@ -123,6 +134,40 @@ async function waitForInterventionCard(page, room, podId) {
|
||||
}
|
||||
}
|
||||
|
||||
function liveKitService() {
|
||||
if (!process.env.LIVEKIT_URL || !process.env.LIVEKIT_API_KEY || !process.env.LIVEKIT_API_SECRET) {
|
||||
throw new Error('screen publication verification requires LIVEKIT_* env vars');
|
||||
}
|
||||
const httpUrl = process.env.LIVEKIT_URL.replace(/^wss:/, 'https:').replace(/^ws:/, 'http:');
|
||||
return new RoomServiceClient(
|
||||
httpUrl,
|
||||
process.env.LIVEKIT_API_KEY,
|
||||
process.env.LIVEKIT_API_SECRET,
|
||||
);
|
||||
}
|
||||
|
||||
function hasScreenShareTrack(participant) {
|
||||
return (participant.tracks ?? []).some((track) => {
|
||||
const source = JSON.stringify(track).toLowerCase();
|
||||
return source.includes('screen') || source.includes('share');
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForPublishedScreenShare(roomName) {
|
||||
const service = liveKitService();
|
||||
let lastParticipants = [];
|
||||
for (let i = 0; i < 30; i++) {
|
||||
lastParticipants = await service.listParticipants(roomName);
|
||||
if (lastParticipants.some(hasScreenShareTrack)) return;
|
||||
await delay(500);
|
||||
}
|
||||
throw new Error(
|
||||
`LiveKit room service did not list a screen-share publication in ${roomName}: ${JSON.stringify(
|
||||
lastParticipants,
|
||||
).slice(0, 1000)}`,
|
||||
);
|
||||
}
|
||||
|
||||
let preview = null;
|
||||
if (shouldStartPreview) {
|
||||
preview = spawn(
|
||||
@@ -139,6 +184,7 @@ if (shouldStartPreview) {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
|
||||
await page.addInitScript(() => {
|
||||
globalThis.__podmanVerifyScreens = [];
|
||||
Object.defineProperty(globalThis.navigator, 'mediaDevices', {
|
||||
configurable: true,
|
||||
value: {
|
||||
@@ -149,12 +195,21 @@ await page.addInitScript(() => {
|
||||
canvas.height = 360;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) throw new Error('canvas context unavailable');
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = '#111';
|
||||
ctx.font = '28px sans-serif';
|
||||
ctx.fillText('PodMan verification screen', 32, 72);
|
||||
return canvas.captureStream(5);
|
||||
let frame = 0;
|
||||
const draw = () => {
|
||||
frame += 1;
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = '#111';
|
||||
ctx.font = '28px sans-serif';
|
||||
ctx.fillText('PodMan verification screen', 32, 72);
|
||||
ctx.fillText(`frame ${frame}`, 32, 120);
|
||||
};
|
||||
draw();
|
||||
const interval = globalThis.setInterval(draw, 200);
|
||||
const stream = canvas.captureStream(5);
|
||||
globalThis.__podmanVerifyScreens.push({ canvas, stream, interval });
|
||||
return stream;
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -217,12 +272,37 @@ try {
|
||||
await page.getByRole('button', { name: 'Share screen' }).click();
|
||||
await page.getByRole('button', { name: 'Stop sharing' }).waitFor({ timeout: 15_000 });
|
||||
await page.getByText(/Screen\s*published/i).waitFor({ timeout: 15_000 });
|
||||
await waitForPublishedScreenShare('frontend-pod');
|
||||
await page.getByRole('button', { name: 'Stop sharing' }).click();
|
||||
await page.getByRole('button', { name: 'Share screen' }).waitFor({ timeout: 15_000 });
|
||||
|
||||
const publisher = await connectPublisher('frontend-pod');
|
||||
try {
|
||||
await waitForInterventionCard(page, publisher, 'frontend-pod');
|
||||
const intervention = await waitForInterventionCard(page, publisher, 'frontend-pod');
|
||||
await publishDataMessage(publisher, {
|
||||
type: 'HERMES_MESSAGE',
|
||||
message: {
|
||||
id: `hermes-${process.pid}`,
|
||||
podId: 'frontend-pod',
|
||||
interventionId: intervention.id,
|
||||
recipients: ['Verify'],
|
||||
text: 'Hermes verification message routed to the team.',
|
||||
urgency: 'normal',
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
await publishDataMessage(publisher, {
|
||||
type: 'VOICE_CUE',
|
||||
text: 'Voice cue verification for urgent escalation.',
|
||||
});
|
||||
await page.getByText('Hermes message').waitFor({ timeout: 15_000 });
|
||||
await page.getByText('Hermes verification message routed to the team.').waitFor({
|
||||
timeout: 15_000,
|
||||
});
|
||||
await page.getByText('Voice cue', { exact: true }).waitFor({ timeout: 15_000 });
|
||||
await page.getByText('Voice cue verification for urgent escalation.').waitFor({
|
||||
timeout: 15_000,
|
||||
});
|
||||
await page.getByRole('button', { name: 'Dismiss' }).click();
|
||||
await page.getByText('No collision detected').waitFor({ timeout: 15_000 });
|
||||
} finally {
|
||||
@@ -240,11 +320,12 @@ try {
|
||||
{
|
||||
ok: true,
|
||||
frontendUrl,
|
||||
apiBase,
|
||||
bodyLength: bodyText.length,
|
||||
graph: true,
|
||||
joined: true,
|
||||
screenShare: true,
|
||||
intervention: true,
|
||||
screenShare: 'livekit-published',
|
||||
intervention: 'collision-hermes-voice',
|
||||
member: verifyMember,
|
||||
},
|
||||
null,
|
||||
|
||||
Reference in New Issue
Block a user