@@ -13,6 +13,10 @@ import type {
1313 RuntimeConfigResponse ,
1414 RuntimeRoutingConfigResponse ,
1515} from "../../types" ;
16+ import {
17+ usePeopleRoutingController ,
18+ type PeopleRoutingController ,
19+ } from "../peopleRouting/usePeopleRoutingController" ;
1620import { MemoryPage } from "./MemoryPage" ;
1721import { useMemoryController } from "./useMemoryController" ;
1822
@@ -483,6 +487,7 @@ function installDefaultMocks() {
483487let root : Root | null = null ;
484488let container : HTMLDivElement ;
485489let latestController : ReturnType < typeof useMemoryController > | null = null ;
490+ let latestPeopleRouting : PeopleRoutingController | null = null ;
486491const setNotice = vi . fn ( ) as unknown as NotifyFn & ReturnType < typeof vi . fn > ;
487492const onOpenAssistant = vi . fn ( ) ;
488493
@@ -499,6 +504,7 @@ afterEach(async () => {
499504 await act ( async ( ) => root ?. unmount ( ) ) ;
500505 root = null ;
501506 latestController = null ;
507+ latestPeopleRouting = null ;
502508 container . remove ( ) ;
503509} ) ;
504510
@@ -507,22 +513,34 @@ function Harness({
507513 agents,
508514 enabled = true ,
509515 preferredAgentId = null ,
516+ tokenConfigured = true ,
510517} : {
511518 settings : RuntimeConnectionSettings ;
512519 agents : Agent [ ] ;
513520 enabled ?: boolean ;
514521 preferredAgentId ?: string | null ;
522+ tokenConfigured ?: boolean ;
515523} ) {
524+ const peopleRouting = usePeopleRoutingController ( {
525+ settings,
526+ tokenConfigured,
527+ agents,
528+ } ) ;
516529 const controller = useMemoryController ( {
517530 settings,
518531 agents,
519532 enabled,
520533 preferredAgentId,
534+ tokenConfigured,
535+ peopleRouting,
521536 setNotice,
522537 } ) ;
523538 useEffect ( ( ) => {
524539 latestController = controller ;
525540 } , [ controller ] ) ;
541+ useEffect ( ( ) => {
542+ latestPeopleRouting = peopleRouting ;
543+ } , [ peopleRouting ] ) ;
526544 return < MemoryPage controller = { controller } onOpenAssistant = { onOpenAssistant } /> ;
527545}
528546
@@ -538,7 +556,8 @@ async function render(
538556 settings : RuntimeConnectionSettings = SETTINGS ,
539557 agents : Agent [ ] = AGENTS ,
540558 enabled = true ,
541- preferredAgentId : string | null = null
559+ preferredAgentId : string | null = null ,
560+ tokenConfigured = true
542561) {
543562 await act ( async ( ) => {
544563 root ??= createRoot ( container ) ;
@@ -548,6 +567,7 @@ async function render(
548567 agents = { agents }
549568 enabled = { enabled }
550569 preferredAgentId = { preferredAgentId }
570+ tokenConfigured = { tokenConfigured }
551571 />
552572 ) ;
553573 } ) ;
@@ -1307,6 +1327,185 @@ describe("MemoryPage runtime defaults, lane policy, and sync mutations", () => {
13071327 } ) ;
13081328} ) ;
13091329
1330+ describe ( "MemoryPage configured-auth gating" , ( ) => {
1331+ it ( "waits for configured authentication before reading anything" , async ( ) => {
1332+ await render ( SETTINGS , AGENTS , true , null , false ) ;
1333+
1334+ expect ( apiMocks . getAgentMemoryStatus ) . not . toHaveBeenCalled ( ) ;
1335+ expect ( apiMocks . getRuntimeConfig ) . not . toHaveBeenCalled ( ) ;
1336+ expect ( apiMocks . getAgentMemoryLaneStatuses ) . not . toHaveBeenCalled ( ) ;
1337+ expect ( container . textContent ) . toContain ( "Waiting for the gateway connection." ) ;
1338+ } ) ;
1339+
1340+ it ( "starts reading only when authentication becomes configured" , async ( ) => {
1341+ await render ( SETTINGS , AGENTS , true , null , false ) ;
1342+ expect ( apiMocks . getAgentMemoryStatus ) . not . toHaveBeenCalled ( ) ;
1343+
1344+ await render ( SETTINGS , AGENTS , true , null , true ) ;
1345+
1346+ expect ( apiMocks . getAgentMemoryStatus ) . toHaveBeenCalledWith ( SETTINGS , "lyra" ) ;
1347+ await clickButton ( "Library" ) ;
1348+ expect ( container . textContent ) . toContain ( "Lyra fact one" ) ;
1349+ } ) ;
1350+
1351+ it ( "discards an in-flight read when authentication is torn down" , async ( ) => {
1352+ const slowStatus = deferred < AgentMemoryStatusResponse > ( ) ;
1353+ apiMocks . getAgentMemoryStatus . mockImplementation ( async ( ) => slowStatus . promise ) ;
1354+ await render ( SETTINGS , AGENTS , true , null , true ) ;
1355+
1356+ await render ( SETTINGS , AGENTS , true , null , false ) ;
1357+ slowStatus . resolve ( makeStatus ( "lyra" ) ) ;
1358+ await flush ( ) ;
1359+
1360+ expect ( container . textContent ) . toContain ( "Waiting for the gateway connection." ) ;
1361+ expect ( apiMocks . listAgentMemoryCards ) . not . toHaveBeenCalled ( ) ;
1362+ } ) ;
1363+
1364+ it ( "refuses every mutation without configured authentication" , async ( ) => {
1365+ await render ( SETTINGS , AGENTS , true , null , false ) ;
1366+
1367+ expect ( latestController ) . toBeTruthy ( ) ;
1368+ let saved = true ;
1369+ await act ( async ( ) => {
1370+ saved = await latestController ! . saveRuntimeMemoryDefaults ( "mno_primary" , [ ] ) ;
1371+ } ) ;
1372+ expect ( saved ) . toBe ( false ) ;
1373+ let laneSaved = true ;
1374+ await act ( async ( ) => {
1375+ laneSaved = await latestController ! . saveLaneMemoryPolicy (
1376+ "local-operator" ,
1377+ "lyra" ,
1378+ "local_only"
1379+ ) ;
1380+ } ) ;
1381+ expect ( laneSaved ) . toBe ( false ) ;
1382+ let synced = true ;
1383+ await act ( async ( ) => {
1384+ synced = await latestController ! . syncRuntimeMemoryDefaults ( ) ;
1385+ } ) ;
1386+ expect ( synced ) . toBe ( false ) ;
1387+ let laneSynced = true ;
1388+ await act ( async ( ) => {
1389+ laneSynced = await latestController ! . syncLaneMemorySources (
1390+ "local-operator" ,
1391+ "lyra"
1392+ ) ;
1393+ } ) ;
1394+ expect ( laneSynced ) . toBe ( false ) ;
1395+
1396+ expect ( apiMocks . updateRuntimeConfig ) . not . toHaveBeenCalled ( ) ;
1397+ expect ( apiMocks . syncMemorySources ) . not . toHaveBeenCalled ( ) ;
1398+ } ) ;
1399+ } ) ;
1400+
1401+ describe ( "MemoryPage same-tick mutation locks" , ( ) => {
1402+ it ( "lets one synchronous lock own a same-tick double runtime-defaults save" , async ( ) => {
1403+ await render ( ) ;
1404+
1405+ let results : boolean [ ] = [ ] ;
1406+ await act ( async ( ) => {
1407+ results = await Promise . all ( [
1408+ latestController ! . saveRuntimeMemoryDefaults ( "mno_primary" , [ "a.md" ] ) ,
1409+ latestController ! . saveRuntimeMemoryDefaults ( "mno_primary" , [ "a.md" ] ) ,
1410+ ] ) ;
1411+ } ) ;
1412+ await flush ( ) ;
1413+
1414+ expect ( apiMocks . updateRuntimeConfig ) . toHaveBeenCalledTimes ( 1 ) ;
1415+ expect ( results . filter ( Boolean ) ) . toHaveLength ( 1 ) ;
1416+ } ) ;
1417+
1418+ it ( "lets one synchronous lock own a same-tick double lane-policy save" , async ( ) => {
1419+ await render ( ) ;
1420+ apiMocks . updateRuntimeConfig . mockClear ( ) ;
1421+
1422+ let results : boolean [ ] = [ ] ;
1423+ await act ( async ( ) => {
1424+ results = await Promise . all ( [
1425+ latestController ! . saveLaneMemoryPolicy ( "local-operator" , "lyra" , "local_only" ) ,
1426+ latestController ! . saveLaneMemoryPolicy ( "local-operator" , "lyra" , "local_only" ) ,
1427+ ] ) ;
1428+ } ) ;
1429+ await flush ( ) ;
1430+
1431+ expect ( apiMocks . updateRuntimeConfig ) . toHaveBeenCalledTimes ( 1 ) ;
1432+ expect ( results . filter ( Boolean ) ) . toHaveLength ( 1 ) ;
1433+ } ) ;
1434+
1435+ it ( "lets one synchronous lock own a same-tick double runtime sync" , async ( ) => {
1436+ await render ( ) ;
1437+
1438+ await act ( async ( ) => {
1439+ await Promise . all ( [
1440+ latestController ! . syncRuntimeMemoryDefaults ( ) ,
1441+ latestController ! . syncRuntimeMemoryDefaults ( ) ,
1442+ ] ) ;
1443+ } ) ;
1444+ await flush ( ) ;
1445+
1446+ expect ( apiMocks . syncMemorySources ) . toHaveBeenCalledTimes ( 1 ) ;
1447+ } ) ;
1448+
1449+ it ( "lets one synchronous lock own a same-tick double lane sync" , async ( ) => {
1450+ await render ( ) ;
1451+
1452+ await act ( async ( ) => {
1453+ await Promise . all ( [
1454+ latestController ! . syncLaneMemorySources ( "local-operator" , "lyra" ) ,
1455+ latestController ! . syncLaneMemorySources ( "local-operator" , "lyra" ) ,
1456+ ] ) ;
1457+ } ) ;
1458+ await flush ( ) ;
1459+
1460+ expect ( apiMocks . syncMemorySources ) . toHaveBeenCalledTimes ( 1 ) ;
1461+ } ) ;
1462+ } ) ;
1463+
1464+ describe ( "MemoryPage shared People & Routing authority" , ( ) => {
1465+ it ( "lands the lane policy save inside the shared routing authority state" , async ( ) => {
1466+ await render ( ) ;
1467+
1468+ await setLabeledSelect ( "Memory mode" , "local_only" ) ;
1469+ await clickButton ( "Save lane settings" ) ;
1470+
1471+ // The one shared People & Routing controller now holds the saved policy,
1472+ // proving the write flowed through it instead of a second writer.
1473+ const sharedPolicy = latestPeopleRouting ?. routingConfig ?. lane_memory_policies . find (
1474+ ( policy ) =>
1475+ policy . human_identity_id === "local-operator" &&
1476+ policy . assistant_agent_id === "lyra"
1477+ ) ;
1478+ expect ( sharedPolicy ?. memory_mode ) . toBe ( "local_only" ) ;
1479+ } ) ;
1480+
1481+ it ( "refuses a lane policy save while Front Desk holds unsaved routing edits" , async ( ) => {
1482+ await render ( ) ;
1483+ apiMocks . updateRuntimeConfig . mockClear ( ) ;
1484+
1485+ await act ( async ( ) => {
1486+ latestPeopleRouting ! . patchRoutingDraft ( ( draft ) => {
1487+ draft . human_identities [ 0 ] ! . display_name = "Edited At The Front Desk" ;
1488+ } ) ;
1489+ } ) ;
1490+ await flush ( ) ;
1491+
1492+ let saved = true ;
1493+ await act ( async ( ) => {
1494+ saved = await latestController ! . saveLaneMemoryPolicy (
1495+ "local-operator" ,
1496+ "lyra" ,
1497+ "local_only"
1498+ ) ;
1499+ } ) ;
1500+ expect ( saved ) . toBe ( false ) ;
1501+ expect ( apiMocks . updateRuntimeConfig ) . not . toHaveBeenCalled ( ) ;
1502+ expect ( setNotice ) . toHaveBeenCalledWith ( {
1503+ tone : "error" ,
1504+ message : expect . stringContaining ( "unsaved" ) ,
1505+ } ) ;
1506+ } ) ;
1507+ } ) ;
1508+
13101509describe ( "MemoryPage refresh" , ( ) => {
13111510 it ( "reloads memory, routing, and lane statuses on refresh" , async ( ) => {
13121511 await render ( ) ;
0 commit comments