11/**
22 * The queue detail page presents a task queue's name with the `task/` prefix stripped, but
3- * `TaskQueue.name` keeps it — so the name the page hands a watch has to be the stored one,
4- * or every task-queue watch is refused as a missing target.
3+ * `TaskQueue.name` keeps it — and nobody asking for a watch can tell which kind of queue
4+ * they are naming. Creation resolves the name against the environment and rewrites the spec
5+ * to the stored one, so the identity and the depth readers never see the other spelling.
56 */
67
78import {
89 createChat ,
910 createDashboardAgentDb ,
11+ getWatch ,
1012 type DashboardAgentDb ,
1113 type DashboardAgentDbClient ,
1214} from "@internal/dashboard-agent-db" ;
@@ -75,13 +77,14 @@ async function boot(prisma: PrismaClient, connectionUri: string) {
7577afterEach ( async ( ) => {
7678 await agentDbClient ?. close ( ) ;
7779 agentDbClient = undefined ;
80+ readNames . length = 0 ;
7881} ) ;
7982
8083function suffix ( ) {
8184 return Math . random ( ) . toString ( 36 ) . slice ( 2 , 10 ) ;
8285}
8386
84- /** One org, project and production environment, holding one virtual queue for `send-receipt` . */
87+ /** One org, project and production environment, holding a task queue and a custom one . */
8588async function seed ( prisma : PrismaClient ) {
8689 const slug = `queue_name_${ suffix ( ) } ` ;
8790 const user = await prisma . user . create ( {
@@ -116,6 +119,17 @@ async function seed(prisma: PrismaClient) {
116119 runtimeEnvironmentId : environment . id ,
117120 } ,
118121 } ) ;
122+ // A custom queue, stored under the plain name the user gave it.
123+ await prisma . taskQueue . create ( {
124+ data : {
125+ friendlyId : `queue_${ suffix ( ) } ` ,
126+ name : "worker-1" ,
127+ orderableName : "worker-1" ,
128+ type : "NAMED" ,
129+ projectId : project . id ,
130+ runtimeEnvironmentId : environment . id ,
131+ } ,
132+ } ) ;
119133
120134 await createChat ( ctx . agentDb , {
121135 id : `chat_${ suffix ( ) } ` ,
@@ -141,37 +155,50 @@ function authenticated(seeded: Seeded) {
141155}
142156
143157/** Only `queueExists` is real: it is the read the target validation is decided by. */
144- function checkDeps ( seeded : Seeded ) : WatchCheckDeps {
158+ function checkDeps ( seeded : Seeded , readNames : string [ ] ) : WatchCheckDeps {
145159 return {
146160 readRun : async ( ) => null ,
147161 queueExists : ( name : string ) => watchQueueExistsOnPrimary ( seeded . environment . id , name ) ,
148162 readQueueDepth : async ( ) => ( { depth : 3 , source : "live_queue" , current : true } ) ,
149- readQueueOldestAge : async ( ) => ( { ageMs : 1_000 , source : "live_queue" , current : true } ) ,
163+ readQueueOldestAge : async ( name : string ) => {
164+ readNames . push ( name ) ;
165+ return { ageMs : 1_000 , source : "live_queue" , current : true } ;
166+ } ,
150167 readErrorRecurrence : async ( ) => null ,
151168 readHealth : async ( ) => null ,
152169 } ;
153170}
154171
155- async function createFor ( seeded : Seeded , spec : WatchSpec ) {
156- const chatId = `chat_${ suffix ( ) } ` ;
157- await createChat ( ctx . agentDb , {
158- id : chatId ,
159- organizationId : seeded . organization . id ,
160- userId : seeded . user . id ,
161- } ) ;
172+ /** The names the depth readers were handed, so a spec left un-rewritten is visible. */
173+ const readNames : string [ ] = [ ] ;
174+
175+ async function createFor ( seeded : Seeded , spec : WatchSpec , chatId ?: string ) {
176+ const id = chatId ?? `chat_${ suffix ( ) } ` ;
177+ if ( ! chatId ) {
178+ await createChat ( ctx . agentDb , {
179+ id,
180+ organizationId : seeded . organization . id ,
181+ userId : seeded . user . id ,
182+ } ) ;
183+ }
162184 return createDashboardAgentWatch ( {
163185 environment : authenticated ( seeded ) ,
164186 userId : seeded . user . id ,
165- chatId,
187+ chatId : id ,
166188 spec,
167189 deps : {
168190 configured : ( ) => true ,
169- checkDeps : ( ) => checkDeps ( seeded ) ,
191+ checkDeps : ( ) => checkDeps ( seeded , readNames ) ,
170192 scheduleTick : async ( ) => { } ,
171193 } ,
172194 } ) ;
173195}
174196
197+ async function persistedQueueName ( created : { watchId ?: string } ) {
198+ const watch = await getWatch ( ctx . agentDb , { id : created . watchId ! } ) ;
199+ return ( watch ?. spec as { queue : string } ) . queue ;
200+ }
201+
175202/** The queue as `QueueRetrievePresenter` hands it to the page: the prefix already stripped. */
176203const PRESENTED = { type : "task" , name : "send-receipt" } ;
177204
@@ -191,13 +218,70 @@ describe("a watch on a task's own queue", () => {
191218 ) ;
192219
193220 postgresTest (
194- "is refused when the display name reaches the spec instead " ,
221+ "is created under the stored name when the display name reaches the spec" ,
195222 async ( { prisma, postgresContainer } ) => {
196223 await boot ( prisma , postgresContainer . getConnectionUri ( ) ) ;
197224 const seeded = await seed ( prisma ) ;
198225
199226 const created = await createFor ( seeded , queueWatchRecommendation ( PRESENTED . name ) ) ;
200- expect ( created ) . toMatchObject ( { ok : false , code : "invalid_target" } ) ;
227+ expect ( created ) . toMatchObject ( { ok : true , watching : true } ) ;
228+ expect ( await persistedQueueName ( created as { watchId : string } ) ) . toBe ( "task/send-receipt" ) ;
229+ expect ( readNames ) . toEqual ( [ "task/send-receipt" ] ) ;
230+ }
231+ ) ;
232+ } ) ;
233+
234+ describe ( "a watch on a custom queue" , ( ) => {
235+ postgresTest (
236+ "is created under the plain name when the model adds the `task/` prefix" ,
237+ async ( { prisma, postgresContainer } ) => {
238+ await boot ( prisma , postgresContainer . getConnectionUri ( ) ) ;
239+ const seeded = await seed ( prisma ) ;
240+
241+ const created = await createFor ( seeded , queueWatchRecommendation ( "task/worker-1" ) ) ;
242+ expect ( created ) . toMatchObject ( { ok : true , watching : true } ) ;
243+ expect ( await persistedQueueName ( created as { watchId : string } ) ) . toBe ( "worker-1" ) ;
244+ expect ( readNames ) . toEqual ( [ "worker-1" ] ) ;
245+ }
246+ ) ;
247+
248+ postgresTest (
249+ "dedupes across both spellings, because the identity sees the stored name" ,
250+ async ( { prisma, postgresContainer } ) => {
251+ await boot ( prisma , postgresContainer . getConnectionUri ( ) ) ;
252+ const seeded = await seed ( prisma ) ;
253+ const chatId = `chat_${ suffix ( ) } ` ;
254+ await createChat ( ctx . agentDb , {
255+ id : chatId ,
256+ organizationId : seeded . organization . id ,
257+ userId : seeded . user . id ,
258+ } ) ;
259+
260+ const first = await createFor ( seeded , queueWatchRecommendation ( "worker-1" ) , chatId ) ;
261+ expect ( first ) . toMatchObject ( { ok : true , watching : true } ) ;
262+
263+ const second = await createFor ( seeded , queueWatchRecommendation ( "task/worker-1" ) , chatId ) ;
264+ expect ( second ) . toMatchObject ( {
265+ ok : false ,
266+ code : "duplicate" ,
267+ existingId : ( first as { watchId : string } ) . watchId ,
268+ } ) ;
201269 }
202270 ) ;
203271} ) ;
272+
273+ describe ( "a queue that exists under neither spelling" , ( ) => {
274+ postgresTest ( "is refused, either way round" , async ( { prisma, postgresContainer } ) => {
275+ await boot ( prisma , postgresContainer . getConnectionUri ( ) ) ;
276+ const seeded = await seed ( prisma ) ;
277+
278+ expect ( await createFor ( seeded , queueWatchRecommendation ( "worker-9" ) ) ) . toMatchObject ( {
279+ ok : false ,
280+ code : "invalid_target" ,
281+ } ) ;
282+ expect ( await createFor ( seeded , queueWatchRecommendation ( "task/worker-9" ) ) ) . toMatchObject ( {
283+ ok : false ,
284+ code : "invalid_target" ,
285+ } ) ;
286+ } ) ;
287+ } ) ;
0 commit comments