diff --git a/README.md b/README.md index 12f3a7a1e..fa1226d65 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ Joltrin (formerly SOP) is an open-source Go library that checks an AI agent's ac **What Joltrin does.** The agent emits intent. Joltrin checks the intent against the trace and the runbook's rules, outside the agent's context window. The check and the commit are one call, so a step is recorded only if it passed. A step whose required state is missing is refused. A claim that something happened is not evidence that it happened.

- - Joltrin demo. Click to watch the video. + + A 43 second silent demo: an agent proposes dropping a production database, Joltrin blocks it, the agent runs the missing steps, and the same action is then allowed. Click to play.

diff --git a/demo/index.html b/demo/index.html index 8b8330c5f..cf58b420a 100644 --- a/demo/index.html +++ b/demo/index.html @@ -477,29 +477,29 @@

-
// db-maintenance runbook
-
-
drop_prod_db BLOCKED backup_validated missing
-
validate_backup BLOCKED backup_taken missing
-
take_backup ALLOWED
-
validate_backup ALLOWED
-
drop_prod_db ALLOWED
+
+
+ +
-

The order is enforced by the barrier, whatever the agent says it already did.

-
- - - - -
-
- +
+ 43 seconds, silent, real output from sop-mcp-server demo. +
+ Text version +
    +
  1. drop_prod_db BLOCKED, backup_validated is missing from the trace
  2. +
  3. take_backup ALLOWED
  4. +
  5. validate_backup ALLOWED
  6. +
  7. drop_prod_db ALLOWED, the evidence now exists
  8. +
+
+
+
@@ -1763,12 +1763,39 @@

Open Source &a + + diff --git a/tests/homepage.spec.ts b/tests/homepage.spec.ts index 279c7e8a3..e9fde4321 100644 --- a/tests/homepage.spec.ts +++ b/tests/homepage.spec.ts @@ -26,9 +26,8 @@ test.describe('Homepage', () => { test('hero shows the blocked then allowed sequence', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded' }); const ex = page.locator('#hero-example'); - await expect(ex).toContainText('backup_validated missing'); - await expect(ex).toContainText('backup_taken missing'); - await expect(ex.getByText('BLOCKED')).toHaveCount(2); + await expect(ex).toContainText('backup_validated'); + await expect(ex.getByText('BLOCKED')).toHaveCount(1); await expect(ex.getByText('ALLOWED')).toHaveCount(3); }); @@ -59,13 +58,68 @@ test.describe('Homepage', () => { } }); - test('intro video sits below the hero and autoplays muted', async ({ page }) => { - await page.goto('/', { waitUntil: 'domcontentloaded' }); - const frame = page.locator('iframe[title="Joltrin intro video"]'); - await expect(frame).toHaveAttribute('src', /youtube-nocookie\.com\/embed\/F0jYkBHJluI\?.*autoplay=1.*mute=1/); - const frameY = (await frame.boundingBox())!.y; - const heroY = (await page.getByRole('heading', { level: 1 }).boundingBox())!.y; - expect(frameY).toBeGreaterThan(heroY); + test('the demo video is in the hero, silent, with a poster, and loads nothing until the page has', async ({ page }) => { + // Record what the video element looks like when the document has parsed, + // which is before the page's load event. + await page.addInitScript(() => { + document.addEventListener('DOMContentLoaded', () => { + const v = document.getElementById('demo-video-el'); + (window as any).__videoAtParse = { src: v?.getAttribute('src'), preload: v?.getAttribute('preload') }; + }); + }); + await page.goto('/', { waitUntil: 'load' }); + const video = page.locator('#demo-video video'); + await expect(video).toHaveCount(1); + await expect(video).toHaveJSProperty('muted', true); + await expect(video).toHaveAttribute('playsinline', ''); + await expect(video).toHaveAttribute('preload', 'none'); + await expect(video).toHaveAttribute('poster', /joltrin-barrier-poster-540\.jpg/); + await expect(video).toHaveAttribute('aria-label', /43 second demo/); + const atParse = await page.evaluate(() => (window as any).__videoAtParse); + expect(atParse, 'the video must not have a source when the document is parsed').toEqual({ src: null, preload: 'none' }); + // It is part of the hero: above the first content section, not buried below it. + const videoY = (await video.boundingBox())!.y; + const barrierY = (await page.locator('#verification-barrier').boundingBox())!.y; + expect(videoY).toBeLessThan(barrierY); + }); + + test('the demo video plays when it is on screen and has a working Pause and Play button', async ({ page }) => { + await page.goto('/', { waitUntil: 'load' }); + const video = page.locator('#demo-video-el'); + const toggle = page.locator('#demo-video-toggle'); + // On a phone the video sits below the headline, so it starts once scrolled to. + await video.scrollIntoViewIfNeeded(); + await expect.poll(() => video.evaluate((v: HTMLVideoElement) => !v.paused), { timeout: 15000 }).toBe(true); + await expect(toggle).toContainText('Pause'); + await toggle.click(); + await expect.poll(() => video.evaluate((v: HTMLVideoElement) => v.paused)).toBe(true); + await expect(toggle).toContainText('Play'); + await expect(toggle).toHaveAttribute('aria-label', /Play the demo video/); + }); + + test('a visitor who prefers reduced motion gets the poster and no autoplay', async ({ page }) => { + await page.emulateMedia({ reducedMotion: 'reduce' }); + await page.goto('/', { waitUntil: 'load' }); + await page.waitForTimeout(1500); + const video = page.locator('#demo-video-el'); + await expect(video).toHaveJSProperty('paused', true); + expect(await video.getAttribute('src')).toBeNull(); + // They can still start it. + await page.locator('#demo-video-toggle').click(); + await expect.poll(() => video.evaluate((v: HTMLVideoElement) => !v.paused), { timeout: 15000 }).toBe(true); + }); + + test('the video files and posters the page names all exist and are small', async ({ request }) => { + const sizes: Record = { + '/assets/joltrin-barrier.mp4': 2_500_000, + '/assets/joltrin-barrier-720.mp4': 1_500_000, + '/assets/joltrin-barrier-poster-540.jpg': 80_000, + }; + for (const [url, max] of Object.entries(sizes)) { + const res = await request.get(url); + expect(res.status(), url).toBe(200); + expect((await res.body()).length, `${url} should stay under ${max} bytes`).toBeLessThan(max); + } }); test('header has a Watch demo button that jumps to the video', async ({ page }) => { @@ -73,7 +127,7 @@ test.describe('Homepage', () => { const btn = page.locator('#watch-demo-btn'); await expect(btn).toBeVisible(); await expect(btn).toHaveAttribute('href', '#demo-video'); - await expect(page.locator('#demo-video iframe')).toHaveCount(1); + await expect(page.locator('#demo-video video')).toHaveCount(1); }); test('explains how to test the barrier with your own AI agent', async ({ page }) => {