11import "../index.css" ;
22
33import {
4+ DEFAULT_SERVER_SETTINGS_VIEW ,
45 DEVICE_WS_METHODS ,
6+ EventId ,
57 ORCHESTRATION_WS_METHODS ,
68 type MessageId ,
79 type OrchestrationReadModel ,
810 type ProjectId ,
911 type ServerConfig ,
12+ type ServerSettingsView ,
1013 type ThreadId ,
14+ TurnId ,
1115 type WsWelcomePayload ,
1216 WS_METHODS ,
1317} from "@forkara/contracts" ;
1418import { RouterProvider , createMemoryHistory } from "@tanstack/react-router" ;
1519import { ws , http , HttpResponse } from "msw" ;
1620import { setupWorker } from "msw/browser" ;
1721import { afterAll , afterEach , beforeAll , beforeEach , describe , expect , it , vi } from "vitest" ;
22+ import { page } from "vitest/browser" ;
1823import { render } from "vitest-browser-react" ;
1924
2025import { useComposerDraftStore } from "../composerDraftStore" ;
@@ -30,6 +35,9 @@ import {
3035} from "../test/effectRpcWebSocketMock" ;
3136import { createBrowserTestServerConfig , createFullscreenTestHost } from "../test/browserHarness" ;
3237import { resetWsNativeApiForTest } from "../wsNativeApi" ;
38+ import { readNativeApi } from "../nativeApi" ;
39+ import { getAchievementSnapshot , resetAchievementState } from "../achievements/engine" ;
40+ import { BULLY_MODE_CAPTURE_ACTIVITY_KIND } from "../achievements/bullyMode" ;
3341
3442const THREAD_ID = "thread-kb-toast-test" as ThreadId ;
3543const PROJECT_ID = "project-1" as ProjectId ;
@@ -38,6 +46,7 @@ const NOW_ISO = "2026-03-04T12:00:00.000Z";
3846interface TestFixture {
3947 snapshot : OrchestrationReadModel ;
4048 serverConfig : ServerConfig ;
49+ serverSettings : ServerSettingsView ;
4150 welcome : WsWelcomePayload ;
4251}
4352
@@ -124,6 +133,7 @@ function buildFixture(): TestFixture {
124133 return {
125134 snapshot : createMinimalSnapshot ( ) ,
126135 serverConfig : createBaseServerConfig ( ) ,
136+ serverSettings : DEFAULT_SERVER_SETTINGS_VIEW ,
127137 welcome : {
128138 cwd : "/repo/project" ,
129139 projectName : "Project" ,
@@ -153,6 +163,9 @@ function resolveWsRpc(tag: string): unknown {
153163 if ( tag === WS_METHODS . serverGetConfig ) {
154164 return fixture . serverConfig ;
155165 }
166+ if ( tag === WS_METHODS . serverGetSettings ) {
167+ return fixture . serverSettings ;
168+ }
156169 if ( tag === WS_METHODS . projectsListDevServers ) {
157170 return { servers : [ ] } ;
158171 }
@@ -303,7 +316,9 @@ async function mountApp(): Promise<{ cleanup: () => Promise<void> }> {
303316
304317 const router = getRouter ( createMemoryHistory ( { initialEntries : [ `/${ THREAD_ID } ` ] } ) ) ;
305318
306- const screen = await render ( < RouterProvider router = { router } /> , { container : host } ) ;
319+ const screen = await render ( < RouterProvider router = { router } /> , {
320+ container : host ,
321+ } ) ;
307322 try {
308323 await vi . waitFor (
309324 ( ) => {
@@ -329,6 +344,65 @@ async function mountApp(): Promise<{ cleanup: () => Promise<void> }> {
329344 } ;
330345}
331346
347+ function installPersonalitySendBoundary ( input ?: { rejectTurnStart ?: boolean } ) : {
348+ readonly commands : unknown [ ] ;
349+ readonly restore : ( ) => void ;
350+ } {
351+ const previousNativeApi = window . nativeApi ;
352+ const api = readNativeApi ( ) ;
353+ if ( ! api ) throw new Error ( "Expected the browser native API fixture." ) ;
354+ const commands : unknown [ ] = [ ] ;
355+ Object . defineProperty ( window , "nativeApi" , {
356+ configurable : true ,
357+ value : {
358+ ...api ,
359+ orchestration : {
360+ ...api . orchestration ,
361+ dispatchCommand : async (
362+ command : Parameters < typeof api . orchestration . dispatchCommand > [ 0 ] ,
363+ ) => {
364+ commands . push ( command ) ;
365+ if ( input ?. rejectTurnStart && command . type === "thread.turn.start" ) {
366+ throw new Error ( "Recoverable provider fixture failure." ) ;
367+ }
368+ return { sequence : fixture . snapshot . snapshotSequence + 1 } ;
369+ } ,
370+ } ,
371+ } ,
372+ } ) ;
373+ return {
374+ commands,
375+ restore : ( ) => {
376+ if ( previousNativeApi ) {
377+ Object . defineProperty ( window , "nativeApi" , {
378+ configurable : true ,
379+ value : previousNativeApi ,
380+ } ) ;
381+ } else {
382+ Reflect . deleteProperty ( window , "nativeApi" ) ;
383+ }
384+ } ,
385+ } ;
386+ }
387+
388+ async function waitForPersonalityComposer ( ) : Promise < {
389+ readonly editor : HTMLElement ;
390+ readonly sendButton : HTMLButtonElement ;
391+ } > {
392+ let editor : HTMLElement | null = null ;
393+ let sendButton : HTMLButtonElement | null = null ;
394+ await vi . waitFor (
395+ ( ) => {
396+ editor = document . querySelector < HTMLElement > ( '[contenteditable="true"]' ) ;
397+ sendButton = document . querySelector < HTMLButtonElement > ( 'button[aria-label="Send message"]' ) ;
398+ expect ( editor ) . not . toBeNull ( ) ;
399+ expect ( sendButton ) . not . toBeNull ( ) ;
400+ } ,
401+ { timeout : 20_000 , interval : 16 } ,
402+ ) ;
403+ return { editor : editor ! , sendButton : sendButton ! } ;
404+ }
405+
332406describe ( "Keybindings update toast" , ( ) => {
333407 beforeAll ( async ( ) => {
334408 fixture = buildFixture ( ) ;
@@ -378,6 +452,106 @@ describe("Keybindings update toast", () => {
378452 document . body . innerHTML = "" ;
379453 } ) ;
380454
455+ it ( "[personality-smoke] sends Bully Mode and per-turn precision through the full app" , async ( ) => {
456+ fixture = buildFixture ( ) ;
457+ resetAchievementState ( ) ;
458+ fixture . serverSettings = {
459+ ...fixture . serverSettings ,
460+ bullyModeEnabled : true ,
461+ } ;
462+ const completedTurnId = TurnId . makeUnsafe ( "turn-personality-bully-completed" ) ;
463+ fixture . snapshot = {
464+ ...fixture . snapshot ,
465+ threads : fixture . snapshot . threads . map ( ( thread ) =>
466+ Object . assign ( { } , thread , {
467+ activities : [
468+ {
469+ id : EventId . makeUnsafe ( "activity-personality-bully-capture" ) ,
470+ createdAt : NOW_ISO ,
471+ kind : BULLY_MODE_CAPTURE_ACTIVITY_KIND ,
472+ summary : "Bully Mode captured" ,
473+ tone : "info" as const ,
474+ turnId : completedTurnId ,
475+ payload : { bullyModeEnabled : true } ,
476+ } ,
477+ {
478+ id : EventId . makeUnsafe ( "activity-personality-bully-completed" ) ,
479+ createdAt : NOW_ISO ,
480+ kind : "turn.completed" ,
481+ summary : "Turn completed" ,
482+ tone : "info" as const ,
483+ turnId : completedTurnId ,
484+ payload : { state : "completed" } ,
485+ } ,
486+ ] ,
487+ } ) ,
488+ ) ,
489+ } ;
490+ const boundary = installPersonalitySendBoundary ( ) ;
491+ const app = await mountApp ( ) ;
492+
493+ try {
494+ await expect
495+ . element ( page . getByRole ( "button" , { name : "Disable Bully Mode" } ) )
496+ . toBeInTheDocument ( ) ;
497+ await page . getByRole ( "button" , { name : / M a k e N o M i s t a k e i s o f f / u } ) . click ( ) ;
498+ await page . getByRole ( "button" , { name : / l e v e l 1 o f 3 / u } ) . click ( ) ;
499+ await page . getByRole ( "button" , { name : / l e v e l 2 o f 3 / u } ) . click ( ) ;
500+ const { sendButton } = await waitForPersonalityComposer ( ) ;
501+ useComposerDraftStore . getState ( ) . setPrompt ( THREAD_ID , "Exercise personality composition" ) ;
502+ await vi . waitFor ( ( ) => expect ( sendButton . disabled ) . toBe ( false ) ) ;
503+ sendButton . click ( ) ;
504+
505+ await vi . waitFor ( ( ) => {
506+ const turnStart = boundary . commands . find (
507+ ( command ) =>
508+ command !== null &&
509+ typeof command === "object" &&
510+ "type" in command &&
511+ command . type === "thread.turn.start" ,
512+ ) as { responseModifiers ?: unknown } | undefined ;
513+ expect ( turnStart ?. responseModifiers ) . toEqual ( { makeNoMistakeLevel : 3 } ) ;
514+ } ) ;
515+ await expect
516+ . element ( page . getByRole ( "button" , { name : / M a k e N o M i s t a k e i s o f f / u } ) )
517+ . toBeInTheDocument ( ) ;
518+ expect ( getAchievementSnapshot ( ) . some ( ( unlock ) => unlock . id === "dirt_in_your_eye" ) ) . toBe (
519+ true ,
520+ ) ;
521+ } finally {
522+ boundary . restore ( ) ;
523+ resetAchievementState ( ) ;
524+ await app . cleanup ( ) ;
525+ }
526+ } ) ;
527+
528+ it ( "[personality-smoke] keeps a recoverable No Forks Given failure inline" , async ( ) => {
529+ fixture = buildFixture ( ) ;
530+ localStorage . setItem (
531+ "synara:app-settings:v1" ,
532+ JSON . stringify ( { noForksGivenModeEnabled : true } ) ,
533+ ) ;
534+ const boundary = installPersonalitySendBoundary ( { rejectTurnStart : true } ) ;
535+ const app = await mountApp ( ) ;
536+
537+ try {
538+ await page . getByRole ( "button" , { name : / M a k e N o M i s t a k e i s o f f / u } ) . click ( ) ;
539+ const { sendButton } = await waitForPersonalityComposer ( ) ;
540+ useComposerDraftStore . getState ( ) . setPrompt ( THREAD_ID , "Trigger recoverable fixture failure" ) ;
541+ await vi . waitFor ( ( ) => expect ( sendButton . disabled ) . toBe ( false ) ) ;
542+ sendButton . click ( ) ;
543+
544+ await vi . waitFor ( ( ) => {
545+ expect ( document . body . textContent ) . toContain ( "Recoverable provider fixture failure." ) ;
546+ } ) ;
547+ await expect . element ( page . getByText ( "Make No Mistake · 1" ) ) . toBeVisible ( ) ;
548+ await expect . element ( page . getByRole ( "dialog" ) ) . not . toBeInTheDocument ( ) ;
549+ } finally {
550+ boundary . restore ( ) ;
551+ await app . cleanup ( ) ;
552+ }
553+ } ) ;
554+
381555 it ( "does not show success toasts for passive keybinding reloads" , async ( ) => {
382556 const mounted = await mountApp ( ) ;
383557
@@ -397,7 +571,10 @@ describe("Keybindings update toast", () => {
397571
398572 try {
399573 await sendServerConfigUpdatedPush ( [
400- { kind : "keybindings.malformed-config" , message : "Expected JSON array" } ,
574+ {
575+ kind : "keybindings.malformed-config" ,
576+ message : "Expected JSON array" ,
577+ } ,
401578 ] ) ;
402579 await waitForToast ( "Invalid keybindings configuration" ) ;
403580 } finally {
0 commit comments