@theia/workspace 1.70.0-next.7 → 1.70.0-next.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -51,6 +51,10 @@ class TestableWorkspaceTrustService extends WorkspaceTrustService {
51
51
  public async testCalculateWorkspaceTrust(): Promise<boolean | undefined> {
52
52
  return this.calculateWorkspaceTrust();
53
53
  }
54
+
55
+ public testShouldReloadForTrustChange(newTrust: boolean): boolean {
56
+ return this.shouldReloadForTrustChange(newTrust);
57
+ }
54
58
  }
55
59
 
56
60
  describe('WorkspaceTrustService', () => {
@@ -310,7 +314,7 @@ describe('WorkspaceTrustService', () => {
310
314
 
311
315
  beforeEach(() => {
312
316
  areAllWorkspaceUrisTrustedStub = sinon.stub(service as unknown as { areAllWorkspaceUrisTrusted: () => Promise<boolean> }, 'areAllWorkspaceUrisTrusted');
313
- setWorkspaceTrustStub = sinon.stub(service, 'setWorkspaceTrust');
317
+ setWorkspaceTrustStub = sinon.stub(service, 'setWorkspaceTrust').resolves();
314
318
  isEmptyWorkspaceStub = sinon.stub(service as unknown as { isEmptyWorkspace: () => Promise<boolean> }, 'isEmptyWorkspace');
315
319
  // Mock workspaceTrustPref - default emptyWindow to false so trusted folders logic runs
316
320
  workspaceTrustPrefStub = { [WORKSPACE_TRUST_EMPTY_WINDOW]: false };
@@ -386,7 +390,7 @@ describe('WorkspaceTrustService', () => {
386
390
 
387
391
  await service.testHandlePreferenceChange(change);
388
392
 
389
- expect(setWorkspaceTrustStub.calledOnceWith(true)).to.be.true;
393
+ expect(setWorkspaceTrustStub.calledOnceWith(true, false)).to.be.true;
390
394
  });
391
395
 
392
396
  it('should update trust to false when emptyWindow setting changes to false for empty window', async () => {
@@ -401,7 +405,7 @@ describe('WorkspaceTrustService', () => {
401
405
 
402
406
  await service.testHandlePreferenceChange(change);
403
407
 
404
- expect(setWorkspaceTrustStub.calledOnceWith(false)).to.be.true;
408
+ expect(setWorkspaceTrustStub.calledOnceWith(false, false)).to.be.true;
405
409
  });
406
410
 
407
411
  it('should not update trust when emptyWindow setting changes but workspace has roots', async () => {
@@ -437,6 +441,59 @@ describe('WorkspaceTrustService', () => {
437
441
  });
438
442
  });
439
443
 
444
+ describe('workspaceTrustEnabled setting changes', () => {
445
+ let windowServiceStub: { reload: sinon.SinonStub; setSafeToShutDown: sinon.SinonStub };
446
+ let confirmRestartStub: sinon.SinonStub;
447
+
448
+ beforeEach(() => {
449
+ windowServiceStub = { reload: sinon.stub(), setSafeToShutDown: sinon.stub() };
450
+ (service as unknown as { windowService: typeof windowServiceStub }).windowService = windowServiceStub;
451
+ confirmRestartStub = sinon.stub(
452
+ service as unknown as { confirmRestart: () => Promise<boolean> },
453
+ 'confirmRestart'
454
+ );
455
+ sinon.stub(
456
+ service as unknown as { resolveWorkspaceTrust: () => Promise<void> },
457
+ 'resolveWorkspaceTrust'
458
+ ).resolves();
459
+ // Stub isWorkspaceTrustResolved to return true (simulates resolved state)
460
+ sinon.stub(service, 'isWorkspaceTrustResolved').returns(true);
461
+ service.setCurrentTrust(true);
462
+ });
463
+
464
+ it('should reload without setSafeToShutDown when user confirms restart', async () => {
465
+ confirmRestartStub.resolves(true);
466
+
467
+ const change: PreferenceChange = {
468
+ preferenceName: WORKSPACE_TRUST_ENABLED,
469
+ scope: PreferenceScope.User,
470
+ domain: [],
471
+ affects: () => true
472
+ };
473
+
474
+ await service.testHandlePreferenceChange(change);
475
+
476
+ expect(windowServiceStub.reload.calledOnce).to.be.true;
477
+ expect(windowServiceStub.setSafeToShutDown.called).to.be.false;
478
+ });
479
+
480
+ it('should not reload when user cancels restart', async () => {
481
+ confirmRestartStub.resolves(false);
482
+
483
+ const change: PreferenceChange = {
484
+ preferenceName: WORKSPACE_TRUST_ENABLED,
485
+ scope: PreferenceScope.User,
486
+ domain: [],
487
+ affects: () => true
488
+ };
489
+
490
+ await service.testHandlePreferenceChange(change);
491
+
492
+ expect(windowServiceStub.reload.called).to.be.false;
493
+ expect(windowServiceStub.setSafeToShutDown.called).to.be.false;
494
+ });
495
+ });
496
+
440
497
  describe('trustedFolders change for empty window with emptyWindow enabled', () => {
441
498
  beforeEach(() => {
442
499
  isEmptyWorkspaceStub.resolves(true);
@@ -459,4 +516,239 @@ describe('WorkspaceTrustService', () => {
459
516
  });
460
517
  });
461
518
  });
519
+
520
+ describe('setWorkspaceTrust and trust change handling', () => {
521
+ let storeWorkspaceTrustStub: sinon.SinonStub;
522
+ let addToTrustedFoldersStub: sinon.SinonStub;
523
+ let removeFromTrustedFoldersStub: sinon.SinonStub;
524
+ let shouldReloadStub: sinon.SinonStub;
525
+ let windowServiceStub: { reload: sinon.SinonStub; setSafeToShutDown: sinon.SinonStub };
526
+ let contextKeyServiceStub: { setContext: sinon.SinonStub };
527
+ let onDidChangeEmitterStub: { fire: sinon.SinonStub };
528
+ let confirmRestartStub: sinon.SinonStub;
529
+
530
+ beforeEach(() => {
531
+ storeWorkspaceTrustStub = sinon.stub(
532
+ service as unknown as { storeWorkspaceTrust: (trust: boolean) => Promise<void> },
533
+ 'storeWorkspaceTrust'
534
+ ).resolves();
535
+ addToTrustedFoldersStub = sinon.stub(service, 'addToTrustedFolders').resolves();
536
+ removeFromTrustedFoldersStub = sinon.stub(service, 'removeFromTrustedFolders').resolves();
537
+ shouldReloadStub = sinon.stub(
538
+ service as unknown as { shouldReloadForTrustChange: () => boolean },
539
+ 'shouldReloadForTrustChange'
540
+ );
541
+ confirmRestartStub = sinon.stub(
542
+ service as unknown as { confirmRestart: () => Promise<boolean> },
543
+ 'confirmRestart'
544
+ ).resolves(true);
545
+ windowServiceStub = { reload: sinon.stub(), setSafeToShutDown: sinon.stub() };
546
+ (service as unknown as { windowService: typeof windowServiceStub }).windowService = windowServiceStub;
547
+ contextKeyServiceStub = { setContext: sinon.stub() };
548
+ (service as unknown as { contextKeyService: typeof contextKeyServiceStub }).contextKeyService = contextKeyServiceStub;
549
+ onDidChangeEmitterStub = { fire: sinon.stub() };
550
+ (service as unknown as { onDidChangeWorkspaceTrustEmitter: typeof onDidChangeEmitterStub }).onDidChangeWorkspaceTrustEmitter = onDidChangeEmitterStub;
551
+ });
552
+
553
+ it('should update currentTrust and fire event', async () => {
554
+ service.setCurrentTrust(false);
555
+ shouldReloadStub.returns(false);
556
+
557
+ await service.setWorkspaceTrust(true);
558
+
559
+ expect(service.getCurrentTrust()).to.equal(true);
560
+ expect(contextKeyServiceStub.setContext.calledWith('isWorkspaceTrusted', true)).to.be.true;
561
+ expect(onDidChangeEmitterStub.fire.calledWith(true)).to.be.true;
562
+ });
563
+
564
+ it('should be a no-op when trust value does not change', async () => {
565
+ service.setCurrentTrust(true);
566
+
567
+ await service.setWorkspaceTrust(true);
568
+
569
+ expect(contextKeyServiceStub.setContext.called).to.be.false;
570
+ expect(onDidChangeEmitterStub.fire.called).to.be.false;
571
+ expect(storeWorkspaceTrustStub.called).to.be.false;
572
+ });
573
+
574
+ it('should confirm restart before storing or firing events when reload is required', async () => {
575
+ const callOrder: string[] = [];
576
+ confirmRestartStub.callsFake(async () => {
577
+ callOrder.push('confirm');
578
+ return true;
579
+ });
580
+ storeWorkspaceTrustStub.callsFake(() => {
581
+ callOrder.push('store');
582
+ return Promise.resolve();
583
+ });
584
+ onDidChangeEmitterStub.fire.callsFake(() => {
585
+ callOrder.push('fire');
586
+ });
587
+ shouldReloadStub.returns(true);
588
+
589
+ service.setCurrentTrust(false);
590
+ await service.setWorkspaceTrust(true);
591
+
592
+ expect(callOrder[0]).to.equal('confirm');
593
+ expect(callOrder).to.include('store');
594
+ expect(callOrder).to.include('fire');
595
+ });
596
+
597
+ it('should call windowService.reload() when shouldReloadForTrustChange returns true', async () => {
598
+ shouldReloadStub.returns(true);
599
+
600
+ service.setCurrentTrust(false);
601
+ await service.setWorkspaceTrust(true);
602
+
603
+ expect(windowServiceStub.reload.calledOnce).to.be.true;
604
+ });
605
+
606
+ it('should NOT call windowService.reload() when shouldReloadForTrustChange returns false', async () => {
607
+ shouldReloadStub.returns(false);
608
+
609
+ service.setCurrentTrust(false);
610
+ await service.setWorkspaceTrust(true);
611
+
612
+ expect(windowServiceStub.reload.called).to.be.false;
613
+ });
614
+
615
+ it('should NOT call setSafeToShutDown (unsaved-changes protection preserved)', async () => {
616
+ shouldReloadStub.returns(true);
617
+
618
+ service.setCurrentTrust(false);
619
+ await service.setWorkspaceTrust(true);
620
+
621
+ expect(windowServiceStub.setSafeToShutDown.called).to.be.false;
622
+ });
623
+
624
+ it('should NOT call windowService.reload() when user cancels confirm dialog', async () => {
625
+ shouldReloadStub.returns(true);
626
+ confirmRestartStub.resolves(false);
627
+
628
+ service.setCurrentTrust(false);
629
+ await service.setWorkspaceTrust(true);
630
+
631
+ expect(windowServiceStub.reload.called).to.be.false;
632
+ });
633
+
634
+ it('should leave trust state unchanged and not fire events when user cancels the restart dialog', async () => {
635
+ shouldReloadStub.returns(true);
636
+ confirmRestartStub.resolves(false);
637
+
638
+ service.setCurrentTrust(false);
639
+ await service.setWorkspaceTrust(true);
640
+
641
+ expect(service.getCurrentTrust()).to.equal(false);
642
+ expect(contextKeyServiceStub.setContext.called).to.be.false;
643
+ expect(onDidChangeEmitterStub.fire.called).to.be.false;
644
+ expect(storeWorkspaceTrustStub.called).to.be.false;
645
+ expect(addToTrustedFoldersStub.called).to.be.false;
646
+ expect(removeFromTrustedFoldersStub.called).to.be.false;
647
+ });
648
+
649
+ it('should call addToTrustedFolders when setting trusted=true', async () => {
650
+ shouldReloadStub.returns(false);
651
+ service.setCurrentTrust(false);
652
+
653
+ await service.setWorkspaceTrust(true);
654
+
655
+ expect(addToTrustedFoldersStub.calledOnce).to.be.true;
656
+ expect(removeFromTrustedFoldersStub.called).to.be.false;
657
+ });
658
+
659
+ it('should call removeFromTrustedFolders when setting trusted=false', async () => {
660
+ shouldReloadStub.returns(false);
661
+ service.setCurrentTrust(true);
662
+
663
+ await service.setWorkspaceTrust(false);
664
+
665
+ expect(removeFromTrustedFoldersStub.calledOnce).to.be.true;
666
+ expect(addToTrustedFoldersStub.called).to.be.false;
667
+ });
668
+
669
+ it('should NOT reload when reload=false even if shouldReloadForTrustChange returns true', async () => {
670
+ shouldReloadStub.returns(true);
671
+
672
+ service.setCurrentTrust(false);
673
+ await service.setWorkspaceTrust(true, false);
674
+
675
+ expect(windowServiceStub.reload.called).to.be.false;
676
+ });
677
+
678
+ it('should still update state and fire event when reload=false', async () => {
679
+ service.setCurrentTrust(false);
680
+ shouldReloadStub.returns(true);
681
+
682
+ await service.setWorkspaceTrust(true, false);
683
+
684
+ expect(service.getCurrentTrust()).to.equal(true);
685
+ expect(contextKeyServiceStub.setContext.calledWith('isWorkspaceTrusted', true)).to.be.true;
686
+ expect(onDidChangeEmitterStub.fire.calledWith(true)).to.be.true;
687
+ });
688
+
689
+ });
690
+
691
+ describe('shouldReloadForTrustChange', () => {
692
+ let freshService: TestableWorkspaceTrustService;
693
+
694
+ beforeEach(() => {
695
+ freshService = new TestableWorkspaceTrustService();
696
+ });
697
+
698
+ it('should return false when no restriction contributions are registered', () => {
699
+ (freshService as unknown as { restrictionContributions: { getContributions: () => unknown[] } })
700
+ .restrictionContributions = { getContributions: () => [] };
701
+
702
+ expect(freshService.testShouldReloadForTrustChange(true)).to.be.false;
703
+ });
704
+
705
+ it('should return false when contribution does not implement requiresReloadOnTrustChange', () => {
706
+ (freshService as unknown as { restrictionContributions: { getContributions: () => unknown[] } })
707
+ .restrictionContributions = { getContributions: () => [{ getRestrictions: () => [] }] };
708
+
709
+ expect(freshService.testShouldReloadForTrustChange(true)).to.be.false;
710
+ });
711
+
712
+ it('should return true when a contribution requires reload for the given trust value', () => {
713
+ (freshService as unknown as { restrictionContributions: { getContributions: () => unknown[] } })
714
+ .restrictionContributions = {
715
+ getContributions: () => [{
716
+ getRestrictions: () => [],
717
+ requiresReloadOnTrustChange: (_newTrust: boolean) => true
718
+ }]
719
+ };
720
+
721
+ expect(freshService.testShouldReloadForTrustChange(true)).to.be.true;
722
+ });
723
+
724
+ it('should return false when contribution returns false for requiresReloadOnTrustChange', () => {
725
+ (freshService as unknown as { restrictionContributions: { getContributions: () => unknown[] } })
726
+ .restrictionContributions = {
727
+ getContributions: () => [{
728
+ getRestrictions: () => [],
729
+ requiresReloadOnTrustChange: (_newTrust: boolean) => false
730
+ }]
731
+ };
732
+
733
+ expect(freshService.testShouldReloadForTrustChange(true)).to.be.false;
734
+ });
735
+
736
+ it('should pass newTrust value to requiresReloadOnTrustChange', () => {
737
+ const calls: boolean[] = [];
738
+ (freshService as unknown as { restrictionContributions: { getContributions: () => unknown[] } })
739
+ .restrictionContributions = {
740
+ getContributions: () => [{
741
+ getRestrictions: () => [],
742
+ requiresReloadOnTrustChange: (newTrust: boolean) => { calls.push(newTrust); return false; }
743
+ }]
744
+ };
745
+
746
+ freshService.testShouldReloadForTrustChange(false);
747
+ expect(calls).to.deep.equal([false]);
748
+
749
+ freshService.testShouldReloadForTrustChange(true);
750
+ expect(calls).to.deep.equal([false, true]);
751
+ });
752
+ });
753
+
462
754
  });
@@ -51,6 +51,14 @@ export interface WorkspaceRestrictionContribution {
51
51
  * Called when building the restricted mode status bar tooltip.
52
52
  */
53
53
  getRestrictions(): WorkspaceRestriction[];
54
+
55
+ /**
56
+ * Returns whether a window reload is required when workspace trust changes to `newTrust`.
57
+ * Called before reloading on trust change to avoid unnecessary reloads when no
58
+ * trust-restricted items are actually affected.
59
+ * If not implemented, the contribution is assumed not to require a reload.
60
+ */
61
+ requiresReloadOnTrustChange?(newTrust: boolean): boolean;
54
62
  }
55
63
 
56
64
  export interface WorkspaceRestriction {
@@ -166,16 +174,28 @@ export class WorkspaceTrustService {
166
174
  }
167
175
  }
168
176
 
169
- setWorkspaceTrust(trusted: boolean): void {
177
+ async setWorkspaceTrust(trusted: boolean, reload = true): Promise<void> {
170
178
  if (this.currentTrust === trusted) {
171
179
  return;
172
180
  }
181
+ const needsReload = reload && this.shouldReloadForTrustChange(trusted);
182
+ if (needsReload && !await this.confirmRestart()) {
183
+ return;
184
+ }
185
+ // Must be set before add/removeFromTrustedFolders to prevent handlePreferenceChange from recursing.
173
186
  this.currentTrust = trusted;
174
187
  this.contextKeyService.setContext('isWorkspaceTrusted', trusted);
175
- if (this.workspaceTrustPref[WORKSPACE_TRUST_STARTUP_PROMPT] === WorkspaceTrustPrompt.ONCE) {
176
- this.storeWorkspaceTrust(trusted);
177
- }
178
188
  this.onDidChangeWorkspaceTrustEmitter.fire(trusted);
189
+ await this.storeWorkspaceTrust(trusted);
190
+ await (trusted ? this.addToTrustedFolders() : this.removeFromTrustedFolders());
191
+ if (needsReload) {
192
+ this.windowService.reload();
193
+ }
194
+ }
195
+
196
+ protected shouldReloadForTrustChange(newTrust: boolean): boolean {
197
+ return this.restrictionContributions.getContributions()
198
+ .some(c => c.requiresReloadOnTrustChange?.(newTrust) ?? false);
179
199
  }
180
200
 
181
201
  protected isWorkspaceTrustResolved(): boolean {
@@ -385,7 +405,7 @@ export class WorkspaceTrustService {
385
405
  }
386
406
  const areAllUrisTrusted = await this.areAllWorkspaceUrisTrusted();
387
407
  if (areAllUrisTrusted !== this.currentTrust) {
388
- this.setWorkspaceTrust(areAllUrisTrusted);
408
+ await this.setWorkspaceTrust(areAllUrisTrusted);
389
409
  }
390
410
  return;
391
411
  }
@@ -397,7 +417,6 @@ export class WorkspaceTrustService {
397
417
 
398
418
  if (change.preferenceName === WORKSPACE_TRUST_ENABLED) {
399
419
  if (!await this.isEmptyWorkspace() && this.isWorkspaceTrustResolved() && await this.confirmRestart()) {
400
- this.windowService.setSafeToShutDown();
401
420
  this.windowService.reload();
402
421
  }
403
422
  this.resolveWorkspaceTrust();
@@ -408,7 +427,9 @@ export class WorkspaceTrustService {
408
427
  // For empty windows, directly update trust based on the new setting value
409
428
  const shouldTrust = !!this.workspaceTrustPref[WORKSPACE_TRUST_EMPTY_WINDOW];
410
429
  if (this.currentTrust !== shouldTrust) {
411
- this.setWorkspaceTrust(shouldTrust);
430
+ // No reload needed: in an empty window there are no workspace folders,
431
+ // so no extensions are blocked by trust and no extension host restart is required.
432
+ await this.setWorkspaceTrust(shouldTrust, false);
412
433
  }
413
434
  }
414
435
  }
@@ -536,9 +557,7 @@ export class WorkspaceTrustService {
536
557
  try {
537
558
  const grantedTrust = await this.showTrustPromptDialog();
538
559
  if (grantedTrust) {
539
- // User granted trust - update the state
540
- this.setWorkspaceTrust(true);
541
- await this.addToTrustedFolders();
560
+ await this.setWorkspaceTrust(true);
542
561
  }
543
562
  this.pendingTrustRequest.resolve(grantedTrust);
544
563
  return grantedTrust;