Browse Source

chore(deps): clean up v1→v2 migration — rename getClient, remove dead v1 branches, migrate dashboard-manager

Michael Henke 3 weeks ago
parent
commit
54f673420f

+ 2 - 2
src/hooks/chat-headers.test.ts

@@ -6,12 +6,12 @@ import {
   createChatHeadersHook,
 } from './chat-headers';
 
-// Mock getV2Client so internal calls use our mock
+// Mock getClient so internal calls use our mock
 let mockV2Client: Record<string, unknown>;
 let mockSession: { message: ReturnType<typeof mock> };
 
 mock.module('../utils/opencode-client', () => ({
-  getV2Client: () => mockV2Client,
+  getClient: () => mockV2Client,
 }));
 
 function createMockContext(parts: unknown[] = []) {

+ 2 - 2
src/hooks/chat-headers.ts

@@ -1,7 +1,7 @@
 import type { PluginInput, ProviderContext } from '@opencode-ai/plugin';
 import type { Model, UserMessage } from '@opencode-ai/sdk';
 import { isInternalInitiatorPart } from '../utils';
-import { getV2Client } from '../utils/opencode-client';
+import { getClient } from '../utils/opencode-client';
 
 interface ChatHeadersInput {
   sessionID: string;
@@ -44,7 +44,7 @@ async function hasInternalMarker(
   }
 
   try {
-    const response = await getV2Client(input).session.message({
+    const response = await getClient(input).session.message({
       sessionID,
       messageID,
       directory: input.directory,

+ 27 - 0
src/hooks/foreground-fallback/index.test.ts

@@ -13,6 +13,27 @@ type ForegroundFallbackClient = ConstructorParameters<
   typeof ForegroundFallbackManager
 >[0];
 
+// Shared session reference so our mock.module for getClient returns the
+// current test's mock session without relying on this.input (which is
+// undefined in tests — always set in production).
+let currentMockSession: Record<string, unknown> | null = null;
+
+// Override manager.test.ts's global mock.module for getClient. Called
+// at module load AND from createMockClient so it takes effect regardless of
+// test file load order.
+function installGetClientMock(): void {
+  mock.module('../../utils/opencode-client', () => ({
+    getClient: () => ({
+      session: currentMockSession ?? {
+        abort: mock(() => Promise.resolve()),
+        messages: mock(() => Promise.resolve({ data: [] })),
+        promptAsync: mock(() => Promise.resolve()),
+      },
+    }),
+  }));
+}
+installGetClientMock();
+
 // ---------------------------------------------------------------------------
 // Helpers
 // ---------------------------------------------------------------------------
@@ -44,6 +65,12 @@ function createMockClient(overrides?: {
     session.promptAsync = promptAsync;
   }
 
+  // Store for getClient mock
+  currentMockSession = session;
+  // Re-register the mock.module at test time so it survives any
+  // overwrite from other test files loaded in the same process.
+  installGetClientMock();
+
   return {
     client: {
       session,

+ 13 - 21
src/hooks/foreground-fallback/index.ts

@@ -20,7 +20,7 @@
 import type { PluginInput } from '@opencode-ai/plugin';
 import { createInternalAgentTextPart } from '../../utils/internal-initiator';
 import { log } from '../../utils/logger';
-import { getV2Client } from '../../utils/opencode-client';
+import { getClient } from '../../utils/opencode-client';
 import {
   abortSessionWithTimeout,
   parseModelReference,
@@ -257,7 +257,7 @@ export class ForegroundFallbackManager {
   }
 
   constructor(
-    private readonly client: OpencodeClient,
+    _client: OpencodeClient,
     /**
      * Ordered fallback chains per agent.
      * e.g. { orchestrator: ['anthropic/claude-opus-4-5', 'openai/gpt-4o'] }
@@ -520,10 +520,7 @@ export class ForegroundFallbackManager {
 
     this.inProgress.add(sessionID);
     try {
-      await abortSessionWithTimeout(
-        this.input ? getV2Client(this.input) : (this.client as any),
-        sessionID,
-      );
+      await abortSessionWithTimeout(getClient(this.input!), sessionID);
       await this.execFallback(sessionID);
     } finally {
       this.inProgress.delete(sessionID);
@@ -598,10 +595,7 @@ export class ForegroundFallbackManager {
             agentName,
             tried: [...tried],
           });
-          await abortSessionWithTimeout(
-            this.input ? getV2Client(this.input) : (this.client as any),
-            sessionID,
-          );
+          await abortSessionWithTimeout(getClient(this.input!), sessionID);
           return;
         }
       }
@@ -619,9 +613,9 @@ export class ForegroundFallbackManager {
       }
 
       // Retrieve the last user message to re-submit with the fallback model.
-      const result = await (this.input
-        ? getV2Client(this.input).session.messages({ sessionID })
-        : (this.client as any).session.messages({ path: { id: sessionID } }));
+      const result = await getClient(this.input!).session.messages({
+        sessionID,
+      });
       // result.data may contain partial/streaming messages whose `info` is
       // undefined at runtime (OpenCode violates its own declared type), so
       // guard each entry instead of dereferencing `info` directly.
@@ -634,17 +628,18 @@ export class ForegroundFallbackManager {
 
       // promptAsync queues the prompt and returns immediately - this avoids
       // blocking the event handler while waiting for a full LLM response.
-      const sessionClient = this.input
-        ? getV2Client(this.input).session
-        : (this.client as any).session;
+      const sessionClient = getClient(this.input!).session;
       if (typeof sessionClient.promptAsync !== 'function') {
         log('[foreground-fallback] promptAsync unavailable', { sessionID });
         return;
       }
 
       const promptBody = {
+        // ponytail: lastUser.parts are MessagePart[] from API, but v2
+        // promptAsync expects TextPartInput[] — runtime-compatible, TS
+        // doesn't know the `type` field is already 'text'.
         parts: [
-          ...lastUser.parts,
+          ...(lastUser.parts as Array<{ type: 'text'; text: string }>),
           createInternalAgentTextPart('Foreground fallback replay.'),
         ],
         model: ref,
@@ -661,10 +656,7 @@ export class ForegroundFallbackManager {
         log('[foreground-fallback] promptAsync on busy session, aborting', {
           sessionID,
         });
-        await abortSessionWithTimeout(
-          this.input ? getV2Client(this.input) : (this.client as any),
-          sessionID,
-        );
+        await abortSessionWithTimeout(getClient(this.input!), sessionID);
         await new Promise((r) => setTimeout(r, REPROMPT_DELAY_MS));
         await sessionClient.promptAsync({ sessionID, ...promptBody });
       }

+ 2 - 2
src/hooks/task-session-manager/index.test.ts

@@ -20,10 +20,10 @@ import {
   createTaskSessionManagerHook,
 } from './index';
 
-// Route getV2Client back to _ctx.client so existing _ctx.client.session
+// Route getClient back to _ctx.client so existing _ctx.client.session
 // mocks continue to work through the new v2 lookup path.
 mock.module('../../utils/opencode-client', () => ({
-  getV2Client: (input: { client: unknown }) => input.client as never,
+  getClient: (input: { client: unknown }) => input.client as never,
 }));
 
 /** Wait for the idle reconciliation delay (2s + margin) to flush. */

+ 2 - 2
src/hooks/task-session-manager/index.ts

@@ -5,7 +5,7 @@ import {
   isInternalInitiatorPart,
 } from '../../utils';
 import { isRecord as isObjectRecord } from '../../utils/guards';
-import { getV2Client } from '../../utils/opencode-client';
+import { getClient } from '../../utils/opencode-client';
 import type { SessionLifecycle } from '../session-lifecycle';
 import { isUserMessageWithParts } from '../types';
 import {
@@ -138,7 +138,7 @@ export function createTaskSessionManagerHook(
     status?: (input: unknown, opts?: unknown) => Promise<SdkResponse>;
     promptAsync?: (input: unknown, opts?: unknown) => Promise<unknown>;
   };
-  const sessionSdk = getV2Client(_ctx).session as SessionSdk;
+  const sessionSdk = getClient(_ctx).session as SessionSdk;
 
   evaluateContinuation = (parentSessionID, sessionToken) =>
     evaluateContinuationFn(parentSessionID, sessionToken, {

+ 2 - 1
src/interview/dashboard-manager.ts

@@ -2,6 +2,7 @@ import path from 'node:path';
 import type { PluginInput } from '@opencode-ai/plugin';
 import type { PluginConfig } from '../config';
 import { log } from '../utils';
+import { getClient } from '../utils/opencode-client';
 import {
   probeDashboard,
   readDashboardAuthFile,
@@ -70,7 +71,7 @@ export function createDashboardManager(
       dashboard = await tryBecomeDashboard({
         port: dashboardPort,
         outputFolder,
-        sessionClient: ctx.client.session,
+        sessionClient: getClient(ctx).session,
       });
 
       if (dashboard) {

+ 2 - 2
src/interview/interview.test.ts

@@ -12,10 +12,10 @@ import {
 import type { InterviewAnswer } from './types';
 import { renderInterviewPage } from './ui';
 
-// Intercept getV2Client calls so service code uses the same session mocks
+// Intercept getClient calls so service code uses the same session mocks
 // that test assertions inspect.
 mock.module('../utils/opencode-client', () => ({
-  getV2Client: (ctx: any) => ({
+  getClient: (ctx: any) => ({
     session: ctx._sessionMock ?? ctx.client.session,
   }),
 }));

+ 2 - 2
src/interview/manager.test.ts

@@ -6,9 +6,9 @@ import type { PluginConfig } from '../config';
 import { readDashboardAuthFile } from './dashboard';
 import { createInterviewManager } from './manager';
 
-// Intercept getV2Client so the manager's service uses the same session mocks.
+// Intercept getClient so the manager's service uses the same session mocks.
 mock.module('../utils/opencode-client', () => ({
-  getV2Client: (ctx: any) => ({
+  getClient: (ctx: any) => ({
     session: ctx._sessionMock ?? ctx.client.session,
   }),
 }));

+ 8 - 8
src/interview/service.ts

@@ -8,7 +8,7 @@ import {
   isInternalInitiatorPart,
   log,
 } from '../utils';
-import { getV2Client } from '../utils/opencode-client';
+import { getClient } from '../utils/opencode-client';
 import { parseModelReference } from '../utils/session';
 import {
   appendInterviewAnswers,
@@ -272,7 +272,7 @@ export function createInterviewService(
   }
 
   async function loadMessages(sessionID: string): Promise<InterviewMessage[]> {
-    const result = await getV2Client(ctx).session.messages({
+    const result = await getClient(ctx).session.messages({
       sessionID,
     });
     return result.data as InterviewMessage[];
@@ -502,7 +502,7 @@ export function createInterviewService(
     // Auto-open browser on initial creation (not on every poll/refresh)
     maybeOpenBrowser(interview.id, url);
 
-    await getV2Client(ctx).session.prompt({
+    await getClient(ctx).session.prompt({
       sessionID,
       noReply: true,
       parts: [
@@ -618,7 +618,7 @@ export function createInterviewService(
       // Use promptAsync for non-blocking - returns immediately, LLM
       // processes in background. State push updates dashboard when done.
       const model = sessionModel.get(interview.sessionID);
-      await getV2Client(ctx).session.promptAsync({
+      await getClient(ctx).session.promptAsync({
         sessionID: interview.sessionID,
         agent: 'orchestrator',
         parts: [createInternalAgentTextPart(prompt)],
@@ -691,7 +691,7 @@ export function createInterviewService(
     if (sessionTitle.length > 50) {
       sessionTitle = `${sessionTitle.slice(0, 49)}…`;
     }
-    getV2Client(ctx)
+    getClient(ctx)
       .session.update({
         sessionID: input.sessionID,
         title: sessionTitle,
@@ -866,7 +866,7 @@ export function createInterviewService(
       ].join('\n');
 
       const model = sessionModel.get(interview.sessionID);
-      await getV2Client(ctx).session.promptAsync({
+      await getClient(ctx).session.promptAsync({
         sessionID: interview.sessionID,
         agent: 'orchestrator',
         parts: [createInternalAgentTextPart(prompt)],
@@ -927,7 +927,7 @@ export function createInterviewService(
       ].join('\n');
 
       const model = sessionModel.get(interview.sessionID);
-      await getV2Client(ctx).session.promptAsync({
+      await getClient(ctx).session.promptAsync({
         sessionID: interview.sessionID,
         agent: 'orchestrator',
         parts: [createInternalAgentTextPart(prompt)],
@@ -1000,7 +1000,7 @@ export function createInterviewService(
       }
 
       const model = sessionModel.get(interview.sessionID);
-      await getV2Client(ctx).session.promptAsync({
+      await getClient(ctx).session.promptAsync({
         sessionID: interview.sessionID,
         agent: 'orchestrator',
         parts: [createInternalAgentTextPart(prompt)],

+ 1 - 1
src/tools/cancel-task.test.ts

@@ -6,7 +6,7 @@ import { createCancelTaskTool } from './cancel-task';
 let mockV2Client: Record<string, unknown>;
 
 mock.module('../utils/opencode-client', () => ({
-  getV2Client: () => mockV2Client,
+  getClient: () => mockV2Client,
 }));
 
 function createTool(overrides?: {

+ 5 - 5
src/tools/cancel-task.ts

@@ -6,7 +6,7 @@ import {
 import type { BackgroundJobStore } from '../utils/background-job-store';
 import { isRecord as isObjectRecord } from '../utils/guards';
 import { log } from '../utils/logger';
-import { getV2Client } from '../utils/opencode-client';
+import { getClient } from '../utils/opencode-client';
 import { abortSessionWithTimeout, withTimeout } from '../utils/session';
 
 const z = tool.schema;
@@ -226,7 +226,7 @@ async function abortAndVerifySession(
   try {
     // ponytail: abortSessionWithTimeout now takes v2 OpencodeClient
     await abortSessionWithTimeout(
-      getV2Client(options.input),
+      getClient(options.input),
       taskID,
       options.abortTimeoutMs ?? 10_000,
     );
@@ -247,7 +247,7 @@ async function deleteAndVerifySession(
   taskID: string,
   reason: string,
 ): Promise<void> {
-  const v2 = getV2Client(options.input);
+  const v2 = getClient(options.input);
 
   log('[cancel-task] deleting session after unstable abort', {
     taskID,
@@ -332,7 +332,7 @@ async function getSessionStatus(
   keys: string[];
 }> {
   try {
-    const result = await getV2Client(input).session.status({
+    const result = await getClient(input).session.status({
       directory: input.directory,
     });
     const data = result.data;
@@ -382,7 +382,7 @@ async function getSessionParentID(
   taskID: string,
 ): Promise<string | undefined> {
   try {
-    const response = await getV2Client(input).session.get({
+    const response = await getClient(input).session.get({
       sessionID: taskID,
       directory: input.directory,
     });

+ 2 - 2
src/tools/smartfetch/secondary-model.test.ts

@@ -7,7 +7,7 @@ type PromptStep = {
   error?: Error;
 };
 
-// Mock getV2Client so internal calls use our mock v2 client.
+// Mock getClient so internal calls use our mock v2 client.
 // The variable is reassigned per-test to control behavior.
 let mockV2Client: Record<string, unknown>;
 let mockV2Session: {
@@ -20,7 +20,7 @@ let mockV2Tool: {
 };
 
 mock.module('../../utils/opencode-client', () => ({
-  getV2Client: () => mockV2Client,
+  getClient: () => mockV2Client,
 }));
 
 function createV2ClientMock(

+ 3 - 3
src/tools/smartfetch/secondary-model.ts

@@ -5,7 +5,7 @@ import type { PluginInput } from '@opencode-ai/plugin';
 import { stripJsonComments } from '../../cli/config-io';
 import { getConfigSearchDirs } from '../../cli/paths';
 import { loadPluginConfig } from '../../config/loader';
-import { getV2Client } from '../../utils/opencode-client';
+import { getClient } from '../../utils/opencode-client';
 import { MAX_MODEL_CONTENT_CHARS } from './constants';
 import type { CachedFetch, SecondaryModel } from './types';
 
@@ -180,7 +180,7 @@ async function deleteSessionSafely(
   input: PluginInput,
   sessionId: string,
 ): Promise<void> {
-  const v2 = getV2Client(input);
+  const v2 = getClient(input);
   for (let attempt = 1; attempt <= SESSION_DELETE_RETRIES; attempt++) {
     try {
       await v2.session.delete({
@@ -210,7 +210,7 @@ async function runSecondaryModel(
   prompt: string,
   content: string,
 ) {
-  const v2 = getV2Client(input);
+  const v2 = getClient(input);
   const directory = input.directory;
 
   const sessionResponse = await v2.session.create({

+ 1 - 1
src/utils/opencode-client.ts

@@ -13,7 +13,7 @@ const v2Clients = new Map<string, OpencodeClient>();
  * plugin hands us. Both clients target the same local server; session
  * state is server-side, so they observe identical sessions.
  */
-export function getV2Client(input: PluginInput): OpencodeClient {
+export function getClient(input: PluginInput): OpencodeClient {
   const cached = v2Clients.get(input.directory);
   if (cached) return cached;
   const client = createOpencodeClient({