11import { readFileSync } from "node:fs" ;
22import { describe , expect , it } from "vitest" ;
3- import { navigateIntentApplies } from "./turn-navigation" ;
3+ import { navigateIntentApplies , takeNavigateIntent } from "./turn-navigation" ;
44
55const runs = "/orgs/acme/projects/api/env/prod/runs" ;
66const queues = "/orgs/acme/projects/api/env/prod/queues" ;
@@ -20,6 +20,62 @@ describe("navigateIntentApplies", () => {
2020 } ) ;
2121} ) ;
2222
23+ describe ( "takeNavigateIntent" , ( ) => {
24+ const target = "trigger://proj_abc/env_123/run/run_abc" ;
25+
26+ function messages ( ) {
27+ return [
28+ {
29+ id : "msg_1" ,
30+ parts : [
31+ {
32+ type : "tool-navigate_to" ,
33+ state : "output-available" ,
34+ toolCallId : "call_1" ,
35+ output : { intent : { kind : "navigate" , target } } ,
36+ } ,
37+ ] ,
38+ } ,
39+ ] ;
40+ }
41+
42+ it ( "takes the navigation on the page the turn was asked for" , ( ) => {
43+ const taken = takeNavigateIntent ( {
44+ messages : messages ( ) ,
45+ handled : new Set ( ) ,
46+ startedPath : runs ,
47+ currentPath : runs ,
48+ } ) ;
49+ expect ( taken ) . toMatchObject ( { kind : "navigate" , target } ) ;
50+ } ) ;
51+
52+ it ( "takes nothing once the user has walked to another screen" , ( ) => {
53+ expect (
54+ takeNavigateIntent ( {
55+ messages : messages ( ) ,
56+ handled : new Set ( ) ,
57+ startedPath : runs ,
58+ currentPath : queues ,
59+ } )
60+ ) . toBeUndefined ( ) ;
61+ } ) ;
62+
63+ // The property the panel depends on: a commit that drops a navigation still consumes it, so
64+ // walking back to the page it was asked on does not make it fire late.
65+ it ( "marks a dropped navigation handled, so a later commit cannot fire it" , ( ) => {
66+ const handled = new Set < string > ( ) ;
67+ const parts = messages ( ) ;
68+
69+ expect (
70+ takeNavigateIntent ( { messages : parts , handled, startedPath : runs , currentPath : queues } )
71+ ) . toBeUndefined ( ) ;
72+
73+ expect (
74+ takeNavigateIntent ( { messages : parts , handled, startedPath : runs , currentPath : runs } )
75+ ) . toBeUndefined ( ) ;
76+ } ) ;
77+ } ) ;
78+
2379/**
2480 * Structural guards, not behavioural proof: whether the started-at path is still right when the
2581 * intent lands depends on effect order and on nothing clearing it, which these assertions pin
@@ -29,9 +85,9 @@ describe("the chat scopes a turn's navigation to the page it started on", () =>
2985 const chat = readFileSync ( new URL ( "./DashboardAgentChat.tsx" , import . meta. url ) , "utf8" ) ;
3086
3187 it ( "gates the navigate intent on the shared rule" , ( ) => {
32- expect ( chat ) . toContain ( "navigateIntentApplies ({" ) ;
88+ expect ( chat ) . toContain ( "takeNavigateIntent ({" ) ;
3389 expect ( chat ) . toContain ( "startedPath: turnStartedPathRef.current" ) ;
34- expect ( chat ) . not . toContain ( "if (target) void goTo(target); " ) ;
90+ expect ( chat ) . not . toContain ( "pendingNavigateIntents(messages " ) ;
3591 } ) ;
3692
3793 it ( "records the path only as a turn goes in flight" , ( ) => {
@@ -47,14 +103,11 @@ describe("the chat scopes a turn's navigation to the page it started on", () =>
47103
48104 it ( "records the path before the intent effect reads it" , ( ) => {
49105 expect ( chat . indexOf ( "turnStartedPathRef.current = renderedPathRef.current" ) ) . toBeLessThan (
50- chat . indexOf ( "navigateIntentApplies ({" )
106+ chat . indexOf ( "takeNavigateIntent ({" )
51107 ) ;
52108 } ) ;
53109
54- it ( "marks a dropped navigation handled, so it cannot fire on a later commit" , ( ) => {
55- const effect = chat . slice ( chat . indexOf ( "const pending = pendingNavigateIntents(messages" ) ) ;
56- expect ( effect . indexOf ( "pendingNavigateIntents(messages" ) ) . toBeLessThan (
57- effect . indexOf ( "navigateIntentApplies({" )
58- ) ;
110+ it ( "hands the persistent handled-set in, so drops are recorded across commits" , ( ) => {
111+ expect ( chat ) . toContain ( "handled: navigatedRef.current!" ) ;
59112 } ) ;
60113} ) ;
0 commit comments