Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion examples/chat/angular/src/app/shell/demo-shell.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { signal } from '@angular/core';
import { describe, it, expect, beforeEach } 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';

Expand Down Expand Up @@ -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/<id>`, 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', () => {
Expand Down
Loading