| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405 |
- import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
- import { shouldInstallCompanion } from './install';
- import type { InstallConfig } from './types';
- const ORIGINAL_ENV = { ...process.env };
- const ORIGINAL_STDIN_IS_TTY = process.stdin.isTTY;
- const actualSkillSync = require('../hooks/auto-update-checker/skill-sync');
- const actualConfigManager = require('./config-manager');
- const actualBackgroundSubagents = require('./background-subagents');
- const actualPaths = require('./paths');
- const originalSyncBundledSkillsFromPackage =
- actualSkillSync.syncBundledSkillsFromPackage;
- const originalIsOpenCodeInstalled = actualConfigManager.isOpenCodeInstalled;
- const originalGetOpenCodeVersion = actualConfigManager.getOpenCodeVersion;
- const originalGetOpenCodePath = actualConfigManager.getOpenCodePath;
- const originalAddPluginToOpenCodeConfig =
- actualConfigManager.addPluginToOpenCodeConfig;
- const originalAddPluginToOpenCodeTuiConfig =
- actualConfigManager.addPluginToOpenCodeTuiConfig;
- const originalWarmOpenCodePluginCache =
- actualConfigManager.warmOpenCodePluginCache;
- const originalDisableDefaultAgents = actualConfigManager.disableDefaultAgents;
- const originalEnableLspByDefault = actualConfigManager.enableLspByDefault;
- const originalDetectCurrentConfig = actualConfigManager.detectCurrentConfig;
- const originalGenerateLiteConfig = actualConfigManager.generateLiteConfig;
- const originalWriteLiteConfig = actualConfigManager.writeLiteConfig;
- const originalIsBackgroundSubagentsEnabled =
- actualBackgroundSubagents.isBackgroundSubagentsEnabled;
- const originalDetectBackgroundSubagentsTarget =
- actualBackgroundSubagents.detectBackgroundSubagentsTarget;
- const originalExpandHomePath = actualBackgroundSubagents.expandHomePath;
- const originalGetBackgroundSubagentsBlock =
- actualBackgroundSubagents.getBackgroundSubagentsBlock;
- const originalWriteBackgroundSubagentsBlock =
- actualBackgroundSubagents.writeBackgroundSubagentsBlock;
- const originalManualBackgroundSubagentsInstructions =
- actualBackgroundSubagents.manualBackgroundSubagentsInstructions;
- const originalGetExistingLiteConfigPath = actualPaths.getExistingLiteConfigPath;
- let importCounter = 0;
- let mockSkippedResult: string[] = [];
- let mockFailedResult: string[] = [];
- let mockStagedResult: string[] = [];
- let mockAdoptedResult: string[] = [];
- let mockCustomizedResult: string[] = [];
- let receivedSkillSyncOptions: unknown;
- let enableInstallMocks = false;
- mock.module('../hooks/auto-update-checker/skill-sync', () => {
- return {
- ...actualSkillSync,
- syncBundledSkillsFromPackage: (packageRoot: string, options?: any) => {
- if (enableInstallMocks) {
- receivedSkillSyncOptions = options;
- return {
- installed: [],
- skippedExisting: mockSkippedResult,
- failed: mockFailedResult,
- staged: mockStagedResult,
- adopted: mockAdoptedResult,
- customized: mockCustomizedResult,
- };
- }
- return originalSyncBundledSkillsFromPackage(packageRoot, options);
- },
- };
- });
- mock.module('./config-manager', () => {
- return {
- ...actualConfigManager,
- isOpenCodeInstalled: async () =>
- enableInstallMocks ? true : originalIsOpenCodeInstalled(),
- getOpenCodeVersion: async () =>
- enableInstallMocks ? '1.0.0' : originalGetOpenCodeVersion(),
- getOpenCodePath: () =>
- enableInstallMocks
- ? '/usr/local/bin/opencode'
- : originalGetOpenCodePath(),
- addPluginToOpenCodeConfig: async () =>
- enableInstallMocks
- ? { success: true, configPath: '/path' }
- : originalAddPluginToOpenCodeConfig(),
- addPluginToOpenCodeTuiConfig: async () =>
- enableInstallMocks
- ? { success: true, configPath: '/path' }
- : originalAddPluginToOpenCodeTuiConfig(),
- warmOpenCodePluginCache: async () =>
- enableInstallMocks
- ? { success: true, configPath: '/path' }
- : originalWarmOpenCodePluginCache(),
- disableDefaultAgents: () =>
- enableInstallMocks
- ? { success: true, configPath: '/path' }
- : originalDisableDefaultAgents(),
- enableLspByDefault: () =>
- enableInstallMocks
- ? { success: true, configPath: '/path' }
- : originalEnableLspByDefault(),
- detectCurrentConfig: () =>
- enableInstallMocks
- ? { isInstalled: true }
- : originalDetectCurrentConfig(),
- generateLiteConfig: (cfg: any) =>
- enableInstallMocks ? {} : originalGenerateLiteConfig(cfg),
- writeLiteConfig: (cfg: any, path?: string) =>
- enableInstallMocks
- ? { success: true, configPath: '/path' }
- : originalWriteLiteConfig(cfg, path),
- };
- });
- mock.module('./background-subagents', () => {
- return {
- ...actualBackgroundSubagents,
- isBackgroundSubagentsEnabled: (env?: string) =>
- enableInstallMocks ? true : originalIsBackgroundSubagentsEnabled(env),
- detectBackgroundSubagentsTarget: () =>
- enableInstallMocks ? '/path' : originalDetectBackgroundSubagentsTarget(),
- expandHomePath: (p: string) =>
- enableInstallMocks ? p : originalExpandHomePath(p),
- getBackgroundSubagentsBlock: (target: string) =>
- enableInstallMocks ? '' : originalGetBackgroundSubagentsBlock(target),
- writeBackgroundSubagentsBlock: (target: string) =>
- enableInstallMocks ? {} : originalWriteBackgroundSubagentsBlock(target),
- manualBackgroundSubagentsInstructions: (opts?: any) =>
- enableInstallMocks
- ? ''
- : originalManualBackgroundSubagentsInstructions(opts),
- };
- });
- mock.module('./paths', () => {
- return {
- ...actualPaths,
- getExistingLiteConfigPath: () =>
- enableInstallMocks
- ? '/path/lite-config.json'
- : originalGetExistingLiteConfigPath(),
- };
- });
- function baseConfig(): InstallConfig {
- return {
- installCustomSkills: false,
- forceSkillSync: false,
- reset: false,
- backgroundSubagents: 'no',
- companion: 'ask',
- };
- }
- describe('shouldInstallCompanion', () => {
- afterEach(() => {
- process.env = { ...ORIGINAL_ENV };
- Object.defineProperty(process.stdin, 'isTTY', {
- configurable: true,
- value: ORIGINAL_STDIN_IS_TTY,
- });
- });
- test('dry-run defaults to skip on niri', async () => {
- process.env.NIRI_SOCKET = '/run/user/1000/niri.sock';
- const config = { ...baseConfig(), dryRun: true };
- await expect(shouldInstallCompanion(config)).resolves.toBe(false);
- expect(config.companion).toBe('no');
- });
- test('explicit companion yes still enables companion on niri', async () => {
- process.env.XDG_CURRENT_DESKTOP = 'niri';
- const config = { ...baseConfig(), companion: 'yes' as const };
- await expect(shouldInstallCompanion(config)).resolves.toBe(true);
- });
- test('dry-run defaults to skip outside niri', async () => {
- delete process.env.NIRI_SOCKET;
- delete process.env.XDG_CURRENT_DESKTOP;
- delete process.env.DESKTOP_SESSION;
- const config = { ...baseConfig(), dryRun: true };
- await expect(shouldInstallCompanion(config)).resolves.toBe(false);
- expect(config.companion).toBe('no');
- });
- });
- describe('install skill synchronization error mapping', () => {
- let logSpy: ReturnType<typeof mock>;
- let originalConsoleLog: typeof console.log;
- beforeEach(() => {
- enableInstallMocks = true;
- mockSkippedResult = [];
- mockFailedResult = [];
- mockStagedResult = [];
- mockAdoptedResult = [];
- mockCustomizedResult = [];
- receivedSkillSyncOptions = undefined;
- originalConsoleLog = console.log;
- logSpy = mock(() => {});
- console.log = logSpy;
- });
- afterEach(() => {
- enableInstallMocks = false;
- console.log = originalConsoleLog;
- });
- test('maps __lock__ to lock acquisition failure', async () => {
- mockFailedResult = ['__lock__'];
- const { install } = await import(`./install?test=${importCounter++}`);
- await install({
- skills: 'yes',
- tui: false,
- companion: 'no',
- });
- const calls = logSpy.mock.calls.map((call: any[]) => call[0] as string);
- const hasLockErr = calls.some((msg: string) =>
- msg?.includes('Lock acquisition failed'),
- );
- expect(hasLockErr).toBe(true);
- const hasRawSentinel = calls.some((msg: string) =>
- msg?.includes('__lock__'),
- );
- expect(hasRawSentinel).toBe(false);
- // Verify summary does not count __lock__ as a failed skill
- const summaryMsg = calls.find((msg: string) =>
- msg?.includes('Skill synchronization complete'),
- );
- expect(summaryMsg).toBeDefined();
- expect(summaryMsg).toContain(
- '0 staged, 0 adopted, 0 customized, 0 failed.',
- );
- });
- test('maps __manifest__ to manifest write failure', async () => {
- mockFailedResult = ['__manifest__'];
- const { install } = await import(`./install?test=${importCounter++}`);
- await install({
- skills: 'yes',
- tui: false,
- companion: 'no',
- });
- const calls = logSpy.mock.calls.map((call: any[]) => call[0] as string);
- const hasManifestErr = calls.some((msg: string) =>
- msg?.includes('Manifest write failed'),
- );
- expect(hasManifestErr).toBe(true);
- const hasRawSentinel = calls.some((msg: string) =>
- msg?.includes('__manifest__'),
- );
- expect(hasRawSentinel).toBe(false);
- // Verify summary does not count __manifest__ as a failed skill
- const summaryMsg = calls.find((msg: string) =>
- msg?.includes('Skill synchronization complete'),
- );
- expect(summaryMsg).toBeDefined();
- expect(summaryMsg).toContain(
- '0 staged, 0 adopted, 0 customized, 0 failed.',
- );
- });
- test('keeps normal skill names prefix as Failed: <name>', async () => {
- mockFailedResult = ['some-custom-skill'];
- const { install } = await import(`./install?test=${importCounter++}`);
- await install({
- skills: 'yes',
- tui: false,
- companion: 'no',
- });
- const calls = logSpy.mock.calls.map((call: any[]) => call[0] as string);
- const hasSkillErr = calls.some((msg: string) =>
- msg?.includes('Failed: some-custom-skill'),
- );
- expect(hasSkillErr).toBe(true);
- // Verify summary DOES count standard skill failures in the failed count
- const summaryMsg = calls.find((msg: string) =>
- msg?.includes('Skill synchronization complete'),
- );
- expect(summaryMsg).toBeDefined();
- expect(summaryMsg).toContain(
- '0 staged, 0 adopted, 0 customized, 1 failed.',
- );
- });
- test('prints staged skills during sync', async () => {
- mockStagedResult = ['staged-skill'];
- const { install } = await import(`./install?test=${importCounter++}`);
- await install({
- skills: 'yes',
- tui: false,
- companion: 'no',
- });
- const calls = logSpy.mock.calls.map((call: any[]) => call[0] as string);
- expect(
- calls.some((msg: string) =>
- msg?.includes('Staged for review: staged-skill'),
- ),
- ).toBe(true);
- });
- test('prints adopted skills during sync', async () => {
- mockAdoptedResult = ['adopted-skill'];
- const { install } = await import(`./install?test=${importCounter++}`);
- await install({
- skills: 'yes',
- tui: false,
- companion: 'no',
- });
- const calls = logSpy.mock.calls.map((call: any[]) => call[0] as string);
- expect(
- calls.some((msg: string) => msg?.includes('Adopted: adopted-skill')),
- ).toBe(true);
- });
- test('prints customized skills during sync', async () => {
- mockCustomizedResult = ['customized-skill'];
- const { install } = await import(`./install?test=${importCounter++}`);
- await install({
- skills: 'yes',
- tui: false,
- companion: 'no',
- });
- const calls = logSpy.mock.calls.map((call: any[]) => call[0] as string);
- expect(
- calls.some((msg: string) =>
- msg?.includes('Customized: customized-skill'),
- ),
- ).toBe(true);
- });
- test('does not double-print categorized skipped skills', async () => {
- mockSkippedResult = ['staged-skill', 'adopted-skill', 'customized-skill'];
- mockStagedResult = ['staged-skill'];
- mockAdoptedResult = ['adopted-skill'];
- mockCustomizedResult = ['customized-skill'];
- const { install } = await import(`./install?test=${importCounter++}`);
- await install({
- skills: 'yes',
- tui: false,
- companion: 'no',
- });
- const calls = logSpy.mock.calls.map((call: any[]) => call[0] as string);
- expect(
- calls.some((msg: string) =>
- msg?.includes('Skipped/Preserved: staged-skill'),
- ),
- ).toBe(false);
- expect(
- calls.some((msg: string) =>
- msg?.includes('Skipped/Preserved: adopted-skill'),
- ),
- ).toBe(false);
- expect(
- calls.some((msg: string) =>
- msg?.includes('Skipped/Preserved: customized-skill'),
- ),
- ).toBe(false);
- const summaryMsg = calls.find((msg: string) =>
- msg?.includes('Skill synchronization complete'),
- );
- expect(summaryMsg).toBeDefined();
- expect(summaryMsg).toContain(
- '0 skipped/preserved, 1 staged, 1 adopted, 1 customized, 0 failed.',
- );
- });
- test('passes force mode to bundled skill synchronization', async () => {
- const { install } = await import(`./install?test=${importCounter++}`);
- await install({
- skills: 'force',
- tui: false,
- companion: 'no',
- });
- expect(receivedSkillSyncOptions).toEqual({ force: true });
- });
- });
|