@@ -47,6 +47,13 @@ describe('AshbyBlock', () => {
4747 expect ( result . socialLinks ) . toEqual ( [ { type : 'Twitter' , url : 'https://twitter.com/jane' } ] )
4848 } )
4949
50+ it ( 'preserves an empty JSON array so users can clear all social links' , ( ) => {
51+ const result = AshbyBlock . tools . config . params ! (
52+ buildParams ( 'update_candidate' , { socialLinks : '[]' } )
53+ )
54+ expect ( result . socialLinks ) . toEqual ( [ ] )
55+ } )
56+
5057 it ( 'throws instead of silently dropping the field when the JSON is malformed' , ( ) => {
5158 // A silent [] here would let the Ashby update proceed without applying
5259 // the requested links and with no error shown to the workflow author.
@@ -66,6 +73,43 @@ describe('AshbyBlock', () => {
6673 } )
6774 } )
6875
76+ describe ( 'nullable candidate updates' , ( ) => {
77+ it ( 'preserves nulls from dynamic references for aliased update fields' , ( ) => {
78+ const result = AshbyBlock . tools . config . params ! (
79+ buildParams ( 'update_candidate' , {
80+ updateName : null ,
81+ candidateLocation : null ,
82+ candidateCreatedAt : null ,
83+ } )
84+ )
85+ expect ( result . name ) . toBeNull ( )
86+ expect ( result . location ) . toBeNull ( )
87+ expect ( result . createdAt ) . toBeNull ( )
88+ } )
89+ } )
90+
91+ describe ( 'archiveEmail parsing (change_application_stage)' , ( ) => {
92+ it ( 'parses the documented archive email object' , ( ) => {
93+ const result = AshbyBlock . tools . config . params ! (
94+ buildParams ( 'change_application_stage' , {
95+ archiveEmail : '{"communicationTemplateId":"template-1","sendAt":"2026-09-02T16:32:00Z"}' ,
96+ } )
97+ )
98+ expect ( result . archiveEmail ) . toEqual ( {
99+ communicationTemplateId : 'template-1' ,
100+ sendAt : '2026-09-02T16:32:00Z' ,
101+ } )
102+ } )
103+
104+ it ( 'rejects non-object archive email input' , ( ) => {
105+ expect ( ( ) =>
106+ AshbyBlock . tools . config . params ! (
107+ buildParams ( 'change_application_stage' , { archiveEmail : 'true' } )
108+ )
109+ ) . toThrow ( / e x p e c t e d a J S O N o b j e c t / )
110+ } )
111+ } )
112+
69113 describe ( 'wandConfig on array-shaped fields' , ( ) => {
70114 it ( 'does not request object-wrapped output for alternateEmailAddresses or socialLinks' , ( ) => {
71115 // generationType 'json-object' makes the wand API append "the response
@@ -324,6 +368,57 @@ describe('AshbyBlock', () => {
324368 const condition = syncToken ?. condition as { value : string [ ] }
325369 expect ( condition . value ) . toContain ( 'list_jobs' )
326370 } )
371+
372+ it ( 'maps the editor status selection to the tool array contract' , ( ) => {
373+ const result = AshbyBlock . tools . config . params ! (
374+ buildParams ( 'list_jobs' , { jobStatus : 'Open' } )
375+ )
376+ expect ( result . status ) . toEqual ( [ 'Open' ] )
377+ } )
378+
379+ it ( 'maps the shared draft-posting switch to the endpoint-specific parameter' , ( ) => {
380+ const listParams = AshbyBlock . tools . config . params ! (
381+ buildParams ( 'list_jobs' , { includeUnpublishedJobPostingIds : true } )
382+ )
383+ expect ( listParams . includeUnpublishedJobPostingsIds ) . toBe ( true )
384+ expect ( listParams . includeUnpublishedJobPostingIds ) . toBeUndefined ( )
385+
386+ const infoParams = AshbyBlock . tools . config . params ! (
387+ buildParams ( 'get_job' , { includeUnpublishedJobPostingIds : true } )
388+ )
389+ expect ( infoParams . includeUnpublishedJobPostingIds ) . toBe ( true )
390+ expect ( infoParams . includeUnpublishedJobPostingsIds ) . toBeUndefined ( )
391+ } )
392+ } )
393+
394+ describe ( 'expanded list controls' , ( ) => {
395+ it ( 'exposes only provider-supported pagination and sync controls' , ( ) => {
396+ const cursor = AshbyBlock . subBlocks . find ( ( s ) => s . id === 'cursor' )
397+ const cursorOperations = ( cursor ?. condition as { value : string [ ] } ) . value
398+ expect ( cursorOperations ) . not . toContain ( 'search_candidates' )
399+ expect ( cursorOperations ) . not . toContain ( 'search_jobs' )
400+
401+ const syncToken = AshbyBlock . subBlocks . find ( ( s ) => s . id === 'syncToken' )
402+ const syncOperations = ( syncToken ?. condition as { value : string [ ] } ) . value
403+ expect ( syncOperations ) . toContain ( 'list_application_feedback' )
404+ } )
405+
406+ it ( 'exposes archived interview plans in the editor' , ( ) => {
407+ const includeArchived = AshbyBlock . subBlocks . find ( ( s ) => s . id === 'includeArchived' )
408+ const operations = ( includeArchived ?. condition as { value : string [ ] } ) . value
409+ expect ( operations ) . toContain ( 'list_interview_plans' )
410+ } )
411+ } )
412+
413+ describe ( 'alternate lookup identifiers' , ( ) => {
414+ it ( 'does not require the Ashby UUID when an alternate lookup is supported' , ( ) => {
415+ const candidateId = AshbyBlock . subBlocks . find ( ( s ) => s . id === 'candidateId' )
416+ const applicationId = AshbyBlock . subBlocks . find ( ( s ) => s . id === 'applicationId' )
417+ const candidateRequired = candidateId ?. required as { value ?: string [ ] }
418+ const applicationRequired = applicationId ?. required as { value ?: string [ ] }
419+ expect ( candidateRequired . value ) . not . toContain ( 'get_candidate' )
420+ expect ( applicationRequired . value ) . not . toContain ( 'get_application' )
421+ } )
327422 } )
328423
329424 describe ( 'list_applications candidateId filter' , ( ) => {
0 commit comments