|
1 | 1 | import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; |
2 | 2 | import { ObjectKernel } from './kernel'; |
3 | 3 | import { ServiceLifecycle, PluginMetadata } from './plugin-loader'; |
| 4 | +import type { PluginStartupResult } from './plugin-loader'; |
4 | 5 | import type { Plugin, PluginContext } from './types'; |
5 | 6 | import { recordGuards, stillPinningTheLoop } from '@objectstack/refd-timer-testkit'; |
6 | 7 |
|
@@ -582,6 +583,73 @@ describe('ObjectKernel', () => { |
582 | 583 |
|
583 | 584 | await kernel.shutdown(); |
584 | 585 | }); |
| 586 | + |
| 587 | + // These two pin the MEANING of the number, not merely that one is |
| 588 | + // present. The result member carrying it was spelled `startTime` while |
| 589 | + // holding `Date.now() - start`, so a reader who correctly took it for an |
| 590 | + // instant and wrote `Date.now() - result.startTime` got an age near the |
| 591 | + // epoch. `toBeGreaterThan(0)` cannot tell the two readings apart -- an |
| 592 | + // epoch-millisecond instant passes it too. A ceiling can: any instant |
| 593 | + // today is ~1.7e12, orders of magnitude above any plugin's start(). |
| 594 | + const INSTANT_FLOOR_MS = 1_000_000_000; // ~11.5 days as a duration; well below any real epoch-ms instant |
| 595 | + |
| 596 | + it('getPluginStartupDurations reports elapsed durations, not start instants', async () => { |
| 597 | + const plugin: Plugin = { |
| 598 | + name: 'timed-plugin', |
| 599 | + version: '1.0.0', |
| 600 | + init: async () => {}, |
| 601 | + start: async () => { |
| 602 | + await new Promise(resolve => setTimeout(resolve, 20)); |
| 603 | + }, |
| 604 | + }; |
| 605 | + |
| 606 | + await kernel.use(plugin); |
| 607 | + await kernel.bootstrap(); |
| 608 | + |
| 609 | + const durations = kernel.getPluginStartupDurations(); |
| 610 | + const value = durations.get('timed-plugin'); |
| 611 | + |
| 612 | + expect(value).toBeGreaterThan(0); |
| 613 | + expect(value).toBeLessThan(INSTANT_FLOOR_MS); |
| 614 | + // The deprecated alias is the same map, so it must agree. |
| 615 | + expect(kernel.getPluginMetrics().get('timed-plugin')).toBe(value); |
| 616 | + |
| 617 | + await kernel.shutdown(); |
| 618 | + }); |
| 619 | + |
| 620 | + it('PluginStartupResult.duration is an elapsed duration on both the success and the failure path', async () => { |
| 621 | + const callStart = (meta: PluginMetadata): Promise<PluginStartupResult> => |
| 622 | + (kernel as unknown as { |
| 623 | + startPluginWithTimeout(p: PluginMetadata): Promise<PluginStartupResult>; |
| 624 | + }).startPluginWithTimeout(meta); |
| 625 | + |
| 626 | + const ok = await callStart({ |
| 627 | + name: 'ok-plugin', |
| 628 | + version: '1.0.0', |
| 629 | + start: async () => { |
| 630 | + await new Promise(resolve => setTimeout(resolve, 20)); |
| 631 | + }, |
| 632 | + } as PluginMetadata); |
| 633 | + |
| 634 | + expect(ok.success).toBe(true); |
| 635 | + expect(ok.duration).toBeGreaterThan(0); |
| 636 | + expect(ok.duration).toBeLessThan(INSTANT_FLOOR_MS); |
| 637 | + // The deprecated alias carries the same elapsed value, not an instant. |
| 638 | + expect(ok.startTime).toBe(ok.duration); |
| 639 | + |
| 640 | + const failed = await callStart({ |
| 641 | + name: 'failing-plugin', |
| 642 | + version: '1.0.0', |
| 643 | + start: async () => { |
| 644 | + throw new Error('boom'); |
| 645 | + }, |
| 646 | + } as PluginMetadata); |
| 647 | + |
| 648 | + expect(failed.success).toBe(false); |
| 649 | + expect(failed.duration).toBeGreaterThanOrEqual(0); |
| 650 | + expect(failed.duration).toBeLessThan(INSTANT_FLOOR_MS); |
| 651 | + expect(failed.startTime).toBe(failed.duration); |
| 652 | + }); |
585 | 653 | }); |
586 | 654 |
|
587 | 655 | describe('Graceful Shutdown', () => { |
|
0 commit comments