From c916e4976fa2a287b62a547783f76de19c1dad85 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 25 May 2026 09:37:07 -0700 Subject: [PATCH 1/2] =?UTF-8?q?test(examples-chat):=20pin=20no-nav-loop=20?= =?UTF-8?q?invariant=20in=20URL=E2=86=94signal=20sync?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a regression guard for the invariant every PR in the routing chain (#500/#504/#514/#518/#527) was dancing around: when the URL→ signal effect hydrates threadIdSignal from /embed/, the signal→URL effect MUST see signal === urlState().threadId and short-circuit. Without that guard we'd loop: URL → signal → router.navigate → URL → ... The test asserts zero NavigationEnd events fire between the initial navigateByUrl and the end of detectChanges — proving the compare- and-set guard at demo-shell.component.ts (signal→URL effect) does its job. 33/33 examples-chat-angular unit tests passing locally. --- .../app/shell/demo-shell.component.spec.ts | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/examples/chat/angular/src/app/shell/demo-shell.component.spec.ts b/examples/chat/angular/src/app/shell/demo-shell.component.spec.ts index 86b84efc5..c6d8cef8c 100644 --- a/examples/chat/angular/src/app/shell/demo-shell.component.spec.ts +++ b/examples/chat/angular/src/app/shell/demo-shell.component.spec.ts @@ -1,7 +1,7 @@ import { signal } from '@angular/core'; -import { describe, it, expect, beforeEach } from 'vitest'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; -import { provideRouter, Router } from '@angular/router'; +import { provideRouter, Router, NavigationEnd } from '@angular/router'; import { LangGraphThreadsAdapter } from '@ngaf/langgraph'; import { DemoShell } from './demo-shell.component'; @@ -215,6 +215,38 @@ describe('DemoShell — URL thread sync', () => { }; expect(cmp.threadIdSignal()).toBe('url-thread'); }); + + it('does not re-navigate when hydrating from URL (no nav-loop)', async () => { + // Regression guard for the URL↔signal sync invariant that every PR + // in the routing chain (#500/#504/#514/#518/#527) was dancing + // around: when the URL→signal effect hydrates `threadIdSignal` + // from `/embed/`, the subsequent signal→URL effect must see + // signal === urlId and short-circuit (compare-and-set guard). + // Without that guard we'd loop: URL → signal → router.navigate → + // URL again, observable as extra NavigationEnd events. + const router = TestBed.inject(Router); + await router.navigateByUrl('/embed/no-loop-thread'); + + // Subscribe BEFORE createComponent so we capture any NavigationEnd + // events the component's effects might emit. The initial nav above + // already fired before we subscribed, so it doesn't count. + const navEnds: string[] = []; + const sub = router.events.subscribe((e) => { + if (e instanceof NavigationEnd) navEnds.push(e.urlAfterRedirects); + }); + + const fx = TestBed.createComponent(DemoShell); + fx.detectChanges(); + sub.unsubscribe(); + + const cmp = fx.componentInstance as unknown as { + threadIdSignal: { (): string | null }; + }; + expect(cmp.threadIdSignal()).toBe('no-loop-thread'); + // Zero NavigationEnd events — the signal→URL effect short-circuited + // because signal already matched urlState (compare-and-set guard). + expect(navEnds).toEqual([]); + }); }); describe('DemoShell — URL knob hydration', () => { From 49f577f078b6173236f831685f10181d2a2388f8 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 25 May 2026 09:55:47 -0700 Subject: [PATCH 2/2] test: remove unused vi import (broke Angular build) --- .../chat/angular/src/app/shell/demo-shell.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/chat/angular/src/app/shell/demo-shell.component.spec.ts b/examples/chat/angular/src/app/shell/demo-shell.component.spec.ts index c6d8cef8c..1032edc66 100644 --- a/examples/chat/angular/src/app/shell/demo-shell.component.spec.ts +++ b/examples/chat/angular/src/app/shell/demo-shell.component.spec.ts @@ -1,5 +1,5 @@ import { signal } from '@angular/core'; -import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { describe, it, expect, beforeEach } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { provideRouter, Router, NavigationEnd } from '@angular/router'; import { LangGraphThreadsAdapter } from '@ngaf/langgraph';