tui.test.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
  2. import * as fs from 'node:fs';
  3. import * as os from 'node:os';
  4. import * as path from 'node:path';
  5. import {
  6. getSidebarAgentNames,
  7. readConfigInvalid,
  8. splitSidebarModelId,
  9. default as tuiPlugin,
  10. } from './tui';
  11. import type { TuiSnapshot } from './tui-state';
  12. function createSnapshot(overrides: Partial<TuiSnapshot> = {}): TuiSnapshot {
  13. return {
  14. version: 1,
  15. updatedAt: 0,
  16. agentModels: {},
  17. agentVariants: {},
  18. ...overrides,
  19. };
  20. }
  21. describe('tui sidebar agents', () => {
  22. test('hides disabled agents when models are persisted explicitly', () => {
  23. const agentNames = getSidebarAgentNames(
  24. createSnapshot({
  25. agentModels: {
  26. explorer: 'openai/gpt-5.4-mini',
  27. fixer: 'openai/gpt-5.4-mini',
  28. },
  29. }),
  30. );
  31. expect(agentNames).toEqual(['explorer', 'fixer']);
  32. expect(agentNames).not.toContain('observer');
  33. expect(agentNames).not.toContain('librarian');
  34. });
  35. test('uses default-enabled fallback before models are persisted', () => {
  36. const agentNames = getSidebarAgentNames(createSnapshot({}));
  37. expect(agentNames).toContain('explorer');
  38. expect(agentNames).toContain('fixer');
  39. expect(agentNames).not.toContain('observer');
  40. expect(agentNames).not.toContain('council');
  41. expect(agentNames).not.toContain('councillor');
  42. });
  43. });
  44. describe('splitSidebarModelId', () => {
  45. test('splits provider from model at the first slash', () => {
  46. expect(splitSidebarModelId('openai/gpt-5.5-fast')).toEqual({
  47. provider: 'openai',
  48. model: 'gpt-5.5-fast',
  49. });
  50. expect(
  51. splitSidebarModelId(
  52. 'fireworks-ai/accounts/fireworks/routers/kimi-k2p5-turbo',
  53. ),
  54. ).toEqual({
  55. provider: 'fireworks-ai',
  56. model: 'accounts/fireworks/routers/kimi-k2p5-turbo',
  57. });
  58. });
  59. test('keeps slashless names as model only', () => {
  60. expect(splitSidebarModelId('pending')).toEqual({ model: 'pending' });
  61. });
  62. });
  63. describe('readConfigInvalid', () => {
  64. let originalEnv: typeof process.env;
  65. let configHome: string;
  66. beforeEach(() => {
  67. originalEnv = { ...process.env };
  68. // Isolate from real user config and env presets
  69. delete process.env.OPENCODE_CONFIG_DIR;
  70. delete process.env.OH_MY_OPENCODE_SLIM_PRESET;
  71. configHome = fs.mkdtempSync(path.join(os.tmpdir(), 'omos-tui-env-'));
  72. process.env.XDG_CONFIG_HOME = configHome;
  73. });
  74. afterEach(() => {
  75. fs.rmSync(configHome, { recursive: true, force: true });
  76. process.env = originalEnv;
  77. });
  78. test('detects invalid config from the current directory without persisted state', () => {
  79. const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'omos-tui-'));
  80. try {
  81. const projectDir = path.join(tempDir, 'project');
  82. const configDir = path.join(projectDir, '.opencode');
  83. fs.mkdirSync(configDir, { recursive: true });
  84. fs.writeFileSync(
  85. path.join(configDir, 'oh-my-opencode-slim.json'),
  86. JSON.stringify({ agents: { oracle: { temperature: 5 } } }),
  87. );
  88. expect(readConfigInvalid(projectDir)).toBe(true);
  89. } finally {
  90. fs.rmSync(tempDir, { recursive: true, force: true });
  91. }
  92. });
  93. test('returns false for valid config', () => {
  94. const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'omos-tui-'));
  95. try {
  96. const projectDir = path.join(tempDir, 'project');
  97. const configDir = path.join(projectDir, '.opencode');
  98. fs.mkdirSync(configDir, { recursive: true });
  99. fs.writeFileSync(
  100. path.join(configDir, 'oh-my-opencode-slim.json'),
  101. JSON.stringify({ agents: { oracle: { model: 'valid/model' } } }),
  102. );
  103. expect(readConfigInvalid(projectDir)).toBe(false);
  104. } finally {
  105. fs.rmSync(tempDir, { recursive: true, force: true });
  106. }
  107. });
  108. });
  109. describe('tui plugin env disable', () => {
  110. let originalEnv: typeof process.env;
  111. beforeEach(() => {
  112. originalEnv = { ...process.env };
  113. });
  114. afterEach(() => {
  115. process.env = originalEnv;
  116. });
  117. test('does not perform setup when plugin is disabled by env', async () => {
  118. process.env.OH_MY_OPENCODE_SLIM_DISABLE = '1';
  119. let disposeRegistered = false;
  120. let renderRequested = false;
  121. let registered = false;
  122. await tuiPlugin.tui(
  123. {
  124. lifecycle: {
  125. onDispose: () => {
  126. disposeRegistered = true;
  127. },
  128. },
  129. renderer: {
  130. requestRender: () => {
  131. renderRequested = true;
  132. },
  133. },
  134. slots: {
  135. register: () => {
  136. registered = true;
  137. },
  138. },
  139. theme: { current: {} },
  140. } as unknown as Parameters<typeof tuiPlugin.tui>[0],
  141. {},
  142. { version: 'test' } as Parameters<typeof tuiPlugin.tui>[2],
  143. );
  144. expect(registered).toBe(false);
  145. expect(disposeRegistered).toBe(false);
  146. expect(renderRequested).toBe(false);
  147. });
  148. });