@@ -16,6 +16,9 @@ interface PapiBackendTestMock {
1616 __mockGetOpenWebViewDefinition : jest . Mock ;
1717 __mockOnDidOpenWebView : jest . Mock ;
1818 __mockOnDidCloseWebView : jest . Mock ;
19+ __mockReadUserData : jest . Mock ;
20+ __mockWriteUserData : jest . Mock ;
21+ __mockNotificationsSend : jest . Mock ;
1922 __mockLogger : { debug : jest . Mock ; error : jest . Mock ; info : jest . Mock ; warn : jest . Mock } ;
2023}
2124
@@ -34,6 +37,9 @@ function isPapiBackendTestMock(m: unknown): m is PapiBackendTestMock {
3437 '__mockGetOpenWebViewDefinition' in m &&
3538 '__mockOnDidOpenWebView' in m &&
3639 '__mockOnDidCloseWebView' in m &&
40+ '__mockReadUserData' in m &&
41+ '__mockWriteUserData' in m &&
42+ '__mockNotificationsSend' in m &&
3743 '__mockLogger' in m
3844 ) ;
3945}
@@ -47,6 +53,9 @@ const {
4753 __mockGetOpenWebViewDefinition,
4854 __mockOnDidOpenWebView,
4955 __mockOnDidCloseWebView,
56+ __mockReadUserData,
57+ __mockWriteUserData,
58+ __mockNotificationsSend,
5059 __mockLogger,
5160} = papiBackendMock ;
5261
@@ -63,6 +72,12 @@ describe('main', () => {
6372 __mockGetOpenWebViewDefinition . mockResolvedValue ( undefined ) ;
6473 __mockOnDidOpenWebView . mockReturnValue ( jest . fn ( ) ) ;
6574 __mockOnDidCloseWebView . mockReturnValue ( jest . fn ( ) ) ;
75+ __mockReadUserData . mockRejectedValue (
76+ Object . assign ( new Error ( 'ENOENT: no such file or directory' ) , { code : 'ENOENT' } ) ,
77+ ) ;
78+ __mockWriteUserData . mockResolvedValue ( undefined ) ;
79+ __mockNotificationsSend . mockResolvedValue ( 'mock-notification-id' ) ;
80+ jest . spyOn ( crypto , 'randomUUID' ) . mockReturnValue ( '00000000-0000-0000-0000-000000000000' ) ;
6681 } ) ;
6782
6883 describe ( 'activate' , ( ) => {
@@ -92,12 +107,12 @@ describe('main', () => {
92107 ) ;
93108 } ) ;
94109
95- it ( 'adds all four registrations to the activation context' , async ( ) => {
110+ it ( 'adds all five registrations to the activation context' , async ( ) => {
96111 const context = createTestActivationContext ( ) ;
97112
98113 await activate ( context ) ;
99114
100- expect ( context . registrations . unsubscribers . size ) . toBe ( 4 ) ;
115+ expect ( context . registrations . unsubscribers . size ) . toBe ( 5 ) ;
101116 } ) ;
102117
103118 it ( 'logs activation start and finish' , async ( ) => {
@@ -330,6 +345,84 @@ describe('main', () => {
330345 } ) ;
331346 } ) ;
332347
348+ describe ( 'interlinearizer.createProject command' , ( ) => {
349+ async function getCreateProjectHandler ( ) : Promise <
350+ ( analysisWritingSystem : string ) => Promise < string | undefined >
351+ > {
352+ const context = createTestActivationContext ( ) ;
353+ await activate ( context ) ;
354+ const rawHandler = findRegisteredHandler ( 'interlinearizer.createProject' ) ;
355+ if ( ! rawHandler ) throw new Error ( 'Handler not found for interlinearizer.createProject' ) ;
356+ return async ( ws : string ) : Promise < string | undefined > => {
357+ const result : unknown = await rawHandler ( ws ) ;
358+ return typeof result === 'string' ? result : undefined ;
359+ } ;
360+ }
361+
362+ it ( 'registers the interlinearizer.createProject command' , async ( ) => {
363+ const context = createTestActivationContext ( ) ;
364+
365+ await activate ( context ) ;
366+
367+ expect ( __mockRegisterCommand ) . toHaveBeenCalledWith (
368+ 'interlinearizer.createProject' ,
369+ expect . any ( Function ) ,
370+ expect . any ( Object ) ,
371+ ) ;
372+ } ) ;
373+
374+ it ( 'creates and returns the new project ID when both pickers are confirmed' , async ( ) => {
375+ __mockSelectProject . mockResolvedValueOnce ( 'src-project' ) . mockResolvedValueOnce ( 'tgt-project' ) ;
376+ const handler = await getCreateProjectHandler ( ) ;
377+
378+ const result = await handler ( 'en' ) ;
379+
380+ expect ( result ) . toBe ( '00000000-0000-0000-0000-000000000000' ) ;
381+ expect ( __mockWriteUserData ) . toHaveBeenCalledWith (
382+ expect . anything ( ) ,
383+ 'project:00000000-0000-0000-0000-000000000000' ,
384+ expect . stringContaining ( '"sourceProjectId":"src-project"' ) ,
385+ ) ;
386+ } ) ;
387+
388+ it ( 'returns undefined when the source picker is cancelled' , async ( ) => {
389+ __mockSelectProject . mockResolvedValue ( undefined ) ;
390+ const handler = await getCreateProjectHandler ( ) ;
391+
392+ const result = await handler ( 'en' ) ;
393+
394+ expect ( result ) . toBeUndefined ( ) ;
395+ expect ( __mockWriteUserData ) . not . toHaveBeenCalled ( ) ;
396+ } ) ;
397+
398+ it ( 'returns undefined when the target picker is cancelled' , async ( ) => {
399+ __mockSelectProject . mockResolvedValueOnce ( 'src-project' ) . mockResolvedValueOnce ( undefined ) ;
400+ const handler = await getCreateProjectHandler ( ) ;
401+
402+ const result = await handler ( 'en' ) ;
403+
404+ expect ( result ) . toBeUndefined ( ) ;
405+ expect ( __mockWriteUserData ) . not . toHaveBeenCalled ( ) ;
406+ } ) ;
407+
408+ it ( 'logs the error, sends an error notification, and returns undefined when storage fails' , async ( ) => {
409+ __mockSelectProject . mockResolvedValueOnce ( 'src-project' ) . mockResolvedValueOnce ( 'tgt-project' ) ;
410+ __mockWriteUserData . mockRejectedValue ( new Error ( 'disk full' ) ) ;
411+ const handler = await getCreateProjectHandler ( ) ;
412+
413+ const result = await handler ( 'en' ) ;
414+
415+ expect ( result ) . toBeUndefined ( ) ;
416+ expect ( __mockLogger . error ) . toHaveBeenCalledWith (
417+ 'Interlinearizer: failed to create project' ,
418+ expect . any ( Error ) ,
419+ ) ;
420+ expect ( __mockNotificationsSend ) . toHaveBeenCalledWith (
421+ expect . objectContaining ( { severity : 'error' } ) ,
422+ ) ;
423+ } ) ;
424+ } ) ;
425+
333426 describe ( 'WebView lifecycle event subscriptions' , ( ) => {
334427 it ( 'subscribes to onDidOpenWebView and onDidCloseWebView during activation' , async ( ) => {
335428 const context = createTestActivationContext ( ) ;
0 commit comments